diff --git a/README.md b/README.md index 8221185..032162c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,20 @@ To handle DNS resoution, we have implemented a local DNS resolver that intercept This plugin targets Android devices running Marshmellow (API 23), or higher. This requirement stems from calling `bindProcessToNetwork`, a [connectivity API](https://developer.android.com/reference/android/net/ConnectivityManager.html#bindProcessToNetwork(android.net.Network)) introduced in version 23, which allows the client application traffic to bypass the VPN. +### Javascript API + +`start(socksServerAddress:string) : Promise;` + +Starts the VPN service, and tunnels all the traffic to the SOCKS5 server at `socksServerAddress`. + +`stop(): Promise;` + +Stops the VPN service. + +`onDisconnect(): Promise;` + +Sets a success callback on the returned promise, to be called if the VPN service gets revoked or disconnected. + ### Code Sources We re-use and have used as a starting point open source code from [Psiphon](https://psiphon.ca/uz@Latn/open-source.html), specifically https://github.com/mei3am/ps. diff --git a/build-extras.gradle b/build-extras.gradle new file mode 100644 index 0000000..7a04871 --- /dev/null +++ b/build-extras.gradle @@ -0,0 +1,21 @@ +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + + packagingOptions { + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + } +} diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..7a222c6 --- /dev/null +++ b/plugin.xml @@ -0,0 +1,52 @@ + + + Tun2Socks + Tun2Socks for Android + Apache 2.0 + cordova,tun2socks,badvpn + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/jni/Android.mk b/src/android/jni/Android.mk new file mode 100644 index 0000000..779f289 --- /dev/null +++ b/src/android/jni/Android.mk @@ -0,0 +1 @@ +include ../badvpn/Android.mk diff --git a/src/android/jni/Application.mk b/src/android/jni/Application.mk new file mode 100644 index 0000000..9840ceb --- /dev/null +++ b/src/android/jni/Application.mk @@ -0,0 +1,2 @@ +APP_ABI := armeabi-v7a +APP_PLATFORM := android-8 diff --git a/src/android/org/uproxy/tun2socks/DnsResolverService.java b/src/android/org/uproxy/tun2socks/DnsResolverService.java new file mode 100644 index 0000000..9b71446 --- /dev/null +++ b/src/android/org/uproxy/tun2socks/DnsResolverService.java @@ -0,0 +1,286 @@ +package org.uproxy.tun2socks; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; + +import socks.Socks5Proxy; +import socks.SocksSocket; + +// This class listens on a UDP socket for incoming DNS traffic, which is proxy'd +// through SOCKS over TCP to Google's Public DNS. +public class DnsResolverService extends Service { + + private static final String LOG_TAG = "DnsResolverService"; + public static final String DNS_ADDRESS_BROADCAST = "dnsResolverAddressBroadcast"; + public static final String DNS_ADDRESS_EXTRA = "dnsResolverAddress"; + public static final String SOCKS_SERVER_ADDRESS_EXTRA = "socksServerAddress"; + + private final IBinder binder = new LocalBinder(); + private String m_socksServerAddress; + private DnsUdpToSocksResolver dnsResolver = null; + + public class LocalBinder extends Binder { + public DnsResolverService getService() { + return DnsResolverService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.i(LOG_TAG, "start command"); + m_socksServerAddress = intent.getStringExtra(SOCKS_SERVER_ADDRESS_EXTRA); + if (m_socksServerAddress == null) { + Log.e(LOG_TAG, "Failed to receive socks server address."); + } + return START_NOT_STICKY; + } + + @Override + public void onCreate() { + Log.i(LOG_TAG, "create"); + dnsResolver = new DnsUdpToSocksResolver(DnsResolverService.this); + dnsResolver.start(); + } + + @Override + public void onDestroy() { + Log.i(LOG_TAG, "destroy"); + if (dnsResolver != null) { + dnsResolver.interrupt(); + } + } + + // Parses the socks server address received on start and returns it as + // a InetSocketAddress. + public InetSocketAddress getSocksServerAddress() { + if (m_socksServerAddress == null) { + return null; + } + int separatorIndex = m_socksServerAddress.indexOf(':'); + String ip = m_socksServerAddress.substring(0, separatorIndex); + int port = Integer.parseInt(m_socksServerAddress.substring(separatorIndex + 1)); + return new InetSocketAddress(ip, port); + } + + private class DnsUdpToSocksResolver extends Thread { + private static final String LOG_TAG = "DnsUdpToSocksResolver"; + private static final String DNS_RESOLVER_IP = "8.8.8.8"; + // UDP and DNS over TCP theoretical max packet length is 64K. + private static final int MAX_BUFFER_SIZE = 65535; + private static final int DEFAULT_DNS_PORT = 53; + + // DNS bit masks + private static final byte DNS_QR = (byte) 0x80; + private static final byte DNS_TC = (byte) 0x02; + private static final byte DNS_Z = (byte) 0x70; + // DNS header constants + private static final int DNS_HEADER_SIZE = 12; + private static final int QR_OPCODE_AA_TC_OFFSET = 2; + private static final int RA_Z_R_CODE_OFFSET = 3; + private static final int QDCOUNT_OFFSET = 4; + private static final int ANCOUNT_OFFSET = 6; + private static final int NSCOUNT_OFFSET = 8; + private static final int ARCOUNT_OFFSET = 10; + + private volatile DatagramSocket udpSocket = null; + private DnsResolverService m_parentService; + + public DnsUdpToSocksResolver(DnsResolverService parentService) { + m_parentService = parentService; + } + + public void run() { + byte[] udpBuffer = new byte[MAX_BUFFER_SIZE]; + DatagramPacket udpPacket = new DatagramPacket(udpBuffer, udpBuffer.length); + InetSocketAddress socksServerAddress = + m_parentService.getSocksServerAddress(); + if (socksServerAddress == null) { + return; + } + Log.d(LOG_TAG, "SOCKS server address: " + socksServerAddress.toString()); + + Socks5Proxy socksProxy = null; + InetAddress dnsServerAddress = null; + try { + socksProxy = new Socks5Proxy(socksServerAddress.getAddress(), + socksServerAddress.getPort()); + dnsServerAddress = InetAddress.getByName(DNS_RESOLVER_IP); + udpSocket = new DatagramSocket(); + } catch (Throwable e) { + e.printStackTrace(); + return; + } + broadcastUdpSocketAddress(); + + try { + while (!isInterrupted()) { + Log.i(LOG_TAG, "listening on " + udpSocket.getLocalSocketAddress().toString()); + try { + udpSocket.receive(udpPacket); + } catch (SocketTimeoutException e) { + continue; + } catch (IOException e) { + Log.e(LOG_TAG, "Receive operation failed on udp socket: ", e); + continue; + } + Log.d( + LOG_TAG, + String.format( + "UDP: got %d bytes from %s:%d\n%s", + udpPacket.getLength(), + udpPacket.getAddress().toString(), + udpPacket.getPort(), + new String(udpBuffer, 0, udpPacket.getLength()))); + + if (!isValidDnsRequest(udpPacket)) { + Log.i(LOG_TAG, "Not a DNS request."); + continue; + } + + Socket dnsSocket = null; + DataOutputStream dnsOutputStream = null; + DataInputStream dnsInputStream = null; + try { + dnsSocket = new SocksSocket(socksProxy, dnsServerAddress, DEFAULT_DNS_PORT); + dnsSocket.setKeepAlive(true); + dnsOutputStream = new DataOutputStream(dnsSocket.getOutputStream()); + dnsInputStream = new DataInputStream(dnsSocket.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + continue; + } + if (!writeUdpPacketToStream(dnsOutputStream, udpPacket)) { + continue; + } + + byte dnsResponse[] = readStreamPayload(dnsInputStream); + if (dnsResponse == null) { + closeSocket(dnsSocket); + continue; + } + Log.d(LOG_TAG, "Got DNS response " + dnsResponse.length); + + if (!sendUdpPayload(dnsResponse, udpSocket, + udpPacket.getSocketAddress())) { + continue; + } + closeSocket(dnsSocket); + } + } finally { + if (udpSocket != null) { + udpSocket.close(); + } + } + } + + private boolean isValidDnsRequest(DatagramPacket packet) { + if (packet.getLength() < DNS_HEADER_SIZE) { + return false; + } + ByteBuffer buffer = ByteBuffer.wrap(packet.getData()); + byte qrOpcodeAaTcRd = buffer.get(QR_OPCODE_AA_TC_OFFSET); + byte raZRcode = buffer.get(RA_Z_R_CODE_OFFSET); + short qdcount = buffer.getShort(QDCOUNT_OFFSET); + short ancount = buffer.getShort(ANCOUNT_OFFSET); + short nscount = buffer.getShort(NSCOUNT_OFFSET); + + // verify DNS header is request + return (qrOpcodeAaTcRd & DNS_QR) == 0 /* query */ + && (raZRcode & DNS_Z) == 0 /* Z is Zero */ + && qdcount > 0 /* some questions */ + && nscount == 0 + && ancount == 0; /* no answers */ + } + + // Sends a UDP packet over TCP. + private boolean writeUdpPacketToStream(DataOutputStream outputStream, + DatagramPacket udpPacket) { + try { + outputStream.writeShort(udpPacket.getLength()); + outputStream.write(udpPacket.getData()); + outputStream.flush(); + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to send UDP packet: ", e); + return false; + } + return true; + } + + // Reads the payload from a TCP stream. + private byte[] readStreamPayload(DataInputStream inputStream) { + final String errorMessage = "Failed to read TCP response: "; + byte buffer[] = null; + try { + short responseBytes = inputStream.readShort(); + buffer = new byte[responseBytes]; + inputStream.readFully(buffer); + } catch (SocketException e) { + Log.e(LOG_TAG, errorMessage, e); + } catch (IOException e) { + Log.e(LOG_TAG, errorMessage, e); + } + return buffer; + } + + // Sends a UDP packet containing |payload| to |destAddress| via |udpSocket|. + private boolean sendUdpPayload(byte[] payload, DatagramSocket udpSocket, + SocketAddress destAddress) { + try { + DatagramPacket outPacket = + new DatagramPacket(payload, payload.length, destAddress); + udpSocket.send(outPacket); + return true; + } catch (IOException e) { + Log.d(LOG_TAG, "Failed to send UDP payload ", e); + } + return false; + } + + // Utility method to close a socket. + private void closeSocket(Socket socket) { + try { + if (socket != null && !socket.isClosed()) { + socket.close(); + } + } catch (IOException e) { + Log.w("Failed to close socket ", e); + } finally { + socket = null; + } + } + + private void broadcastUdpSocketAddress() { + Log.d(LOG_TAG, "Broadcasting address"); + if (udpSocket == null) { + return; + } + String dnsResolverAddress = String.format("127.0.0.1:%d", udpSocket.getLocalPort()); + Intent addressBroadcast = new Intent(DnsResolverService.DNS_ADDRESS_BROADCAST); + addressBroadcast.putExtra(DnsResolverService.DNS_ADDRESS_EXTRA, dnsResolverAddress); + LocalBroadcastManager.getInstance(m_parentService).sendBroadcast(addressBroadcast); + } + } +} diff --git a/src/android/org/uproxy/tun2socks/Tun2Socks.java b/src/android/org/uproxy/tun2socks/Tun2Socks.java new file mode 100644 index 0000000..812a63c --- /dev/null +++ b/src/android/org/uproxy/tun2socks/Tun2Socks.java @@ -0,0 +1,194 @@ +package org.uproxy.tun2socks; + +import android.annotation.TargetApi; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.VpnService; +import android.os.Build; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; + +public class Tun2Socks extends CordovaPlugin { + + private static final String LOG_TAG = "Tun2Socks"; + private static final String START_ACTION = "start"; + private static final String STOP_ACTION = "stop"; + private static final String ON_DISCONNECT_ACTION = "onDisconnect"; + private static final String DEVICE_SUPPORTS_PLUGIN_ACTION = "deviceSupportsPlugin"; + private static final int REQUEST_CODE_PREPARE_VPN = 100; + // Standard activity result: operation succeeded. + public static final int RESULT_OK = -1; + + private String m_socksServerAddress; + private CallbackContext m_onDisconnectCallback = null; + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) + throws JSONException { + if (action.equals(START_ACTION)) { + if (args.length() < 1) { + callbackContext.error("Missing socks server address argument"); + } else { + // Set instance variable in case we need to start the tunnel vpn service + // from onActivityResult. + m_socksServerAddress = args.getString(0); + Log.i(LOG_TAG, "Got socks server address: " + m_socksServerAddress); + prepareAndStartTunnelService(callbackContext); + } + return true; + } else if (action.equals(STOP_ACTION)) { + stopTunnelService(); + callbackContext.success("Stopped tun2socks."); + return true; + } else if (action.equals(ON_DISCONNECT_ACTION)) { + m_onDisconnectCallback = callbackContext; + return true; + } else if (action.equals(DEVICE_SUPPORTS_PLUGIN_ACTION)) { + callbackContext.sendPluginResult( + new PluginResult(PluginResult.Status.OK, hasVpnService())); + return true; + } + return false; + } + + // Initializes the plugin. + // Requires API 23 (Marshmallow) to call bindProcessToNetwork. + @TargetApi(Build.VERSION_CODES.M) + @Override + protected void pluginInitialize() { + // Bind process to network before establishing VPN. + ConnectivityManager cm = + (ConnectivityManager) + this.cordova.getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); + if (!cm.bindProcessToNetwork(cm.getActiveNetwork())) { + Log.e(LOG_TAG, "Failed to bind process to network."); + return; + } + + LocalBroadcastManager.getInstance(getBaseContext()) + .registerReceiver( + m_disconnectBroadcastReceiver, + new IntentFilter(TunnelVpnService.TUNNEL_VPN_DISCONNECT_BROADCAST)); + } + + @Override + public void onDestroy() { + // Stop tunnel service in case the user has quit the app without + // disconnecting the VPN. + stopTunnelService(); + } + + protected void prepareAndStartTunnelService(CallbackContext callbackContext) { + Log.d(LOG_TAG, "Starting tun2socks..."); + if (hasVpnService()) { + if (prepareVpnService()) { + startTunnelService(getBaseContext()); + } + callbackContext.success("Started tun2socks"); + } else { + Log.e(LOG_TAG, "Device does not support whole device VPN mode."); + callbackContext.error("Failed to start tun2socks"); + } + } + + // Returns whether the device supports the tunnel VPN service. + private boolean hasVpnService() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected boolean prepareVpnService() throws ActivityNotFoundException { + // VpnService: need to display OS user warning. If whole device + // option is selected and we expect to use VpnService, so show the prompt + // in the UI before starting the service. + Intent prepareVpnIntent = VpnService.prepare(getBaseContext()); + if (prepareVpnIntent != null) { + Log.d(LOG_TAG, "prepare vpn with activity"); + this.cordova.setActivityResultCallback(Tun2Socks.this); + this.cordova.getActivity().startActivityForResult( + prepareVpnIntent, REQUEST_CODE_PREPARE_VPN); + return false; + } + return true; + } + + @Override + public void onActivityResult(int request, int result, Intent data) { + if (request == REQUEST_CODE_PREPARE_VPN && result == RESULT_OK) { + startTunnelService(getBaseContext()); + } + } + + protected void startTunnelService(Context context) { + Log.i(LOG_TAG, "starting tunnel service"); + if (isServiceRunning()) { + Log.w(LOG_TAG, "already running service"); + return; + } + Intent startTunnelVpn = new Intent(context, TunnelVpnService.class); + startTunnelVpn.putExtra(TunnelManager.SOCKS_SERVER_ADDRESS_EXTRA, m_socksServerAddress); + if (this.cordova.getActivity().startService(startTunnelVpn) == null) { + Log.d(LOG_TAG, "failed to start tunnel vpn service"); + return; + } + TunnelState.getTunnelState().setStartingTunnelManager(); + } + + protected boolean isServiceRunning() { + TunnelState tunnelState = TunnelState.getTunnelState(); + return tunnelState.getStartingTunnelManager() || tunnelState.getTunnelManager() != null; + } + + private void stopTunnelService() { + // Use signalStopService to asynchronously stop the service. + // 1. VpnService doesn't respond to stopService calls + // 2. The UI will not block while waiting for stopService to return + // This scheme assumes that the UI will monitor that the service is + // running while the Activity is not bound to it. This is the state + // while the tunnel is shutting down. + Log.i(LOG_TAG, "stopping tunnel service"); + TunnelManager currentTunnelManager = TunnelState.getTunnelState().getTunnelManager(); + if (currentTunnelManager != null) { + currentTunnelManager.signalStopService(); + } + } + + private Context getBaseContext() { + return this.cordova.getActivity().getApplicationContext(); + } + + public void onDisconnect() { + if (m_onDisconnectCallback != null) { + PluginResult result = new PluginResult(PluginResult.Status.OK); + result.setKeepCallback(true); + m_onDisconnectCallback.sendPluginResult(result); + } + } + + private DisconnectBroadcastReceiver m_disconnectBroadcastReceiver = + new DisconnectBroadcastReceiver(Tun2Socks.this); + + private class DisconnectBroadcastReceiver extends BroadcastReceiver { + private Tun2Socks m_handler; + + public DisconnectBroadcastReceiver(Tun2Socks handler) { + m_handler = handler; + } + + @Override + public void onReceive(Context context, Intent intent) { + // Callback into handler so we can communicate the disconnect event to js. + m_handler.onDisconnect(); + } + }; +} diff --git a/src/android/org/uproxy/tun2socks/Tun2SocksJni.java b/src/android/org/uproxy/tun2socks/Tun2SocksJni.java index 0ce4a38..03c38e1 100644 --- a/src/android/org/uproxy/tun2socks/Tun2SocksJni.java +++ b/src/android/org/uproxy/tun2socks/Tun2SocksJni.java @@ -3,39 +3,43 @@ * Released under badvpn licence: https://github.com/ambrop72/badvpn#license */ -package ca.psiphon; - -public class Tun2Socks { - - // runTun2Socks takes a tun device file descriptor (from Android's VpnService, - // for example) and plugs it into tun2socks, which routes the tun TCP traffic - // through the specified SOCKS proxy. UDP traffic is sent to the specified - // udpgw server. - // - // The tun device file descriptor should be set to non-blocking mode. - // tun2Socks takes ownership of the tun device file descriptor and will close - // it when tun2socks is stopped. - // - // runTun2Socks blocks until tun2socks is stopped by calling terminateTun2Socks. - // It's safe to call terminateTun2Socks from a different thread. - // - // logTun2Socks is called from tun2socks when an event is to be logged. - - private native static int runTun2Socks( - int vpnInterfaceFileDescriptor, - int vpnInterfaceMTU, - String vpnIpAddress, - String vpnNetMask, - String socksServerAddress, - String udpgwServerAddress, - int udpgwTransparentDNS); - - private native static int terminateTun2Socks(); - - public static void logTun2Socks(String level, String channel, String msg) { - } - - static { - System.loadLibrary("tun2socks"); - } +package org.uproxy.tun2socks; + +import android.util.Log; + +public class Tun2SocksJni { + + // runTun2Socks takes a tun device file descriptor (from Android's VpnService, + // for example) and plugs it into tun2socks, which routes the tun TCP traffic + // through the specified SOCKS proxy. UDP traffic is sent to the specified + // udpgw server. + // + // The tun device file descriptor should be set to non-blocking mode. + // tun2Socks takes ownership of the tun device file descriptor and will close + // it when tun2socks is stopped. + // + // runTun2Socks blocks until tun2socks is stopped by calling terminateTun2Socks. + // It's safe to call terminateTun2Socks from a different thread. + // + // logTun2Socks is called from tun2socks when an event is to be logged. + + public static native int runTun2Socks( + int vpnInterfaceFileDescriptor, + int vpnInterfaceMTU, + String vpnIpAddress, + String vpnNetMask, + String socksServerAddress, + String dnsServerAddress, + int transparentDNS); + + public static native int terminateTun2Socks(); + + public static void logTun2Socks(String level, String channel, String msg) { + String logMsg = String.format("%s (%s): %s", level, channel, msg); + Log.i("Tun2Socks", logMsg); + } + + static { + System.loadLibrary("tun2socks"); + } } diff --git a/src/android/org/uproxy/tun2socks/Tunnel.java b/src/android/org/uproxy/tun2socks/Tunnel.java index c89a71b..6b17c06 100644 --- a/src/android/org/uproxy/tun2socks/Tunnel.java +++ b/src/android/org/uproxy/tun2socks/Tunnel.java @@ -17,795 +17,325 @@ * */ -package ca.psiphon; +package org.uproxy.tun2socks; import android.annotation.TargetApi; import android.content.Context; -import android.net.ConnectivityManager; -import android.net.LinkProperties; -import android.net.NetworkInfo; import android.net.VpnService; import android.os.Build; import android.os.ParcelFileDescriptor; -import android.telephony.TelephonyManager; -import android.util.Base64; -import org.apache.http.conn.util.InetAddressUtils; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.PrintStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import go.psi.Psi; - -public class PsiphonTunnel extends Psi.PsiphonProvider.Stub { - - public interface HostService { - public String getAppName(); - public Context getContext(); - public Object getVpnService(); // Object must be a VpnService (Android < 4 cannot reference this class name) - public Object newVpnServiceBuilder(); // Object must be a VpnService.Builder (Android < 4 cannot reference this class name) - public String getPsiphonConfig(); - public void onDiagnosticMessage(String message); - public void onAvailableEgressRegions(List regions); - public void onSocksProxyPortInUse(int port); - public void onHttpProxyPortInUse(int port); - public void onListeningSocksProxyPort(int port); - public void onListeningHttpProxyPort(int port); - public void onUpstreamProxyError(String message); - public void onConnecting(); - public void onConnected(); - public void onHomepage(String url); - public void onClientRegion(String region); - public void onClientUpgradeDownloaded(String filename); - public void onClientIsLatestVersion(); - public void onSplitTunnelRegion(String region); - public void onUntunneledAddress(String address); - public void onBytesTransferred(long sent, long received); - public void onStartedWaitingForNetworkConnectivity(); - public void onClientVerificationRequired(String serverNonce, int ttlSeconds, boolean resetCache); - public void onExiting(); - } +public class Tunnel { - private final HostService mHostService; - private AtomicBoolean mVpnMode; - private PrivateAddress mPrivateAddress; - private AtomicReference mTunFd; - private AtomicInteger mLocalSocksProxyPort; - private AtomicBoolean mRoutingThroughTunnel; - private Thread mTun2SocksThread; - private AtomicBoolean mIsWaitingForNetworkConnectivity; - - // Only one PsiphonVpn instance may exist at a time, as the underlying - // go.psi.Psi and tun2socks implementations each contain global state. - private static PsiphonTunnel mPsiphonTunnel; - - public static synchronized PsiphonTunnel newPsiphonTunnel(HostService hostService) { - if (mPsiphonTunnel != null) { - mPsiphonTunnel.stop(); + public interface HostService { + public String getAppName(); + + public Context getContext(); + + // Object must be a VpnService; Android < 4 cannot reference this class name + public Object getVpnService(); + + // Object must be a VpnService.Builder; + // Android < 4 cannot reference this class name + public Object newVpnServiceBuilder(); + + public void onDiagnosticMessage(String message); + + public void onTunnelConnected(); + + public void onVpnEstablished(); + } + + private final HostService mHostService; + private PrivateAddress mPrivateAddress; + private AtomicReference mTunFd; + private AtomicBoolean mRoutingThroughTunnel; + private Thread mTun2SocksThread; + + // Only one VpnService instance may exist at a time, as the underlying + // tun2socks implementation contains global state. + private static Tunnel mTunnel; + + public static synchronized Tunnel newTunnel(HostService hostService) { + if (mTunnel != null) { + mTunnel.stop(); + } + mTunnel = new Tunnel(hostService); + return mTunnel; + } + + private Tunnel(HostService hostService) { + mHostService = hostService; + mTunFd = new AtomicReference(); + mRoutingThroughTunnel = new AtomicBoolean(false); + } + + public Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + //---------------------------------------------------------------------------------------------- + // Public API + //---------------------------------------------------------------------------------------------- + + // To start, call in sequence: startRouting(), then startTunneling(). After startRouting() + // succeeds, the caller must call stop() to clean up. + + // Returns true when the VPN routing is established; returns false if the VPN could not + // be started due to lack of prepare or revoked permissions (called should re-prepare and + // try again); throws exception for other error conditions. + public synchronized boolean startRouting() throws Exception { + return startVpn(); + } + + // Starts tun2socks. Returns true on success. + public synchronized boolean startTunneling(String socksServerAddress, String dnsServerAddress) + throws Exception { + return routeThroughTunnel(socksServerAddress, dnsServerAddress); + } + + // Note: to avoid deadlock, do not call directly from a HostService callback; + // instead post to a Handler if necessary to trigger from a HostService callback. + public synchronized void stop() { + stopVpn(); + } + + //---------------------------------------------------------------------------- + // VPN Routing + //---------------------------------------------------------------------------- + + private static final String VPN_INTERFACE_NETMASK = "255.255.255.0"; + private static final int VPN_INTERFACE_MTU = 1500; + + // Note: Atomic variables used for getting/setting local proxy port, routing flag, and + // tun fd, as these functions may be called via callbacks. Do not use + // synchronized functions as stop() is synchronized and a deadlock is possible as callbacks + // can be called while stop holds the lock. + // + // Calling allowBypass on VPNService.Builder requires API 21 (Lollipop). + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private boolean startVpn() throws Exception { + mPrivateAddress = selectPrivateAddress(); + + Locale previousLocale = Locale.getDefault(); + + final String errorMessage = "startVpn failed"; + try { + // Workaround for https://code.google.com/p/android/issues/detail?id=61096 + Locale.setDefault(new Locale("en")); + + ParcelFileDescriptor tunFd = + ((VpnService.Builder) mHostService.newVpnServiceBuilder()) + .setSession(mHostService.getAppName()) + .setMtu(VPN_INTERFACE_MTU) + .addAddress(mPrivateAddress.mIpAddress, mPrivateAddress.mPrefixLength) + .addRoute("0.0.0.0", 0) + .addRoute(mPrivateAddress.mSubnet, mPrivateAddress.mPrefixLength) + .addDnsServer(mPrivateAddress.mRouter) + .allowBypass() + .establish(); + if (tunFd == null) { + // As per http://developer.android.com/reference/android/net/VpnService.Builder.html#establish%28%29, + // this application is no longer prepared or was revoked. + return false; + } + mTunFd.set(tunFd); + mRoutingThroughTunnel.set(false); + mHostService.onVpnEstablished(); + + } catch (IllegalArgumentException e) { + throw new Exception(errorMessage, e); + } catch (SecurityException e) { + throw new Exception(errorMessage, e); + } catch (IllegalStateException e) { + throw new Exception(errorMessage, e); + } finally { + // Restore the original locale. + Locale.setDefault(previousLocale); + } + + return true; + } + + private boolean routeThroughTunnel(String socksServerAddress, String dnsServerAddress) { + if (!mRoutingThroughTunnel.compareAndSet(false, true)) { + return false; + } + ParcelFileDescriptor tunFd = mTunFd.getAndSet(null); + if (tunFd == null) { + return false; + } + + startTun2Socks( + tunFd, + VPN_INTERFACE_MTU, + mPrivateAddress.mRouter, + VPN_INTERFACE_NETMASK, + socksServerAddress, + dnsServerAddress, + true /* transparent DNS */); + + mHostService.onTunnelConnected(); + mHostService.onDiagnosticMessage("routing through tunnel"); + + // TODO: should double-check tunnel routing; see: + // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/1dc5e4257dca99790109f3bf374e8ab3a0ead4d7/Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelCore.java?at=default#cl-779 + return true; + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void protectSocket(long fileDescriptor) { + if (!((VpnService) mHostService.getVpnService()).protect((int) fileDescriptor)) { + mHostService.onDiagnosticMessage("protect socket failed"); + } + } + + private void stopVpn() { + stopTun2Socks(); + ParcelFileDescriptor tunFd = mTunFd.getAndSet(null); + if (tunFd != null) { + try { + tunFd.close(); + } catch (IOException e) { + } + } + mRoutingThroughTunnel.set(false); + } + + //---------------------------------------------------------------------------- + // Tun2Socks + //---------------------------------------------------------------------------- + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) + private void startTun2Socks( + final ParcelFileDescriptor vpnInterfaceFileDescriptor, + final int vpnInterfaceMTU, + final String vpnIpAddress, + final String vpnNetMask, + final String socksServerAddress, + final String dnsServerAddress, + final boolean transparentDns) { + if (mTun2SocksThread != null) { + return; + } + mTun2SocksThread = + new Thread( + new Runnable() { + @Override + public void run() { + Tun2SocksJni.runTun2Socks( + vpnInterfaceFileDescriptor.detachFd(), + vpnInterfaceMTU, + vpnIpAddress, + vpnNetMask, + socksServerAddress, + dnsServerAddress, + transparentDns ? 1 : 0); + } + }); + mTun2SocksThread.start(); + mHostService.onDiagnosticMessage("tun2socks started"); + } + + private void stopTun2Socks() { + if (mTun2SocksThread != null) { + try { + Tun2SocksJni.terminateTun2Socks(); + mTun2SocksThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + mTun2SocksThread = null; + mHostService.onDiagnosticMessage("tun2socks stopped"); + } + } + + //---------------------------------------------------------------------------- + // Implementation: Network Utils + //---------------------------------------------------------------------------- + + private static class PrivateAddress { + public final String mIpAddress; + public final String mSubnet; + public final int mPrefixLength; + public final String mRouter; + + public PrivateAddress(String ipAddress, String subnet, int prefixLength, String router) { + mIpAddress = ipAddress; + mSubnet = subnet; + mPrefixLength = prefixLength; + mRouter = router; + } + } + + private static PrivateAddress selectPrivateAddress() throws Exception { + // Select one of 10.0.0.1, 172.16.0.1, or 192.168.0.1 depending on + // which private address range isn't in use. + Map candidates = new HashMap(); + candidates.put("10", new PrivateAddress("10.0.0.1", "10.0.0.0", 8, "10.0.0.2")); + candidates.put("172", new PrivateAddress("172.16.0.1", "172.16.0.0", 12, "172.16.0.2")); + candidates.put("192", new PrivateAddress("192.168.0.1", "192.168.0.0", 16, "192.168.0.2")); + candidates.put("169", new PrivateAddress("169.254.1.1", "169.254.1.0", 24, "169.254.1.2")); + + List netInterfaces; + try { + netInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + } catch (SocketException e) { + e.printStackTrace(); + throw new Exception("selectPrivateAddress failed", e); + } + + for (NetworkInterface netInterface : netInterfaces) { + for (InetAddress inetAddress : Collections.list(netInterface.getInetAddresses())) { + + if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) { + String ipAddress = inetAddress.getHostAddress(); + if (ipAddress.startsWith("10.")) { + candidates.remove("10"); + } else if (ipAddress.length() >= 6 + && ipAddress.substring(0, 6).compareTo("172.16") >= 0 + && ipAddress.substring(0, 6).compareTo("172.31") <= 0) { + candidates.remove("172"); + } else if (ipAddress.startsWith("192.168")) { + candidates.remove("192"); + } } - // Load the native go code embedded in psi.aar - System.loadLibrary("gojni"); - mPsiphonTunnel = new PsiphonTunnel(hostService); - return mPsiphonTunnel; - } - - private PsiphonTunnel(HostService hostService) { - mHostService = hostService; - mVpnMode = new AtomicBoolean(false); - mTunFd = new AtomicReference(); - mLocalSocksProxyPort = new AtomicInteger(0); - mRoutingThroughTunnel = new AtomicBoolean(false); - mIsWaitingForNetworkConnectivity = new AtomicBoolean(false); - } - - public Object clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException(); - } - - //---------------------------------------------------------------------------------------------- - // Public API - //---------------------------------------------------------------------------------------------- - - // To start, call in sequence: startRouting(), then startTunneling(). After startRouting() - // succeeds, the caller must call stop() to clean up. - - // Returns true when the VPN routing is established; returns false if the VPN could not - // be started due to lack of prepare or revoked permissions (called should re-prepare and - // try again); throws exception for other error conditions. - public synchronized boolean startRouting() throws Exception { - return startVpn(); + } } - // Throws an exception in error conditions. In the case of an exception, the routing - // started by startRouting() is not immediately torn down (this allows the caller to control - // exactly when VPN routing is stopped); caller should call stop() to clean up. - public synchronized void startTunneling(String embeddedServerEntries) throws Exception { - startPsiphon(embeddedServerEntries); + if (candidates.size() > 0) { + return candidates.values().iterator().next(); } - // Note: to avoid deadlock, do not call directly from a HostService callback; - // instead post to a Handler if necessary to trigger from a HostService callback. - // For example, deadlock can occur when a Notice callback invokes stop() since stop() calls - // Psi.Stop() which will block waiting for tunnel-core Controller to shutdown which in turn - // waits for Notice callback invoker to stop, meanwhile the callback thread has blocked waiting - // for stop(). - public synchronized void stop() { - stopVpn(); - stopPsiphon(); - mVpnMode.set(false); - mLocalSocksProxyPort.set(0); - } + throw new Exception("no private address available"); + } - // Note: same deadlock note as stop(). - public synchronized void restartPsiphon() throws Exception { - stopPsiphon(); - startPsiphon(""); - } + //---------------------------------------------------------------------------- + // Exception + //---------------------------------------------------------------------------- - // Call through to tunnel-core Controller.SetClientVerificationPayload. See description in - // Controller.SetClientVerificationPayload. - // Note: same deadlock note as stop(). - // Note: this function only has an effect after Psi.Start() and before Psi.Stop(), - // so call it after startTunneling() and before stop(). - public synchronized void setClientVerificationPayload (String requestPayload) { - Psi.SetClientVerificationPayload(requestPayload); - } + public static class Exception extends java.lang.Exception { + private static final long serialVersionUID = 1L; - //---------------------------------------------------------------------------------------------- - // VPN Routing - //---------------------------------------------------------------------------------------------- - - private final static String VPN_INTERFACE_NETMASK = "255.255.255.0"; - private final static int VPN_INTERFACE_MTU = 1500; - private final static int UDPGW_SERVER_PORT = 7300; - private final static String DEFAULT_PRIMARY_DNS_SERVER = "8.8.4.4"; - private final static String DEFAULT_SECONDARY_DNS_SERVER = "8.8.8.8"; - - // Note: Atomic variables used for getting/setting local proxy port, routing flag, and - // tun fd, as these functions may be called via PsiphonProvider callbacks. Do not use - // synchronized functions as stop() is synchronized and a deadlock is possible as callbacks - // can be called while stop holds the lock. - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - private boolean startVpn() throws Exception { - - mVpnMode.set(true); - mPrivateAddress = selectPrivateAddress(); - - Locale previousLocale = Locale.getDefault(); - - final String errorMessage = "startVpn failed"; - try { - // Workaround for https://code.google.com/p/android/issues/detail?id=61096 - Locale.setDefault(new Locale("en")); - - ParcelFileDescriptor tunFd = - ((VpnService.Builder) mHostService.newVpnServiceBuilder()) - .setSession(mHostService.getAppName()) - .setMtu(VPN_INTERFACE_MTU) - .addAddress(mPrivateAddress.mIpAddress, mPrivateAddress.mPrefixLength) - .addRoute("0.0.0.0", 0) - .addRoute(mPrivateAddress.mSubnet, mPrivateAddress.mPrefixLength) - .addDnsServer(mPrivateAddress.mRouter) - .establish(); - if (tunFd == null) { - // As per http://developer.android.com/reference/android/net/VpnService.Builder.html#establish%28%29, - // this application is no longer prepared or was revoked. - return false; - } - mTunFd.set(tunFd); - mRoutingThroughTunnel.set(false); - - mHostService.onDiagnosticMessage("VPN established"); - - } catch(IllegalArgumentException e) { - throw new Exception(errorMessage, e); - } catch(IllegalStateException e) { - throw new Exception(errorMessage, e); - } catch(SecurityException e) { - throw new Exception(errorMessage, e); - } finally { - // Restore the original locale. - Locale.setDefault(previousLocale); - } - - return true; - } - - private boolean isVpnMode() { - return mVpnMode.get(); - } - - private void setLocalSocksProxyPort(int port) { - mLocalSocksProxyPort.set(port); - } - - private void routeThroughTunnel() { - if (!mRoutingThroughTunnel.compareAndSet(false, true)) { - return; - } - ParcelFileDescriptor tunFd = mTunFd.getAndSet(null); - if (tunFd == null) { - return; - } - String socksServerAddress = "127.0.0.1:" + Integer.toString(mLocalSocksProxyPort.get()); - String udpgwServerAddress = "127.0.0.1:" + Integer.toString(UDPGW_SERVER_PORT); - startTun2Socks( - tunFd, - VPN_INTERFACE_MTU, - mPrivateAddress.mRouter, - VPN_INTERFACE_NETMASK, - socksServerAddress, - udpgwServerAddress, - true); - mHostService.onDiagnosticMessage("routing through tunnel"); - - // TODO: should double-check tunnel routing; see: - // https://bitbucket.org/psiphon/psiphon-circumvention-system/src/1dc5e4257dca99790109f3bf374e8ab3a0ead4d7/Android/PsiphonAndroidLibrary/src/com/psiphon3/psiphonlibrary/TunnelCore.java?at=default#cl-779 - } - - private void stopVpn() { - stopTun2Socks(); - ParcelFileDescriptor tunFd = mTunFd.getAndSet(null); - if (tunFd != null) { - try { - tunFd.close(); - } catch (IOException e) { - } - } - mRoutingThroughTunnel.set(false); - } - - //---------------------------------------------------------------------------------------------- - // PsiphonProvider (Core support) interface implementation - //---------------------------------------------------------------------------------------------- - - @Override - public void Notice(String noticeJSON) { - handlePsiphonNotice(noticeJSON); + public Exception(String message) { + super(message); } - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - @Override - public void BindToDevice(long fileDescriptor) throws Exception { - if (!((VpnService)mHostService.getVpnService()).protect((int)fileDescriptor)) { - throw new Exception("protect socket failed"); - } - } - - @Override - public long HasNetworkConnectivity() { - boolean hasConnectivity = hasNetworkConnectivity(mHostService.getContext()); - boolean wasWaitingForNetworkConnectivity = mIsWaitingForNetworkConnectivity.getAndSet(!hasConnectivity); - if (!hasConnectivity && !wasWaitingForNetworkConnectivity) { - // HasNetworkConnectivity may be called many times, but only call - // onStartedWaitingForNetworkConnectivity once per loss of connectivity, - // so the HostService may log a single message. - mHostService.onStartedWaitingForNetworkConnectivity(); - } - // TODO: change to bool return value once gobind supports that type - return hasConnectivity ? 1 : 0; - } - - @Override - public String GetPrimaryDnsServer() { - String dnsResolver = null; - try { - dnsResolver = getFirstActiveNetworkDnsResolver(mHostService.getContext()); - } catch (Exception e) { - mHostService.onDiagnosticMessage("failed to get active network DNS resolver: " + e.getMessage()); - dnsResolver = DEFAULT_PRIMARY_DNS_SERVER; - } - return dnsResolver; - } - - @Override - public String GetSecondaryDnsServer() { - return DEFAULT_SECONDARY_DNS_SERVER; - } - - //---------------------------------------------------------------------------------------------- - // Psiphon Tunnel Core - //---------------------------------------------------------------------------------------------- - - private void startPsiphon(String embeddedServerEntries) throws Exception { - stopPsiphon(); - mHostService.onDiagnosticMessage("starting Psiphon library"); - try { - Psi.Start( - loadPsiphonConfig(mHostService.getContext()), - embeddedServerEntries, - this, - isVpnMode()); - } catch (java.lang.Exception e) { - throw new Exception("failed to start Psiphon library", e); - } - mHostService.onDiagnosticMessage("Psiphon library started"); - } - - private void stopPsiphon() { - mHostService.onDiagnosticMessage("stopping Psiphon library"); - Psi.Stop(); - mHostService.onDiagnosticMessage("Psiphon library stopped"); - } - - private String loadPsiphonConfig(Context context) - throws IOException, JSONException { - - // Load settings from the raw resource JSON config file and - // update as necessary. Then write JSON to disk for the Go client. - JSONObject json = new JSONObject(mHostService.getPsiphonConfig()); - - // On Android, this directory must be set to the app private storage area. - // The Psiphon library won't be able to use its current working directory - // and the standard temporary directories do not exist. - if (!json.has("DataStoreDirectory")) { - json.put("DataStoreDirectory", context.getFilesDir()); - } - - if (!json.has("RemoteServerListDownloadFilename")) { - File remoteServerListDownload = new File(context.getFilesDir(), "remote_server_list"); - json.put("RemoteServerListDownloadFilename", remoteServerListDownload.getAbsolutePath()); - } - - // Note: onConnecting/onConnected logic assumes 1 tunnel connection - json.put("TunnelPoolSize", 1); - - // Continue to run indefinitely until connected - if (!json.has("EstablishTunnelTimeoutSeconds")) { - json.put("EstablishTunnelTimeoutSeconds", 0); - } - - // This parameter is for stats reporting - if (!json.has("TunnelWholeDevice")) { - json.put("TunnelWholeDevice", isVpnMode() ? 1 : 0); - } - - json.put("EmitBytesTransferred", true); - - if (mLocalSocksProxyPort.get() != 0 && !json.has("LocalSocksProxyPort")) { - // When mLocalSocksProxyPort is set, tun2socks is already configured - // to use that port value. So we force use of the same port. - // A side-effect of this is that changing the SOCKS port preference - // has no effect with restartPsiphon(), a full stop() is necessary. - json.put("LocalSocksProxyPort", mLocalSocksProxyPort); - } - - json.put("UseIndistinguishableTLS", true); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - json.put("UseTrustedCACertificatesForStockTLS", true); - } - - try { - // Also enable indistinguishable TLS for HTTPS requests that - // require system CAs. - json.put( - "TrustedCACertificatesFilename", - setupTrustedCertificates(mHostService.getContext())); - } catch (Exception e) { - mHostService.onDiagnosticMessage(e.getMessage()); - } - - json.put("DeviceRegion", getDeviceRegion(mHostService.getContext())); - - return json.toString(); - } - - private void handlePsiphonNotice(String noticeJSON) { - try { - // All notices are sent on as diagnostic messages - // except those that may contain private user data. - boolean diagnostic = true; - - JSONObject notice = new JSONObject(noticeJSON); - String noticeType = notice.getString("noticeType"); - - if (noticeType.equals("Tunnels")) { - int count = notice.getJSONObject("data").getInt("count"); - if (count > 0) { - if (isVpnMode()) { - routeThroughTunnel(); - } - mHostService.onConnected(); - } else { - mHostService.onConnecting(); - } - - } else if (noticeType.equals("AvailableEgressRegions")) { - JSONArray egressRegions = notice.getJSONObject("data").getJSONArray("regions"); - ArrayList regions = new ArrayList(); - for (int i=0; i= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - keyStore = KeyStore.getInstance("AndroidCAStore"); - keyStore.load(null, null); - } else { - keyStore = KeyStore.getInstance("BKS"); - FileInputStream inputStream = new FileInputStream("/etc/security/cacerts.bks"); - try { - keyStore.load(inputStream, "changeit".toCharArray()); - } finally { - if (inputStream != null) { - inputStream.close(); - } - } - } - - Enumeration aliases = keyStore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); - - output.println("-----BEGIN CERTIFICATE-----"); - String pemCert = new String(Base64.encode(cert.getEncoded(), Base64.NO_WRAP), "UTF-8"); - // OpenSSL appears to reject the default linebreaking done by Base64.encode, - // so we manually linebreak every 64 characters - for (int i = 0; i < pemCert.length() ; i+= 64) { - output.println(pemCert.substring(i, Math.min(i + 64, pemCert.length()))); - } - output.println("-----END CERTIFICATE-----"); - } - - mHostService.onDiagnosticMessage("prepared PsiphonCAStore"); - - return file.getAbsolutePath(); - - } finally { - if (output != null) { - output.close(); - } - } - - } catch (KeyStoreException e) { - throw new Exception(errorMessage, e); - } catch (NoSuchAlgorithmException e) { - throw new Exception(errorMessage, e); - } catch (CertificateException e) { - throw new Exception(errorMessage, e); - } catch (IOException e) { - throw new Exception(errorMessage, e); - } - } - - private static String getDeviceRegion(Context context) { - String region = ""; - TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager != null) { - region = telephonyManager.getSimCountryIso(); - if (region == null) { - region = ""; - } - if (region.length() == 0 && telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { - region = telephonyManager.getNetworkCountryIso(); - if (region == null) { - region = ""; - } - } - } - if (region.length() == 0) { - Locale defaultLocale = Locale.getDefault(); - if (defaultLocale != null) { - region = defaultLocale.getCountry(); - } - } - return region.toUpperCase(Locale.US); - } - - //---------------------------------------------------------------------------------------------- - // Tun2Socks - //---------------------------------------------------------------------------------------------- - - @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) - private void startTun2Socks( - final ParcelFileDescriptor vpnInterfaceFileDescriptor, - final int vpnInterfaceMTU, - final String vpnIpAddress, - final String vpnNetMask, - final String socksServerAddress, - final String udpgwServerAddress, - final boolean udpgwTransparentDNS) { - if (mTun2SocksThread != null) { - return; - } - mTun2SocksThread = new Thread(new Runnable() { - @Override - public void run() { - runTun2Socks( - vpnInterfaceFileDescriptor.detachFd(), - vpnInterfaceMTU, - vpnIpAddress, - vpnNetMask, - socksServerAddress, - udpgwServerAddress, - udpgwTransparentDNS ? 1 : 0); - } - }); - mTun2SocksThread.start(); - mHostService.onDiagnosticMessage("tun2socks started"); - } - - private void stopTun2Socks() { - if (mTun2SocksThread != null) { - try { - terminateTun2Socks(); - mTun2SocksThread.join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - mTun2SocksThread = null; - mHostService.onDiagnosticMessage("tun2socks stopped"); - } - } - - public static void logTun2Socks(String level, String channel, String msg) { - String logMsg = "tun2socks: " + level + "(" + channel + "): " + msg; - mPsiphonTunnel.mHostService.onDiagnosticMessage(logMsg); - } - - private native static int runTun2Socks( - int vpnInterfaceFileDescriptor, - int vpnInterfaceMTU, - String vpnIpAddress, - String vpnNetMask, - String socksServerAddress, - String udpgwServerAddress, - int udpgwTransparentDNS); - - private native static int terminateTun2Socks(); - - static { - System.loadLibrary("tun2socks"); - } - - //---------------------------------------------------------------------------------------------- - // Implementation: Network Utils - //---------------------------------------------------------------------------------------------- - - private static boolean hasNetworkConnectivity(Context context) { - ConnectivityManager connectivityManager = - (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivityManager == null) { - return false; - } - NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); - return networkInfo != null && networkInfo.isConnected(); - } - - private static class PrivateAddress { - final public String mIpAddress; - final public String mSubnet; - final public int mPrefixLength; - final public String mRouter; - public PrivateAddress(String ipAddress, String subnet, int prefixLength, String router) { - mIpAddress = ipAddress; - mSubnet = subnet; - mPrefixLength = prefixLength; - mRouter = router; - } - } - - private static PrivateAddress selectPrivateAddress() throws Exception { - // Select one of 10.0.0.1, 172.16.0.1, or 192.168.0.1 depending on - // which private address range isn't in use. - - Map candidates = new HashMap(); - candidates.put( "10", new PrivateAddress("10.0.0.1", "10.0.0.0", 8, "10.0.0.2")); - candidates.put("172", new PrivateAddress("172.16.0.1", "172.16.0.0", 12, "172.16.0.2")); - candidates.put("192", new PrivateAddress("192.168.0.1", "192.168.0.0", 16, "192.168.0.2")); - candidates.put("169", new PrivateAddress("169.254.1.1", "169.254.1.0", 24, "169.254.1.2")); - - List netInterfaces; - try { - netInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); - } catch (SocketException e) { - throw new Exception("selectPrivateAddress failed", e); - } - - for (NetworkInterface netInterface : netInterfaces) { - for (InetAddress inetAddress : Collections.list(netInterface.getInetAddresses())) { - String ipAddress = inetAddress.getHostAddress(); - if (InetAddressUtils.isIPv4Address(ipAddress)) { - if (ipAddress.startsWith("10.")) { - candidates.remove("10"); - } - else if ( - ipAddress.length() >= 6 && - ipAddress.substring(0, 6).compareTo("172.16") >= 0 && - ipAddress.substring(0, 6).compareTo("172.31") <= 0) { - candidates.remove("172"); - } - else if (ipAddress.startsWith("192.168")) { - candidates.remove("192"); - } - } - } - } - - if (candidates.size() > 0) { - return candidates.values().iterator().next(); - } - - throw new Exception("no private address available"); - } - - public static String getFirstActiveNetworkDnsResolver(Context context) - throws Exception { - Collection dnsResolvers = getActiveNetworkDnsResolvers(context); - if (!dnsResolvers.isEmpty()) { - // strip the leading slash e.g., "/192.168.1.1" - String dnsResolver = dnsResolvers.iterator().next().toString(); - if (dnsResolver.startsWith("/")) { - dnsResolver = dnsResolver.substring(1); - } - return dnsResolver; - } - throw new Exception("no active network DNS resolver"); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static Collection getActiveNetworkDnsResolvers(Context context) - throws Exception { - final String errorMessage = "getActiveNetworkDnsResolvers failed"; - ArrayList dnsAddresses = new ArrayList(); - try { - // Hidden API - // - only available in Android 4.0+ - // - no guarantee will be available beyond 4.2, or on all vendor devices - ConnectivityManager connectivityManager = - (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - Class LinkPropertiesClass = Class.forName("android.net.LinkProperties"); - Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{}); - Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager); - if (linkProperties != null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{}); - Collection dnses = (Collection)getDnsesMethod.invoke(linkProperties); - for (Object dns : dnses) { - dnsAddresses.add((InetAddress)dns); - } - } else { - // LinkProperties is public in API 21 (and the DNS function signature has changed) - for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) { - dnsAddresses.add(dns); - } - } - } - } catch (ClassNotFoundException e) { - throw new Exception(errorMessage, e); - } catch (NoSuchMethodException e) { - throw new Exception(errorMessage, e); - } catch (IllegalArgumentException e) { - throw new Exception(errorMessage, e); - } catch (IllegalAccessException e) { - throw new Exception(errorMessage, e); - } catch (InvocationTargetException e) { - throw new Exception(errorMessage, e); - } catch (NullPointerException e) { - throw new Exception(errorMessage, e); - } - - return dnsAddresses; - } - - //---------------------------------------------------------------------------------------------- - // Exception - //---------------------------------------------------------------------------------------------- - - public static class Exception extends java.lang.Exception { - private static final long serialVersionUID = 1L; - public Exception(String message) { - super(message); - } - public Exception(String message, Throwable cause) { - super(message + ": " + cause.getMessage()); - } + public Exception(String message, Throwable cause) { + super(message + ": " + cause.getMessage()); } + } } diff --git a/src/android/org/uproxy/tun2socks/TunnelManager.java b/src/android/org/uproxy/tun2socks/TunnelManager.java index b8b494e..f7b8eed 100644 --- a/src/android/org/uproxy/tun2socks/TunnelManager.java +++ b/src/android/org/uproxy/tun2socks/TunnelManager.java @@ -1,3 +1,5 @@ +package org.uproxy.tun2socks; + /* * Copyright (c) 2016, Psiphon Inc. * All rights reserved. @@ -17,931 +19,213 @@ * */ -package com.psiphon3.psiphonlibrary; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; +import android.annotation.TargetApi; import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.net.Uri; +import android.content.IntentFilter; import android.net.VpnService; -import android.net.VpnService.Builder; -import android.os.Bundle; -import android.os.Handler; +import android.os.Build; import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; -import android.support.v4.app.NotificationCompat; - -import com.psiphon3.R; -import com.psiphon3.psiphonlibrary.Utils.MyLog; - -import net.grandcentrix.tray.AppPreferences; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; -import ca.psiphon.PsiphonTunnel; - -public class TunnelManager implements PsiphonTunnel.HostService, MyLog.ILogger { - private static final int MAX_CLIENT_VERIFICATION_ATTEMPTS = 5; - private int m_clientVerificationAttempts = 0; - - - // Android IPC messages - - // Client -> Service - public static final int MSG_REGISTER = 0; - public static final int MSG_UNREGISTER = 1; - public static final int MSG_STOP_SERVICE = 2; - - // Service -> Client - public static final int MSG_REGISTER_RESPONSE = 3; - public static final int MSG_KNOWN_SERVER_REGIONS = 4; - public static final int MSG_TUNNEL_STARTING = 5; - public static final int MSG_TUNNEL_STOPPING = 6; - public static final int MSG_TUNNEL_CONNECTION_STATE = 7; - public static final int MSG_CLIENT_REGION = 8; - public static final int MSG_DATA_TRANSFER_STATS = 9; - - public static final String INTENT_ACTION_HANDSHAKE = "com.psiphon3.psiphonlibrary.TunnelManager.HANDSHAKE"; - - // Service -> Client bundle parameter names - public static final String DATA_TUNNEL_STATE_AVAILABLE_EGRESS_REGIONS = "availableEgressRegions"; - public static final String DATA_TUNNEL_STATE_IS_CONNECTED = "isConnected"; - public static final String DATA_TUNNEL_STATE_LISTENING_LOCAL_SOCKS_PROXY_PORT = "listeningLocalSocksProxyPort"; - public static final String DATA_TUNNEL_STATE_LISTENING_LOCAL_HTTP_PROXY_PORT = "listeningLocalHttpProxyPort"; - public static final String DATA_TUNNEL_STATE_CLIENT_REGION = "clientRegion"; - public static final String DATA_TUNNEL_STATE_HOME_PAGES = "homePages"; - public static final String DATA_TRANSFER_STATS_CONNECTED_TIME = "dataTransferStatsConnectedTime"; - public static final String DATA_TRANSFER_STATS_TOTAL_BYTES_SENT = "dataTransferStatsTotalBytesSent"; - public static final String DATA_TRANSFER_STATS_TOTAL_BYTES_RECEIVED = "dataTransferStatsTotalBytesReceived"; - public static final String DATA_TRANSFER_STATS_SLOW_BUCKETS = "dataTransferStatsSlowBuckets"; - public static final String DATA_TRANSFER_STATS_SLOW_BUCKETS_LAST_START_TIME = "dataTransferStatsSlowBucketsLastStartTime"; - public static final String DATA_TRANSFER_STATS_FAST_BUCKETS = "dataTransferStatsFastBuckets"; - public static final String DATA_TRANSFER_STATS_FAST_BUCKETS_LAST_START_TIME = "dataTransferStatsFastBucketsLastStartTime"; - - // Extras in handshake intent - public static final String DATA_HANDSHAKE_IS_RECONNECT = "isReconnect"; - - // Extras in start service intent (Client -> Service) - public static final String DATA_TUNNEL_CONFIG_HANDSHAKE_PENDING_INTENT = "tunnelConfigHandshakePendingIntent"; - public static final String DATA_TUNNEL_CONFIG_NOTIFICATION_PENDING_INTENT = "tunnelConfigNotificationPendingIntent"; - public static final String DATA_TUNNEL_CONFIG_WHOLE_DEVICE = "tunnelConfigWholeDevice"; - public static final String DATA_TUNNEL_CONFIG_EGRESS_REGION = "tunnelConfigEgressRegion"; - public static final String DATA_TUNNEL_CONFIG_DISABLE_TIMEOUTS = "tunnelConfigDisableTimeouts"; - - // Tunnel config, received from the client. - public static class Config { - PendingIntent handshakePendingIntent = null; - PendingIntent notificationPendingIntent = null; - boolean wholeDevice = false; - String egressRegion = PsiphonConstants.REGION_CODE_ANY; - boolean disableTimeouts = false; - } - - private Config m_tunnelConfig = new Config(); - - // Shared tunnel state, sent to the client in the HANDSHAKE - // intent and various state-related Messages. - public static class State { - ArrayList availableEgressRegions = new ArrayList<>(); - boolean isConnected = false; - int listeningLocalSocksProxyPort = 0; - int listeningLocalHttpProxyPort = 0; - String clientRegion; - ArrayList homePages = new ArrayList<>(); - } - - private State m_tunnelState = new State(); - - private NotificationManager mNotificationManager = null; - private NotificationCompat.Builder mNotificationBuilder = null; - private Service m_parentService = null; - private boolean m_serviceDestroyed = false; - private boolean m_firstStart = true; - private CountDownLatch m_tunnelThreadStopSignal; - private Thread m_tunnelThread; - private AtomicBoolean m_isReconnect; - private AtomicBoolean m_isStopping; - private PsiphonTunnel m_tunnel = null; - private String m_lastUpstreamProxyErrorMessage; - private Handler m_Handler = new Handler(); - private GoogleSafetyNetApiWrapper m_safetyNetwrapper; - - public TunnelManager(Service parentService) { - m_parentService = parentService; - m_isReconnect = new AtomicBoolean(false); - m_isStopping = new AtomicBoolean(false); - m_tunnel = PsiphonTunnel.newPsiphonTunnel(this); - } - - // Implementation of android.app.Service.onStartCommand - public int onStartCommand(Intent intent, int flags, int startId) { - if (mNotificationManager == null) { - mNotificationManager = (NotificationManager) m_parentService.getSystemService(Context.NOTIFICATION_SERVICE); - } - - if (mNotificationBuilder == null) { - mNotificationBuilder = new NotificationCompat.Builder(m_parentService); - } - - if (m_firstStart && intent != null) { - getTunnelConfig(intent); - m_parentService.startForeground(R.string.psiphon_service_notification_id, this.createNotification(false)); - MyLog.v(R.string.client_version, MyLog.Sensitivity.NOT_SENSITIVE, EmbeddedValues.CLIENT_VERSION); - m_firstStart = false; - m_tunnelThreadStopSignal = new CountDownLatch(1); - m_tunnelThread = new Thread(new Runnable() { +public class TunnelManager implements Tunnel.HostService { + + private static final String LOG_TAG = "TunnelManager"; + public static final String SOCKS_SERVER_ADDRESS_EXTRA = "socksServerAddress"; + + private Service m_parentService = null; + private boolean m_firstStart = true; + private boolean m_signalledStop = false; + private CountDownLatch m_tunnelThreadStopSignal; + private Thread m_tunnelThread; + private AtomicBoolean m_isStopping; + private Tunnel m_tunnel = null; + private String m_socksServerAddress; + + public TunnelManager(Service parentService) { + m_parentService = parentService; + m_isStopping = new AtomicBoolean(false); + m_tunnel = Tunnel.newTunnel(this); + } + + // Implementation of android.app.Service.onStartCommand + public int onStartCommand(Intent intent, int flags, int startId) { + Log.i(LOG_TAG, "onStartCommand"); + LocalBroadcastManager.getInstance(m_parentService) + .registerReceiver( + m_broadcastReceiver, new IntentFilter(DnsResolverService.DNS_ADDRESS_BROADCAST)); + + m_socksServerAddress = intent.getStringExtra(SOCKS_SERVER_ADDRESS_EXTRA); + if (m_socksServerAddress == null) { + Log.e(LOG_TAG, "Failed to receive the socks server address."); + return 0; + } + + try { + if (!m_tunnel.startRouting()) { + Log.e(LOG_TAG, "Failed to establish VPN"); + } + } catch (Tunnel.Exception e) { + Log.e(LOG_TAG, String.format("Failed to establish VPN: %s", e.getMessage())); + } + return android.app.Service.START_NOT_STICKY; + } + + // Implementation of android.app.Service.onDestroy + public void onDestroy() { + if (m_tunnelThread == null) { + return; + } + + LocalBroadcastManager.getInstance(m_parentService).unregisterReceiver(m_broadcastReceiver); + + // Stop DNS resolver service + m_parentService.stopService(new Intent(m_parentService, DnsResolverService.class)); + + // signalStopService should have been called, but in case is was not, call here. + // If signalStopService was not already called, the join may block the calling + // thread for some time. + signalStopService(); + + try { + m_tunnelThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + m_tunnelThreadStopSignal = null; + m_tunnelThread = null; + } + + // Signals the runTunnel thread to stop. The thread will self-stop the service. + // This is the preferred method for stopping the tunnel service: + // 1. VpnService doesn't respond to stopService calls + // 2. The UI will not block while waiting for stopService to return + public void signalStopService() { + m_signalledStop = true; + if (m_tunnelThreadStopSignal != null) { + m_tunnelThreadStopSignal.countDown(); + } + } + + public boolean signalledStop() { + return m_signalledStop; + } + + private void startTunnel(final String dnsResolverAddress) { + if (m_firstStart) { + m_firstStart = false; + m_tunnelThreadStopSignal = new CountDownLatch(1); + m_tunnelThread = + new Thread( + new Runnable() { @Override public void run() { - runTunnel(); - } - }); - m_tunnelThread.start(); - } - - return Service.START_REDELIVER_INTENT; - } - - public void onCreate() { - // This service runs as a separate process, so it needs to initialize embedded values - EmbeddedValues.initialize(this.getContext()); - - MyLog.setLogger(this); - } - - // Implementation of android.app.Service.onDestroy - public void onDestroy() { - m_serviceDestroyed = true; - - stopAndWaitForTunnel(); - - MyLog.unsetLogger(); - } - - public void onRevoke() { - MyLog.w(R.string.vpn_service_revoked, MyLog.Sensitivity.NOT_SENSITIVE); - - stopAndWaitForTunnel(); - } - - private void stopAndWaitForTunnel() { - if (m_tunnelThread == null) { - return; - } - - // signalStopService could have been called, but in case is was not, call here. - // If signalStopService was not already called, the join may block the calling - // thread for some time. - signalStopService(); - - try { - m_tunnelThread.join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - m_tunnelThreadStopSignal = null; - m_tunnelThread = null; - } - - // signalStopService signals the runTunnel thread to stop. The thread will - // self-stop the service. This is the preferred method for stopping the - // Psiphon tunnel service: - // 1. VpnService doesn't respond to stopService calls - // 2. The UI will not block while waiting for stopService to return - public void signalStopService() { - if (m_tunnelThreadStopSignal != null) { - m_tunnelThreadStopSignal.countDown(); - } - - if (m_safetyNetwrapper != null) { - m_safetyNetwrapper.disconnect(); - } - } - - private void getTunnelConfig(Intent intent) { - m_tunnelConfig.handshakePendingIntent = intent.getParcelableExtra( - TunnelManager.DATA_TUNNEL_CONFIG_HANDSHAKE_PENDING_INTENT); - - m_tunnelConfig.notificationPendingIntent = intent.getParcelableExtra( - TunnelManager.DATA_TUNNEL_CONFIG_NOTIFICATION_PENDING_INTENT); - - m_tunnelConfig.wholeDevice = intent.getBooleanExtra( - TunnelManager.DATA_TUNNEL_CONFIG_WHOLE_DEVICE, false); - - m_tunnelConfig.egressRegion = intent.getStringExtra( - TunnelManager.DATA_TUNNEL_CONFIG_EGRESS_REGION); - - m_tunnelConfig.disableTimeouts = intent.getBooleanExtra( - TunnelManager.DATA_TUNNEL_CONFIG_DISABLE_TIMEOUTS, false); - } - - private Notification createNotification(boolean alert) { - int contentTextID; - int iconID; - CharSequence ticker = null; - - if (m_tunnelState.isConnected) { - if (m_tunnelConfig.wholeDevice) { - contentTextID = R.string.psiphon_running_whole_device; - } else { - contentTextID = R.string.psiphon_running_browser_only; - } - iconID = R.drawable.notification_icon_connected; - } else { - contentTextID = R.string.psiphon_service_notification_message_connecting; - ticker = m_parentService.getText(R.string.psiphon_service_notification_message_connecting); - iconID = R.drawable.notification_icon_connecting_animation; - } - - mNotificationBuilder - .setSmallIcon(iconID) - .setContentTitle(m_parentService.getText(R.string.app_name)) - .setContentText(m_parentService.getText(contentTextID)) - .setTicker(ticker) - .setContentIntent(m_tunnelConfig.notificationPendingIntent); - - Notification notification = mNotificationBuilder.build(); - - if (alert) { - final AppPreferences multiProcessPreferences = new AppPreferences(getContext()); - - if (multiProcessPreferences.getBoolean( - m_parentService.getString(R.string.preferenceNotificationsWithSound), false)) { - notification.defaults |= Notification.DEFAULT_SOUND; - } - if (multiProcessPreferences.getBoolean( - m_parentService.getString(R.string.preferenceNotificationsWithVibrate), false)) { - notification.defaults |= Notification.DEFAULT_VIBRATE; - } - } - - return notification; - } - - private void setIsConnected(boolean isConnected) { - boolean alert = (isConnected != m_tunnelState.isConnected); - - m_tunnelState.isConnected = isConnected; - - // Don't update notification to CONNECTING, etc., when a stop was commanded. - if (!m_serviceDestroyed && !m_isStopping.get()) { - if (mNotificationManager != null) { - mNotificationManager.notify( - R.string.psiphon_service_notification_id, - createNotification(alert)); - } - } - } - - public IBinder onBind(Intent intent) { - return m_incomingMessenger.getBinder(); - } - - private final Messenger m_incomingMessenger = new Messenger( - new IncomingMessageHandler(this)); - private Messenger m_outgoingMessenger = null; - - private static class IncomingMessageHandler extends Handler { - private final WeakReference mTunnelManager; - - IncomingMessageHandler(TunnelManager manager) { - mTunnelManager = new WeakReference<>(manager); - } - - @Override - public void handleMessage(Message msg) - { - TunnelManager manager = mTunnelManager.get(); - switch (msg.what) - { - case TunnelManager.MSG_REGISTER: - if (manager != null) { - manager.m_outgoingMessenger = msg.replyTo; - manager.sendClientMessage(MSG_REGISTER_RESPONSE, manager.getTunnelStateBundle()); - } - break; - - case TunnelManager.MSG_UNREGISTER: - if (manager != null) { - manager.m_outgoingMessenger = null; - } - break; - - case TunnelManager.MSG_STOP_SERVICE: - if (manager != null) { - manager.signalStopService(); - } - break; - - default: - super.handleMessage(msg); - } - } - } - - private void sendClientMessage(int what, Bundle data) { - if (m_incomingMessenger == null || m_outgoingMessenger == null) { - return; - } - try { - Message msg = Message.obtain(null, what); - msg.replyTo = m_incomingMessenger; - if (data != null) { - msg.setData(data); - } - m_outgoingMessenger.send(msg); - } catch (RemoteException e) { - MyLog.g("sendClientMessage failed: %s", e.getMessage()); - } - } - - private void sendHandshakeIntent(boolean isReconnect) { - // Only send this intent if the StatusActivity is - // in the foreground, or if this is an initial connection - // so we can show the home tab. - // If it isn't and we sent the intent, the activity will - // interrupt the user in some other app. - // It's too late to do this check in StatusActivity - // onNewIntent. - - final AppPreferences multiProcessPreferences = new AppPreferences(getContext()); - if (multiProcessPreferences.getBoolean(m_parentService.getString(R.string.status_activity_foreground), false) || - !isReconnect) { - Intent fillInExtras = new Intent(); - fillInExtras.putExtra(DATA_HANDSHAKE_IS_RECONNECT, isReconnect); - fillInExtras.putExtras(getTunnelStateBundle()); - try { - m_tunnelConfig.handshakePendingIntent.send( - m_parentService, 0, fillInExtras); - } catch (PendingIntent.CanceledException e) { - MyLog.g("sendHandshakeIntent failed: %s", e.getMessage()); - } - } - } - - private Bundle getTunnelStateBundle() { - Bundle data = new Bundle(); - data.putStringArrayList(DATA_TUNNEL_STATE_AVAILABLE_EGRESS_REGIONS, m_tunnelState.availableEgressRegions); - data.putBoolean(DATA_TUNNEL_STATE_IS_CONNECTED, m_tunnelState.isConnected); - data.putInt(DATA_TUNNEL_STATE_LISTENING_LOCAL_SOCKS_PROXY_PORT, m_tunnelState.listeningLocalSocksProxyPort); - data.putInt(DATA_TUNNEL_STATE_LISTENING_LOCAL_HTTP_PROXY_PORT, m_tunnelState.listeningLocalHttpProxyPort); - data.putString(DATA_TUNNEL_STATE_CLIENT_REGION, m_tunnelState.clientRegion); - data.putStringArrayList(DATA_TUNNEL_STATE_HOME_PAGES, m_tunnelState.homePages); - return data; - } - - private Bundle getDataTransferStatsBundle() { - Bundle data = new Bundle(); - data.putLong(DATA_TRANSFER_STATS_CONNECTED_TIME, DataTransferStats.getDataTransferStatsForService().m_connectedTime); - data.putLong(DATA_TRANSFER_STATS_TOTAL_BYTES_SENT, DataTransferStats.getDataTransferStatsForService().m_totalBytesSent); - data.putLong(DATA_TRANSFER_STATS_TOTAL_BYTES_RECEIVED, DataTransferStats.getDataTransferStatsForService().m_totalBytesReceived); - data.putParcelableArrayList(DATA_TRANSFER_STATS_SLOW_BUCKETS, DataTransferStats.getDataTransferStatsForService().m_slowBuckets); - data.putLong(DATA_TRANSFER_STATS_SLOW_BUCKETS_LAST_START_TIME, DataTransferStats.getDataTransferStatsForService().m_slowBucketsLastStartTime); - data.putParcelableArrayList(DATA_TRANSFER_STATS_FAST_BUCKETS, DataTransferStats.getDataTransferStatsForService().m_fastBuckets); - data.putLong(DATA_TRANSFER_STATS_FAST_BUCKETS_LAST_START_TIME, DataTransferStats.getDataTransferStatsForService().m_fastBucketsLastStartTime); - return data; - } - - private final static String LEGACY_SERVER_ENTRY_FILENAME = "psiphon_server_entries.json"; - private final static int MAX_LEGACY_SERVER_ENTRIES = 100; - - public static String getServerEntries(Context context) { - StringBuilder list = new StringBuilder(); - - for (String encodedServerEntry : EmbeddedValues.EMBEDDED_SERVER_LIST) { - list.append(encodedServerEntry); - list.append("\n"); - } - - // Import legacy server entries - try { - FileInputStream file = context.openFileInput(LEGACY_SERVER_ENTRY_FILENAME); - BufferedReader reader = new BufferedReader(new InputStreamReader(file)); - StringBuilder json = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - json.append(line); - } - file.close(); - JSONObject obj = new JSONObject(json.toString()); - JSONArray jsonServerEntries = obj.getJSONArray("serverEntries"); - - // MAX_LEGACY_SERVER_ENTRIES ensures the list we pass through to tunnel-core - // is unlikely to trigger an OutOfMemoryError - for (int i = 0; i < jsonServerEntries.length() && i < MAX_LEGACY_SERVER_ENTRIES; i++) { - list.append(jsonServerEntries.getString(i)); - list.append("\n"); - } - - // Don't need to repeat the import again - context.deleteFile(LEGACY_SERVER_ENTRY_FILENAME); - } catch (FileNotFoundException e) { - // pass - } catch (IOException | JSONException | OutOfMemoryError e) { - MyLog.g("prepareServerEntries failed: %s", e.getMessage()); - } - - return list.toString(); - } - - private Handler sendDataTransferStatsHandler = new Handler(); - private final long sendDataTransferStatsIntervalMs = 1000; - private Runnable sendDataTransferStats = new Runnable() { - @Override - public void run() { - sendClientMessage(MSG_DATA_TRANSFER_STATS, getDataTransferStatsBundle()); - sendDataTransferStatsHandler.postDelayed(this, sendDataTransferStatsIntervalMs); - } - }; - - private void runTunnel() { - - Utils.initializeSecureRandom(); - - m_isStopping.set(false); - m_isReconnect.set(false); - - // Notify if an upgrade has already been downloaded and is waiting for install - UpgradeManager.UpgradeInstaller.notifyUpgrade(m_parentService); - - sendClientMessage(MSG_TUNNEL_STARTING, null); - - MyLog.v(R.string.current_network_type, MyLog.Sensitivity.NOT_SENSITIVE, Utils.getNetworkTypeName(m_parentService)); - - MyLog.v(R.string.starting_tunnel, MyLog.Sensitivity.NOT_SENSITIVE); - - m_tunnelState.homePages.clear(); - - DataTransferStats.getDataTransferStatsForService().startSession(); - sendDataTransferStatsHandler.postDelayed(sendDataTransferStats, sendDataTransferStatsIntervalMs); - - boolean runVpn = - m_tunnelConfig.wholeDevice && - Utils.hasVpnService() && - // Guard against trying to start WDM mode when the global option flips while starting a TunnelService - (m_parentService instanceof TunnelVpnService); - - try { - if (runVpn) { - if (!m_tunnel.startRouting()) { - throw new PsiphonTunnel.Exception("application is not prepared or revoked"); - } - MyLog.v(R.string.vpn_service_running, MyLog.Sensitivity.NOT_SENSITIVE); - } - - m_tunnel.startTunneling(getServerEntries(m_parentService)); - - try { - m_tunnelThreadStopSignal.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - m_isStopping.set(true); - - } catch (PsiphonTunnel.Exception e) { - MyLog.e(R.string.start_tunnel_failed, MyLog.Sensitivity.NOT_SENSITIVE, e.getMessage()); - } finally { - - MyLog.v(R.string.stopping_tunnel, MyLog.Sensitivity.NOT_SENSITIVE); - - sendClientMessage(MSG_TUNNEL_STOPPING, null); - - m_tunnel.stop(); - - sendDataTransferStatsHandler.removeCallbacks(sendDataTransferStats); - DataTransferStats.getDataTransferStatsForService().stop(); - - MyLog.v(R.string.stopped_tunnel, MyLog.Sensitivity.NOT_SENSITIVE); - - // Stop service - m_parentService.stopForeground(true); - m_parentService.stopSelf(); - if(m_safetyNetwrapper != null) { - m_safetyNetwrapper.saveCache(m_parentService); - } - } - } - - @Override - public String getAppName() { - return m_parentService.getString(R.string.app_name); - } - - @Override - public Context getContext() { - return m_parentService; - } - - @Override - public VpnService getVpnService() { - return ((TunnelVpnService) m_parentService); - } - - @Override - public Builder newVpnServiceBuilder() { - return ((TunnelVpnService) m_parentService).newBuilder(); - } - - /** - * Create a tunnel-core config suitable for different tunnel types (i.e., the main Psiphon app - * tunnel and the UpgradeChecker temp tunnel). - * - * @param context - * @param tempTunnelName null if not a temporary tunnel. If set, must be a valid to use in file path. - * @param clientPlatformPrefix null if not applicable (i.e., for main Psiphon app); should be provided - * for temp tunnels. Will be prepended to standard client platform value. - * @return JSON string of config. null on error. - */ - public static String buildTunnelCoreConfig( - Context context, - Config tunnelConfig, - String tempTunnelName, - String clientPlatformPrefix) { - boolean temporaryTunnel = tempTunnelName != null && !tempTunnelName.isEmpty(); - - JSONObject json = new JSONObject(); - - try { - String clientPlatform = PsiphonConstants.PLATFORM; - - if (clientPlatformPrefix != null && !clientPlatformPrefix.isEmpty()) { - clientPlatform = clientPlatformPrefix + clientPlatform; - } - - // Detect if device is rooted and append to the client_platform string - if (Utils.isRooted()) { - clientPlatform += PsiphonConstants.ROOTED; - } - - // Detect if this is a Play Store build - if (EmbeddedValues.IS_PLAY_STORE_BUILD) { - clientPlatform += PsiphonConstants.PLAY_STORE_BUILD; - } - - json.put("ClientPlatform", clientPlatform); - - json.put("ClientVersion", EmbeddedValues.CLIENT_VERSION); - - if (UpgradeChecker.upgradeCheckNeeded(context)) { - Uri upgradeDownloadUrl = Uri.parse(EmbeddedValues.UPGRADE_URL); - upgradeDownloadUrl = upgradeDownloadUrl.buildUpon() - .appendQueryParameter("tunnel_name", tempTunnelName == null ? "main" : tempTunnelName) - .appendQueryParameter("client_version", EmbeddedValues.CLIENT_VERSION) - .appendQueryParameter("client_platform", clientPlatform) - .build(); - json.put("UpgradeDownloadUrl", upgradeDownloadUrl.toString()); - - json.put("UpgradeDownloadClientVersionHeader", "x-amz-meta-psiphon-client-version"); - - json.put("UpgradeDownloadFilename", - new UpgradeManager.DownloadedUpgradeFile(context).getFullPath()); - } - - json.put("PropagationChannelId", EmbeddedValues.PROPAGATION_CHANNEL_ID); - - json.put("SponsorId", EmbeddedValues.SPONSOR_ID); - - json.put("RemoteServerListUrl", EmbeddedValues.REMOTE_SERVER_LIST_URL); - - json.put("RemoteServerListSignaturePublicKey", EmbeddedValues.REMOTE_SERVER_LIST_SIGNATURE_PUBLIC_KEY); - - json.put("UpstreamProxyUrl", UpstreamProxySettings.getUpstreamProxyUrl(context)); - - json.put("EmitDiagnosticNotices", true); - - // If this is a temporary tunnel (like for UpgradeChecker) we need to override some of - // the implicit config values. - if (temporaryTunnel) { - File tempTunnelDir = new File(context.getFilesDir(), tempTunnelName); - if (!tempTunnelDir.exists() - && !tempTunnelDir.mkdirs()) { - // Failed to create DB directory - return null; - } - - // On Android, these directories must be set to the app private storage area. - // The Psiphon library won't be able to use its current working directory - // and the standard temporary directories do not exist. - json.put("DataStoreDirectory", tempTunnelDir.getAbsolutePath()); - - File remoteServerListDownload = new File(tempTunnelDir, "remote_server_list"); - json.put("RemoteServerListDownloadFilename", remoteServerListDownload.getAbsolutePath()); - - // This number is an arbitrary guess at what might be the "best" balance between - // wake-lock-battery-burning and successful upgrade downloading. - // Note that the fall-back untunneled upgrade download doesn't start for 30 secs, - // so we should be waiting longer than that. - json.put("EstablishTunnelTimeoutSeconds", 300); - - json.put("TunnelWholeDevice", 0); - - json.put("LocalHttpProxyPort", 0); - json.put("LocalSocksProxyPort", 0); - - json.put("EgressRegion", ""); - } else { - // TODO: configure local proxy ports - json.put("LocalHttpProxyPort", 0); - json.put("LocalSocksProxyPort", 0); - - String egressRegion = tunnelConfig.egressRegion; - MyLog.g("EgressRegion", "regionCode", egressRegion); - json.put("EgressRegion", egressRegion); - } - - if (tunnelConfig.disableTimeouts) { - //disable timeouts - MyLog.g("DisableTimeouts", "disableTimeouts", true); - json.put("TunnelConnectTimeoutSeconds", 0); - json.put("TunnelPortForwardTimeoutSeconds", 0); - json.put("TunnelSshKeepAliveProbeTimeoutSeconds", 0); - json.put("TunnelSshKeepAlivePeriodicTimeoutSeconds", 0); - json.put("FetchRemoteServerListTimeoutSeconds", 0); - json.put("PsiphonApiServerTimeoutSeconds", 0); - json.put("FetchRoutesTimeoutSeconds", 0); - json.put("HttpProxyOriginServerTimeoutSeconds", 0); - } - - return json.toString(); - } catch (JSONException e) { - return null; - } - } - - @Override - public String getPsiphonConfig() { - String config = buildTunnelCoreConfig(m_parentService, m_tunnelConfig, null, null); - return config == null ? "" : config; - } - - @Override - public void onDiagnosticMessage(final String message) { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.g(message, "msg", message); - } - }); - } - - @Override - public void onAvailableEgressRegions(final List regions) { - m_Handler.post(new Runnable() { - @Override - public void run() { - m_tunnelState.availableEgressRegions.clear(); - m_tunnelState.availableEgressRegions.addAll(regions); - Bundle data = new Bundle(); - data.putStringArrayList(DATA_TUNNEL_STATE_AVAILABLE_EGRESS_REGIONS, - m_tunnelState.availableEgressRegions); - sendClientMessage(MSG_KNOWN_SERVER_REGIONS, data); - } - }); - } - - @Override - public void onSocksProxyPortInUse(final int port) { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.e(R.string.socks_port_in_use, MyLog.Sensitivity.NOT_SENSITIVE, port); - signalStopService(); - } - }); - } - - @Override - public void onHttpProxyPortInUse(final int port) { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.e(R.string.http_proxy_port_in_use, MyLog.Sensitivity.NOT_SENSITIVE, port); - signalStopService(); - } - }); - } - - @Override - public void onListeningSocksProxyPort(final int port) { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.v(R.string.socks_running, MyLog.Sensitivity.NOT_SENSITIVE, port); - m_tunnelState.listeningLocalSocksProxyPort = port; - } - }); - } - - @Override - public void onListeningHttpProxyPort(final int port) { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.v(R.string.http_proxy_running, MyLog.Sensitivity.NOT_SENSITIVE, port); - m_tunnelState.listeningLocalHttpProxyPort = port; - - final AppPreferences multiProcessPreferences = new AppPreferences(getContext()); - multiProcessPreferences.put( - m_parentService.getString(R.string.current_local_http_proxy_port), - port); - } - }); - } - - @Override - public void onUpstreamProxyError(final String message) { - m_Handler.post(new Runnable() { - @Override - public void run() { - // Display the error message only once, and continue trying to connect in - // case the issue is temporary. - if (m_lastUpstreamProxyErrorMessage == null || !m_lastUpstreamProxyErrorMessage.equals(message)) { - MyLog.v(R.string.upstream_proxy_error, MyLog.Sensitivity.SENSITIVE_FORMAT_ARGS, message); - m_lastUpstreamProxyErrorMessage = message; - } - } - }); - } - - @Override - public void onConnecting() { - m_Handler.post(new Runnable() { - @Override - public void run() { - DataTransferStats.getDataTransferStatsForService().stop(); - - if (!m_isStopping.get()) { - MyLog.v(R.string.tunnel_connecting, MyLog.Sensitivity.NOT_SENSITIVE); - } - - setIsConnected(false); - m_tunnelState.homePages.clear(); - Bundle data = new Bundle(); - data.putBoolean(DATA_TUNNEL_STATE_IS_CONNECTED, false); - sendClientMessage(MSG_TUNNEL_CONNECTION_STATE, data); - } - }); - } - - @Override - public void onConnected() { - m_Handler.post(new Runnable() { - @Override - public void run() { - DataTransferStats.getDataTransferStatsForService().startConnected(); - - MyLog.v(R.string.tunnel_connected, MyLog.Sensitivity.NOT_SENSITIVE); - - sendHandshakeIntent(m_isReconnect.get()); - // Any subsequent onConnecting after this first onConnect will be a reconnect. - m_isReconnect.set(true); - - setIsConnected(true); - Bundle data = new Bundle(); - data.putBoolean(DATA_TUNNEL_STATE_IS_CONNECTED, true); - sendClientMessage(MSG_TUNNEL_CONNECTION_STATE, data); - - // Reset verification attempts count and - // request client verification status from the server by - // sending an empty message to client verification handler - m_clientVerificationAttempts = 0; - TunnelManager.this.setClientVerificationResult(""); - } - }); - } - - @Override - public void onHomepage(final String url) { - m_Handler.post(new Runnable() { - @Override - public void run() { - for (String homePage : m_tunnelState.homePages) { - if (homePage.equals(url)) { - return; - } + runTunnel(m_socksServerAddress, dnsResolverAddress); } - m_tunnelState.homePages.add(url); - } - }); - } - - @Override - public void onClientRegion(final String region) { - m_Handler.post(new Runnable() { - @Override - public void run() { - Bundle data = new Bundle(); - data.putString(DATA_TUNNEL_STATE_CLIENT_REGION, region); - sendClientMessage(MSG_CLIENT_REGION, data); - } - }); - } - - @Override - public void onClientUpgradeDownloaded(String filename) { - m_Handler.post(new Runnable() { - @Override - public void run() { - UpgradeManager.UpgradeInstaller.notifyUpgrade(m_parentService); - } - }); - } - - @Override - public void onClientIsLatestVersion() { - } - - @Override - public void onSplitTunnelRegion(final String region) { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.v(R.string.split_tunnel_region, MyLog.Sensitivity.SENSITIVE_FORMAT_ARGS, region); - } - }); - } - - @Override - public void onUntunneledAddress(final String address) { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.v(R.string.untunneled_address, MyLog.Sensitivity.SENSITIVE_FORMAT_ARGS, address); - } - }); - } - - @Override - public void onBytesTransferred(final long sent, final long received) { - m_Handler.post(new Runnable() { - @Override - public void run() { - DataTransferStats.DataTransferStatsForService stats = DataTransferStats.getDataTransferStatsForService(); - stats.addBytesSent(sent); - stats.addBytesReceived(received); - } - }); - } - - @Override - public void onStartedWaitingForNetworkConnectivity() { - m_Handler.post(new Runnable() { - @Override - public void run() { - MyLog.v(R.string.waiting_for_network_connectivity, MyLog.Sensitivity.NOT_SENSITIVE); - } - }); - } - - @Override - public void onClientVerificationRequired(final String serverNonce, final int ttlSeconds, final boolean resetCache) { - - // Server may reply with a new verification request after verification payload is sent - // In this case we want to limit a number of possible retries per each session - m_clientVerificationAttempts ++; - - if (m_clientVerificationAttempts > MAX_CLIENT_VERIFICATION_ATTEMPTS) { - return; - } - if (ttlSeconds == 0) { - // do not send payload if requested TTL is 0 - return; - } - m_Handler.post(new Runnable() { - @Override - public void run() { - // Perform safetyNet check - m_safetyNetwrapper = GoogleSafetyNetApiWrapper.getInstance(getContext()); - m_safetyNetwrapper.verify(TunnelManager.this, serverNonce, ttlSeconds, resetCache); - } - }); - } - - @Override - public void onExiting() {} - - public void setClientVerificationResult(String payload) { - if (m_tunnel != null) { - m_tunnel.setClientVerificationPayload(payload); - } - } + }); + m_tunnelThread.start(); + } + } + + private void runTunnel(String socksServerAddress, String dnsResolverAddress) { + m_isStopping.set(false); + + try { + if (!m_tunnel.startTunneling(socksServerAddress, dnsResolverAddress)) { + throw new Tunnel.Exception("application is not prepared or revoked"); + } + Log.i(LOG_TAG, "VPN service running"); + + try { + m_tunnelThreadStopSignal.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + m_isStopping.set(true); + + } catch (Tunnel.Exception e) { + Log.e(LOG_TAG, String.format("Start tunnel failed: %s", e.getMessage())); + } finally { + Log.i(LOG_TAG, "Stopping tunnel..."); + m_tunnel.stop(); + + // Stop service + m_parentService.stopForeground(true); + m_parentService.stopSelf(); + } + } + + //---------------------------------------------------------------------------- + // Tunnel.HostService + //---------------------------------------------------------------------------- + + @Override + public String getAppName() { + return "Tun2Socks"; + } + + @Override + public Context getContext() { + return m_parentService; + } + + @Override + public VpnService getVpnService() { + return ((TunnelVpnService) m_parentService); + } + + @Override + public VpnService.Builder newVpnServiceBuilder() { + return ((TunnelVpnService) m_parentService).newBuilder(); + } + + @Override + public void onDiagnosticMessage(String message) { + Log.d(LOG_TAG, message); + } + + @Override + public void onTunnelConnected() { + Log.i(LOG_TAG, "Tunnel connected."); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onVpnEstablished() { + Log.i(LOG_TAG, "VPN established."); + Intent dnsResolverStart = new Intent(m_parentService, DnsResolverService.class); + dnsResolverStart.putExtra(SOCKS_SERVER_ADDRESS_EXTRA, m_socksServerAddress); + m_parentService.startService(dnsResolverStart); + } + + private DnsResolverAddressBroadcastReceiver m_broadcastReceiver = + new DnsResolverAddressBroadcastReceiver(TunnelManager.this); + + private class DnsResolverAddressBroadcastReceiver extends BroadcastReceiver { + private TunnelManager m_tunnelManager; + + public DnsResolverAddressBroadcastReceiver(TunnelManager tunnelManager) { + m_tunnelManager = tunnelManager; + } + + @Override + public void onReceive(Context context, Intent intent) { + String dnsResolverAddress = intent.getStringExtra(DnsResolverService.DNS_ADDRESS_EXTRA); + if (dnsResolverAddress == null || dnsResolverAddress.isEmpty()) { + Log.e(LOG_TAG, "Failed to receive DNS resolver address"); + return; + } + Log.d(LOG_TAG, "DNS resolver address: " + dnsResolverAddress); + // Callback into tunnel manager to start tunneling. + m_tunnelManager.startTunnel(dnsResolverAddress); + } + }; } diff --git a/src/android/org/uproxy/tun2socks/TunnelState.java b/src/android/org/uproxy/tun2socks/TunnelState.java new file mode 100644 index 0000000..dcd92d3 --- /dev/null +++ b/src/android/org/uproxy/tun2socks/TunnelState.java @@ -0,0 +1,40 @@ +package org.uproxy.tun2socks; + +// Singleton class to maintain state related to VPN Tunnel service. +public class TunnelState { + + private static TunnelState m_tunnelState; + + public Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + public static synchronized TunnelState getTunnelState() { + if (m_tunnelState == null) { + m_tunnelState = new TunnelState(); + } + return m_tunnelState; + } + + private TunnelManager m_tunnelManager = null; + private boolean m_startingTunnelManager = false; + + private TunnelState() {} + + public synchronized void setTunnelManager(TunnelManager tunnelManager) { + m_tunnelManager = tunnelManager; + m_startingTunnelManager = false; + } + + public synchronized TunnelManager getTunnelManager() { + return m_tunnelManager; + } + + public synchronized void setStartingTunnelManager() { + m_startingTunnelManager = true; + } + + public synchronized boolean getStartingTunnelManager() { + return m_startingTunnelManager; + } +}; diff --git a/src/android/org/uproxy/tun2socks/TunnelVpnService.java b/src/android/org/uproxy/tun2socks/TunnelVpnService.java index 403e1ce..dbf690f 100644 --- a/src/android/org/uproxy/tun2socks/TunnelVpnService.java +++ b/src/android/org/uproxy/tun2socks/TunnelVpnService.java @@ -6,71 +6,87 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ -package com.psiphon3.psiphonlibrary; +package org.uproxy.tun2socks; import android.annotation.TargetApi; import android.content.Intent; import android.net.VpnService; +import android.os.Binder; import android.os.Build; import android.os.IBinder; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) -public class TunnelVpnService extends VpnService -{ - private TunnelManager m_Manager = new TunnelManager(this); +public class TunnelVpnService extends VpnService { - @Override - public IBinder onBind(Intent intent) - { - // Need to use super class behavior in specified cases: - // http://developer.android.com/reference/android/net/VpnService.html#onBind%28android.content.Intent%29 - - String action = intent.getAction(); - if (action != null && action.equals(SERVICE_INTERFACE)) - { - return super.onBind(intent); - } - - return m_Manager.onBind(intent); - } + private static final String LOG_TAG = "TunnelVpnService"; + public static final String TUNNEL_VPN_DISCONNECT_BROADCAST = + "tunnelVpnDisconnectBroadcast"; - @Override - public int onStartCommand(Intent intent, int flags, int startId) - { - return m_Manager.onStartCommand(intent, flags, startId); - } + private TunnelManager m_tunnelManager = new TunnelManager(this); - @Override - public void onCreate() - { - m_Manager.onCreate(); + public class LocalBinder extends Binder { + public TunnelVpnService getService() { + return TunnelVpnService.this; } + } - @Override - public void onDestroy() - { - m_Manager.onDestroy(); - } + private final IBinder m_binder = new LocalBinder(); - @Override - public void onRevoke() - { - m_Manager.onRevoke(); - } - - public VpnService.Builder newBuilder() - { - return new VpnService.Builder(); + @Override + public IBinder onBind(Intent intent) { + String action = intent.getAction(); + if (action != null && action.equals(SERVICE_INTERFACE)) { + return super.onBind(intent); } + return m_binder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(LOG_TAG, "on start"); + return m_tunnelManager.onStartCommand(intent, flags, startId); + } + + @Override + public void onCreate() { + Log.d(LOG_TAG, "on create"); + TunnelState.getTunnelState().setTunnelManager(m_tunnelManager); + } + + @Override + public void onDestroy() { + TunnelState.getTunnelState().setTunnelManager(null); + m_tunnelManager.onDestroy(); + } + + @Override + public void onRevoke() { + Log.e(LOG_TAG, "VPN service revoked."); + broadcastVpnDisconnect(); + // stopSelf will trigger onDestroy in the main thread. + stopSelf(); + } + + public VpnService.Builder newBuilder() { + return new VpnService.Builder(); + } + + private void broadcastVpnDisconnect() { + Intent disconnectBroadcast = new Intent(TUNNEL_VPN_DISCONNECT_BROADCAST); + LocalBroadcastManager.getInstance(TunnelVpnService.this) + .sendBroadcast(disconnectBroadcast); + } } diff --git a/src/android/org/uproxy/tun2socks/build.gradle b/src/android/org/uproxy/tun2socks/build.gradle new file mode 100644 index 0000000..5c45af4 --- /dev/null +++ b/src/android/org/uproxy/tun2socks/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.application' + +android { + repositories { + jcenter() + } + + sourceSets { + main { + jniLibs.srcDirs = ["libs"] + jni.srcDirs = [] + } + } + + packagingOptions { + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + } + + task buildNative(type: Exec, description: 'Compile JNI source via NDK') { + commandLine 'ndk-build', '-C', file('jni').absolutePath + } + + task cleanNative(type: Exec, description: 'Clean JNI object files') { + commandLine 'ndk-build', '-C', file('jni').absolutePath, 'clean' + } + + clean.dependsOn 'cleanNative' + + tasks.withType(JavaCompile) { + compileTask -> compileTask.dependsOn buildNative + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar', '*.so']) + compile 'com.android.support:appcompat-v7:23.4.0' + compile 'org.uproxy.jsocks:jsocks:1.0.1' +} + diff --git a/src/badvpn/Android.mk b/src/badvpn/Android.mk index 47c8b59..1207915 100644 --- a/src/badvpn/Android.mk +++ b/src/badvpn/Android.mk @@ -69,7 +69,8 @@ LOCAL_SRC_FILES := \ base/BPending.c \ flowextra/PacketPassInactivityMonitor.c \ tun2socks/SocksUdpGwClient.c \ - udpgw_client/UdpGwClient.c + udpgw_client/UdpGwClient.c \ + stringmap/BStringMap.c include $(BUILD_SHARED_LIBRARY) diff --git a/src/badvpn/misc/dns_proto.h b/src/badvpn/misc/dns_proto.h new file mode 100644 index 0000000..2aadb9b --- /dev/null +++ b/src/badvpn/misc/dns_proto.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) uProxy + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the author nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Definitions for the DNS protocol. + */ + +#ifndef BADVPN_MISC_DNS_PROTO_H +#define BADVPN_MISC_DNS_PROTO_H + +#include +#include + +B_START_PACKED +struct dns_header { + uint16_t id; + uint8_t qr_opcode_aa_tc_rd; + uint8_t ra_z_rcode; + uint16_t qdcount; + uint16_t ancount; + uint16_t nscount; + uint16_t arcount; +} B_PACKED; +B_END_PACKED + +// DNS header field masks +#define DNS_QR 0x80 +#define DNS_TC 0x02 +#define DNS_Z 0x70 + +#define DNS_ID_STRLEN 6 + +static void dns_get_header_id_str(char* id_str, uint8_t* data) { + struct dns_header* dnsh = (struct dns_header*)data; + sprintf(id_str, "%u", dnsh->id); + id_str[DNS_ID_STRLEN - 1] = '\0'; +} + +static int dns_check(const uint8_t *data, int data_len, + struct dns_header *out_header) { + ASSERT(data_len >= 0) + ASSERT(out_header) + + // parse DNS header + if (data_len < sizeof(struct dns_header)) { + return 0; + } + memcpy(out_header, data, sizeof(*out_header)); + + // verify DNS header is request + return (out_header->qr_opcode_aa_tc_rd & DNS_QR) == 0 /* query */ + && (out_header->ra_z_rcode & DNS_Z) == 0 /* Z is Zero */ + && out_header->qdcount > 0 /* some questions */ + && !out_header->nscount && !out_header->ancount /* no answers */; +} + + #endif // BADVPN_MISC_DNS_PROTO_H diff --git a/src/badvpn/tun2socks/tun2socks.c b/src/badvpn/tun2socks/tun2socks.c index d68cd9e..c34f5ad 100644 --- a/src/badvpn/tun2socks/tun2socks.c +++ b/src/badvpn/tun2socks/tun2socks.c @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +69,8 @@ #include #include #include +#include +#include #ifndef BADVPN_USE_WINAPI #include @@ -117,13 +120,16 @@ struct { char *udpgw_remote_server_addr; int udpgw_max_connections; int udpgw_connection_buffer_size; - int udpgw_transparent_dns; + int transparent_dns; // ==== PSIPHON ==== int tun_fd; int tun_mtu; int set_signal; // ==== PSIPHON ==== + // ==== UPROXY ==== + char *dns_server_address; + // ==== UPROXY ==== } options; // TCP client @@ -219,6 +225,11 @@ static void run (void); static void init_arguments (const char* program_name); // ==== PSIPHON ==== +//==== UPROXY ==== +BAddr dns_server_address; // IP address of local DNS resolver +//==== UPROXY ==== + + static void terminate (void); static void print_help (const char *name); static void print_version (void); @@ -226,7 +237,7 @@ static int parse_arguments (int argc, char *argv[]); static int process_arguments (void); static void signal_handler (void *unused); static BAddr baddr_from_lwip (int is_ipv6, const ipX_addr_t *ipx_addr, uint16_t port_hostorder); -static void lwip_init_job_hadler (void *unused); +static void lwip_init_job_handler (void *unused); static void tcp_timer_handler (void *unused); static void device_error_handler (void *unused); static void device_read_handler_send (void *unused, uint8_t *data, int data_len); @@ -254,8 +265,143 @@ static void client_socks_recv_initiate (struct tcp_client *client); static void client_socks_recv_handler_done (struct tcp_client *client, int data_len); static int client_socks_recv_send_out (struct tcp_client *client); static err_t client_sent_func (void *arg, struct tcp_pcb *tpcb, u16_t len); -static void udpgw_client_handler_received (void *unused, BAddr local_addr, BAddr remote_addr, const uint8_t *data, int data_len); +static void udp_send_packet_to_device (void *unused, BAddr local_addr, BAddr remote_addr, const uint8_t *data, int data_len); + +//==== UPROXY ==== + +typedef struct { + int sockfd; + uint8_t *udp_recv_buffer; + BAddr netif_addr; + BFileDescriptor bfd; + BStringMap map; +} UdpPcb; + +UdpPcb udp_pcb; + +static int udp_init(UdpPcb* udp_pcb); +static int udp_send(int sockfd, uint8_t* data, int data_len); +static int udp_recv(int sockfd, uint8_t* buffer, int buffer_len); +static void udp_fd_handler(UdpPcb* udp_pcb, int event); +static void udp_free(UdpPcb* udp_pcb); + +// File descriptor handler for UDP socket. +static void udp_fd_handler(UdpPcb* udp_pcb, int event) { + int recv_bytes = udp_recv(udp_pcb->sockfd, udp_pcb->udp_recv_buffer, + UDP_MAX_DATAGRAM_BYTES); + if (recv_bytes <= 0) { + BLog(BLOG_ERROR, "udp_fd_handler: udp_recv failed, err %d", recv_bytes); + return; + } + + char dns_id_str[DNS_ID_STRLEN]; + dns_get_header_id_str(dns_id_str, udp_pcb->udp_recv_buffer); + const char* local_addr_str = BStringMap_Get(&udp_pcb->map, dns_id_str); + if (!local_addr_str) { + BLog(BLOG_ERROR, "udp_fd_handler: no address for dns reqeust id %s", + dns_id_str); + return; + } + + BAddr local_addr; + if (!BAddr_Parse(&local_addr, (char *)local_addr_str, NULL, 0)) { + BLog(BLOG_ERROR, "udp_fd_handler: failed to parse address %s", + local_addr_str); + BStringMap_Unset(&udp_pcb->map, dns_id_str); + return; + } + // Remove entry from map + BStringMap_Unset(&udp_pcb->map, dns_id_str); + + // Send data to device + udp_send_packet_to_device( + NULL, local_addr, udp_pcb->netif_addr, + udp_pcb->udp_recv_buffer, recv_bytes); +} + +// Initializes a UDP socket, binds it locally and connects it to a local +// address. Returns the socket file descriptor on success, and 0 otherwise. +static int udp_init(UdpPcb* udp_pcb) { + // Init receive buffer + udp_pcb->udp_recv_buffer = (uint8_t *)malloc(UDP_MAX_DATAGRAM_BYTES); + if (!udp_pcb->udp_recv_buffer) { + BLog(BLOG_ERROR, "udp_init: failed to init recv buffer"); + return 0; + } + + int sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sockfd < 0 ) { + BLog(BLOG_ERROR, "udp_init: failed to create socket"); + return 0; + } + // Bind to local host in order to receive data + struct sockaddr_in local_addr; + memset(&local_addr, 0, sizeof(local_addr)); + local_addr.sin_family = AF_INET; + local_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + local_addr.sin_port = htons(0); + if (bind(sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) { + BLog(BLOG_ERROR, "udp_init: failed to bind socket"); + close(sockfd); + return 0; + } + + // Connect to UDP server on specified address + struct sockaddr_in remote_addr; + memset(&remote_addr, 0, sizeof(remote_addr)); + remote_addr.sin_family = AF_INET; + // BAddr is already in network order, use htonl/htons for ip/port otherwise + remote_addr.sin_addr.s_addr = dns_server_address.ipv4.ip; + remote_addr.sin_port = dns_server_address.ipv4.port; + if (connect(sockfd, (struct sockaddr *)&remote_addr, sizeof(remote_addr)) < 0) { + BLog(BLOG_ERROR, "udp_init: failed to connect to remote"); + close(sockfd); + return 0; + } + // Monitor socket for read + BFileDescriptor_Init(&udp_pcb->bfd, sockfd, + (BFileDescriptor_handler)udp_fd_handler, udp_pcb); + if (!BReactor_AddFileDescriptor(&ss, &udp_pcb->bfd)) { + BLog(BLOG_ERROR, "udp_init: failed to add fd to event loop"); + close(sockfd); + return 0; + } + BReactor_SetFileDescriptorEvents(&ss, &udp_pcb->bfd, BREACTOR_READ); + + udp_pcb->sockfd = sockfd; + udp_pcb->netif_addr = + BAddr_MakeFromIpaddrAndPort(netif_ipaddr, hton16(UDP_DNS_PORT)); + BStringMap_Init(&udp_pcb->map); + + return sockfd; +} +// Sends |data| through |sockfd|, a connected udp socket. +static int udp_send(int sockfd, uint8_t* data, int data_len) { + int sent_bytes = send(sockfd, (void *)data, (size_t)data_len, 0); + if (sent_bytes < 0) { + BLog(BLOG_ERROR, "udp_send: failed to send data"); + return 0; + } + return sent_bytes; +} + +static int udp_recv(int sockfd, uint8_t* buffer, int buffer_len) { + int recv_bytes = recv(sockfd, buffer, buffer_len, 0); + BLog(BLOG_INFO, "udp_recv: received %d bytes", recv_bytes); + return recv_bytes; +} + +static void udp_free(UdpPcb* udp_pcb) { + if (udp_pcb->udp_recv_buffer) + free(udp_pcb->udp_recv_buffer); + if (udp_pcb->sockfd) + close(udp_pcb->sockfd); + + BStringMap_Free(&udp_pcb->map); +} + +//==== UPROXY ===== //==== PSIPHON ==== @@ -276,7 +422,7 @@ void PsiphonLog(const char *levelStr, const char *channelStr, const char *msgStr jstring channel = (*g_env)->NewStringUTF(g_env, channelStr); jstring msg = (*g_env)->NewStringUTF(g_env, msgStr); - jclass cls = (*g_env)->FindClass(g_env, "ca/psiphon/PsiphonTunnel"); + jclass cls = (*g_env)->FindClass(g_env, "org/uproxy/tun2socks/Tun2SocksJni"); jmethodID logMethod = (*g_env)->GetStaticMethodID(g_env, cls, "logTun2Socks", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); (*g_env)->CallStaticVoidMethod(g_env, cls, logMethod, level, channel, msg); @@ -287,7 +433,7 @@ void PsiphonLog(const char *levelStr, const char *channelStr, const char *msgStr (*g_env)->DeleteLocalRef(g_env, msg); } -JNIEXPORT jint JNICALL Java_ca_psiphon_PsiphonTunnel_runTun2Socks( +JNIEXPORT jint JNICALL Java_org_uproxy_tun2socks_Tun2SocksJni_runTun2Socks( JNIEnv* env, jclass cls, jint vpnInterfaceFileDescriptor, @@ -295,27 +441,27 @@ JNIEXPORT jint JNICALL Java_ca_psiphon_PsiphonTunnel_runTun2Socks( jstring vpnIpAddress, jstring vpnNetMask, jstring socksServerAddress, - jstring udpgwServerAddress, - jint udpgwTransparentDNS) + jstring dnsServerAddress, + jint transparentDNS) { g_env = env; const char* vpnIpAddressStr = (*env)->GetStringUTFChars(env, vpnIpAddress, 0); const char* vpnNetMaskStr = (*env)->GetStringUTFChars(env, vpnNetMask, 0); const char* socksServerAddressStr = (*env)->GetStringUTFChars(env, socksServerAddress, 0); - const char* udpgwServerAddressStr = (*env)->GetStringUTFChars(env, udpgwServerAddress, 0); + const char* dnsServerAddressStr = (*env)->GetStringUTFChars(env, dnsServerAddress, 0); - init_arguments("Psiphon tun2socks"); + init_arguments("uProxy tun2socks"); options.netif_ipaddr = (char*)vpnIpAddressStr; options.netif_netmask = (char*)vpnNetMaskStr; options.socks_server_addr = (char*)socksServerAddressStr; - options.udpgw_remote_server_addr = (char*)udpgwServerAddressStr; - options.udpgw_transparent_dns = udpgwTransparentDNS; + options.dns_server_address = (char*)dnsServerAddressStr; + options.transparent_dns = transparentDNS; options.tun_fd = vpnInterfaceFileDescriptor; options.tun_mtu = vpnInterfaceMTU; options.set_signal = 0; - options.loglevel = 2; + options.loglevel = 4; BLog_InitPsiphon(); @@ -326,7 +472,7 @@ JNIEXPORT jint JNICALL Java_ca_psiphon_PsiphonTunnel_runTun2Socks( (*env)->ReleaseStringUTFChars(env, vpnIpAddress, vpnIpAddressStr); (*env)->ReleaseStringUTFChars(env, vpnNetMask, vpnNetMaskStr); (*env)->ReleaseStringUTFChars(env, socksServerAddress, socksServerAddressStr); - (*env)->ReleaseStringUTFChars(env, udpgwServerAddress, udpgwServerAddressStr); + (*env)->ReleaseStringUTFChars(env, dnsServerAddress, dnsServerAddressStr); g_env = 0; @@ -335,7 +481,7 @@ JNIEXPORT jint JNICALL Java_ca_psiphon_PsiphonTunnel_runTun2Socks( return 1; } -JNIEXPORT jint JNICALL Java_ca_psiphon_PsiphonTunnel_terminateTun2Socks( +JNIEXPORT jint JNICALL Java_org_uproxy_tun2socks_Tun2SocksJni_terminateTun2Socks( jclass cls, JNIEnv* env) { @@ -490,18 +636,19 @@ void run() goto fail4; } - if (options.udpgw_remote_server_addr) { - // compute maximum UDP payload size we need to pass through udpgw - udp_mtu = BTap_GetMTU(&device) - (int)(sizeof(struct ipv4_header) + sizeof(struct udp_header)); - if (options.netif_ip6addr) { - int udp_ip6_mtu = BTap_GetMTU(&device) - (int)(sizeof(struct ipv6_header) + sizeof(struct udp_header)); - if (udp_mtu < udp_ip6_mtu) { - udp_mtu = udp_ip6_mtu; - } - } - if (udp_mtu < 0) { - udp_mtu = 0; + // uProxy: always calculate udp_mtu + // compute maximum UDP payload size we need to pass through udpgw + udp_mtu = BTap_GetMTU(&device) - (int)(sizeof(struct ipv4_header) + sizeof(struct udp_header)); + if (options.netif_ip6addr) { + int udp_ip6_mtu = BTap_GetMTU(&device) - (int)(sizeof(struct ipv6_header) + sizeof(struct udp_header)); + if (udp_mtu < udp_ip6_mtu) { + udp_mtu = udp_ip6_mtu; } + } + if (udp_mtu < 0) { + udp_mtu = 0; + } + if (options.udpgw_remote_server_addr) { // make sure our UDP payloads aren't too large for udpgw int udpgw_mtu = udpgw_compute_mtu(udp_mtu); @@ -513,7 +660,7 @@ void run() // init udpgw client if (!SocksUdpGwClient_Init(&udpgw_client, udp_mtu, DEFAULT_UDPGW_MAX_CONNECTIONS, options.udpgw_connection_buffer_size, UDPGW_KEEPALIVE_TIME, socks_server_addr, socks_auth_info, socks_num_auth_info, - udpgw_remote_server_addr, UDPGW_RECONNECT_TIME, &ss, NULL, udpgw_client_handler_received + udpgw_remote_server_addr, UDPGW_RECONNECT_TIME, &ss, NULL, udp_send_packet_to_device )) { BLog(BLOG_ERROR, "SocksUdpGwClient_Init failed"); goto fail4a; @@ -521,7 +668,7 @@ void run() } // init lwip init job - BPending_Init(&lwip_init_job, BReactor_PendingGroup(&ss), lwip_init_job_hadler, NULL); + BPending_Init(&lwip_init_job, BReactor_PendingGroup(&ss), lwip_init_job_handler, NULL); BPending_Set(&lwip_init_job); // init device write buffer @@ -548,6 +695,12 @@ void run() // init number of clients num_clients = 0; + // ==== UPROXY ==== + if (!udp_init(&udp_pcb)) { + goto fail5; + } + // ==== UPROXY ==== + // enter event loop BLog(BLOG_NOTICE, "entering event loop"); BReactor_Exec(&ss); @@ -589,6 +742,9 @@ void run() tcp_remove(tcp_tw_pcbs); // ==== PSIPHON ==== + // ==== UPROXY ==== + udp_free(&udp_pcb); + // ==== UPROXY ==== BReactor_RemoveTimer(&ss, &tcp_timer); BFree(device_write_buf); @@ -693,7 +849,8 @@ void init_arguments (const char* program_name) options.udpgw_remote_server_addr = NULL; options.udpgw_max_connections = DEFAULT_UDPGW_MAX_CONNECTIONS; options.udpgw_connection_buffer_size = DEFAULT_UDPGW_CONNECTION_BUFFER_SIZE; - options.udpgw_transparent_dns = 0; + options.transparent_dns = 0; + options.dns_server_address = NULL; options.tun_fd = 0; options.set_signal = 1; @@ -883,8 +1040,8 @@ int parse_arguments (int argc, char *argv[]) } i++; } - else if (!strcmp(arg, "--udpgw-transparent-dns")) { - options.udpgw_transparent_dns = 1; + else if (!strcmp(arg, "--transparent-dns")) { + options.transparent_dns = 1; } else { fprintf(stderr, "unknown option: %s\n", arg); @@ -996,6 +1153,13 @@ int process_arguments (void) return 0; } } + // resolve local DNS server address + if (options.dns_server_address) { + if (!BAddr_Parse2(&dns_server_address, options.dns_server_address, NULL, 0, 0)) { + BLog(BLOG_ERROR, "local dns server addr: BAddr_Parse2 failed"); + return 0; + } + } return 1; } @@ -1020,7 +1184,7 @@ BAddr baddr_from_lwip (int is_ipv6, const ipX_addr_t *ipx_addr, uint16_t port_ho return addr; } -void lwip_init_job_hadler (void *unused) +void lwip_init_job_handler (void *unused) { ASSERT(!quitting) ASSERT(netif_ipaddr.type == BADDR_TYPE_IPV4) @@ -1201,9 +1365,8 @@ void device_read_handler_send (void *unused, uint8_t *data, int data_len) int process_device_udp_packet (uint8_t *data, int data_len) { ASSERT(data_len >= 0) - - // do nothing if we don't have udpgw - if (!options.udpgw_remote_server_addr) { + // do nothing if we don't have udpgw or dns resolver + if (!options.udpgw_remote_server_addr && !options.dns_server_address) { goto fail; } @@ -1220,12 +1383,14 @@ int process_device_udp_packet (uint8_t *data, int data_len) case 4: { // ignore non-UDP packets if (data_len < sizeof(struct ipv4_header) || data[offsetof(struct ipv4_header, protocol)] != IPV4_PROTOCOL_UDP) { + BLog(BLOG_DEBUG, "got non-UDP packet, protocol: %d", data[offsetof(struct ipv4_header, protocol)]); goto fail; } // parse IPv4 header struct ipv4_header ipv4_header; if (!ipv4_check(data, data_len, &ipv4_header, &data, &data_len)) { + BLog(BLOG_WARNING, "got non-IP packet"); goto fail; } @@ -1251,9 +1416,9 @@ int process_device_udp_packet (uint8_t *data, int data_len) // if transparent DNS is enabled, any packet arriving at out netif // address to port 53 is considered a DNS packet - is_dns = (options.udpgw_transparent_dns && + is_dns = (options.transparent_dns && ipv4_header.destination_address == netif_ipaddr.ipv4 && - udp_header.dest_port == hton16(53)); + udp_header.dest_port == hton16(UDP_DNS_PORT)); } break; case 6: { @@ -1308,8 +1473,32 @@ int process_device_udp_packet (uint8_t *data, int data_len) goto fail; } - // submit packet to udpgw - SocksUdpGwClient_SubmitPacket(&udpgw_client, local_addr, remote_addr, is_dns, data, data_len); + char local_addr_str[BADDR_MAX_PRINT_LEN]; + BAddr_Print(&local_addr, local_addr_str); + char remote_addr_str[BADDR_MAX_PRINT_LEN]; + BAddr_Print(&remote_addr, remote_addr_str); + BLog(BLOG_INFO, "UDP: %s -> %s. DNS: %d", local_addr_str, remote_addr_str, is_dns); + + if (options.dns_server_address && is_dns) { + int sent_bytes = udp_send(udp_pcb.sockfd, data, data_len); + if (sent_bytes < data_len) { + BLog(BLOG_ERROR, "udp_send: sent %d bytes, expected %d", + sent_bytes, data_len); + return 1; + } + + char dns_id_str[DNS_ID_STRLEN]; + dns_get_header_id_str(dns_id_str, data); + if (!BStringMap_Set(&udp_pcb.map, dns_id_str, local_addr_str)) { + BLog(BLOG_ERROR, + "failed to associate dns request id to local address"); + return 1; + } + } else if (options.udpgw_remote_server_addr) { + // submit packet to udpgw + SocksUdpGwClient_SubmitPacket(&udpgw_client, local_addr, remote_addr, + is_dns, data, data_len); + } return 1; @@ -1442,6 +1631,12 @@ err_t listener_accept_func (void *arg, struct tcp_pcb *newpcb, err_t err) client->local_addr = baddr_from_lwip(PCB_ISIPV6(newpcb), &newpcb->local_ip, newpcb->local_port); client->remote_addr = baddr_from_lwip(PCB_ISIPV6(newpcb), &newpcb->remote_ip, newpcb->remote_port); + char local_addr_str[BADDR_MAX_PRINT_LEN]; + BAddr_Print(&client->local_addr, local_addr_str); + char remote_addr_str[BADDR_MAX_PRINT_LEN]; + BAddr_Print(&client->remote_addr, remote_addr_str); + BLog(BLOG_NOTICE, "TCP: %s -> %s", local_addr_str, remote_addr_str); + // get destination address BAddr addr = client->local_addr; #ifdef OVERRIDE_DEST_ADDR @@ -1968,7 +2163,7 @@ err_t client_sent_func (void *arg, struct tcp_pcb *tpcb, u16_t len) return ERR_OK; } -void udpgw_client_handler_received (void *unused, BAddr local_addr, BAddr remote_addr, const uint8_t *data, int data_len) +void udp_send_packet_to_device (void *unused, BAddr local_addr, BAddr remote_addr, const uint8_t *data, int data_len) { ASSERT(options.udpgw_remote_server_addr) ASSERT(local_addr.type == BADDR_TYPE_IPV4 || local_addr.type == BADDR_TYPE_IPV6) diff --git a/src/badvpn/tun2socks/tun2socks.h b/src/badvpn/tun2socks/tun2socks.h index caf5778..4c7399f 100644 --- a/src/badvpn/tun2socks/tun2socks.h +++ b/src/badvpn/tun2socks/tun2socks.h @@ -1,6 +1,6 @@ /* * Copyright (C) Ambroz Bizjak - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * 1. Redistributions of source code must retain the above copyright @@ -11,7 +11,7 @@ * 3. Neither the name of the author nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -44,3 +44,9 @@ // option to override the destination addresses to give the SOCKS server //#define OVERRIDE_DEST_ADDR "10.111.0.2:2000" + +// maximum limit for UDP packet length + #define UDP_MAX_DATAGRAM_BYTES 65535 + +// port for dns traffic + #define UDP_DNS_PORT 53 diff --git a/tun2socks.js b/tun2socks.js new file mode 100644 index 0000000..6d5ec18 --- /dev/null +++ b/tun2socks.js @@ -0,0 +1,18 @@ +/* globals cordova, window, Promise */ + +window.tun2socks = {}; + +window.tun2socks._genericHandler = function(method, params) { + "use strict"; + var args = Array.prototype.slice.call(arguments, 1); + return new Promise(function(resolve, reject) { + cordova.exec(resolve, reject, "Tun2Socks", method, args); + }); +}; + +module.exports = { + start: window.tun2socks._genericHandler.bind({}, "start"), + stop: window.tun2socks._genericHandler.bind({}, "stop"), + onDisconnect: window.tun2socks._genericHandler.bind({}, "onDisconnect"), + deviceSupportsPlugin: window.tun2socks._genericHandler.bind({}, "deviceSupportsPlugin") +};