Parcourir la source

Fixes for Android VpnService with Go client. This is now working.

Rod Hynes il y a 11 ans
Parent
commit
c1762e6a84

+ 1 - 1
AndroidApp/README.md

@@ -19,7 +19,7 @@ Status
 Native libraries
 --------------------------------------------------------------------------------
 
-`app\src\main\jniLibs\<platform>\libtun2socks.so` is built from the Psiphon fork of badvpn. Source code is here: [https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/](https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/).
+`app\src\main\jniLibs\<platform>\libtun2socks.so` is built from the Psiphon fork of badvpn. Source code is here: [https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/](https://bitbucket.org/psiphon/psiphon-circumvention-system/src/default/Android/badvpn/). The source was modified to change the package name to `ca.psiphon.psibot`.
 
 Go client binary and config file
 --------------------------------------------------------------------------------

+ 1 - 0
AndroidApp/app/src/main/AndroidManifest.xml

@@ -3,6 +3,7 @@
     package="ca.psiphon.psibot" >
 
     <uses-permission android:name="android.permission.INTERNET"></uses-permission>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
 
     <application
         android:name="ca.psiphon.psibot.App"

+ 63 - 46
AndroidApp/app/src/main/java/ca/psiphon/psibot/Client.java

@@ -22,61 +22,61 @@ package ca.psiphon.psibot;
 import android.content.Context;
 import android.os.Build;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Scanner;
 import java.util.concurrent.CountDownLatch;
 
 public class Client {
 
     private final Context mContext;
+    private final CountDownLatch mTunnelStartedSignal;
     private File mRootDirectory;
     private File mExecutableFile;
     private File mConfigFile;
     private Process mProcess;
-    private Thread mThread;
+    private Thread mStdoutThread;
     private int mLocalSocksProxyPort;
     private int mLocalHttpProxyPort;
     private List<String> mHomePages;
 
-    public Client(Context context) {
+    public Client(Context context, CountDownLatch tunnelStartedSignal) {
         mContext = context;
+        mTunnelStartedSignal = tunnelStartedSignal;
     }
 
-    public void start(final CountDownLatch tunnelStartedSignal) throws Utils.PsibotError {
+    public void start() throws Utils.PsibotError {
         stop();
         prepareFiles();
 
-        ProcessBuilder processBuilder =
-                new ProcessBuilder(
-                        mExecutableFile.getAbsolutePath(),
-                        "--config", mConfigFile.getAbsolutePath());
-        processBuilder.directory(mRootDirectory);
-
         try {
-            mProcess = processBuilder.start();
+            mProcess = new ProcessBuilder(
+                    mExecutableFile.getAbsolutePath(),
+                    "--config", mConfigFile.getAbsolutePath())
+                    .directory(mRootDirectory)
+                    .redirectErrorStream(true)
+                    .start();
         } catch (IOException e) {
             throw new Utils.PsibotError("failed to start client process", e);
         }
 
-        mThread = new Thread(new Runnable() {
+        mStdoutThread = new Thread(new Runnable() {
             @Override
             public void run() {
-                Scanner stdout = new Scanner(mProcess.getInputStream());
-                while(stdout.hasNextLine()) {
-                    String line = stdout.nextLine();
-                    boolean isTunnelStarted = parseLine(line);
-                    if (isTunnelStarted) {
-                        tunnelStartedSignal.countDown();
-                    }
-                    Log.addEntry(line);
-                }
-                stdout.close();
+                consumeStream(mProcess.getInputStream());
+                Log.addEntry("Psiphon client stopping");
             }
         });
-        mThread.start();
+        mStdoutThread.start();
+
+        mLocalSocksProxyPort = 0;
+        mLocalHttpProxyPort = 0;
+        mHomePages = new ArrayList<String>();
+
         Log.addEntry("Psiphon client started");
     }
 
@@ -88,36 +88,17 @@ public class Client {
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
             }
+            Log.addEntry("Psiphon client stopped");
         }
-        if (mThread != null) {
+        if (mStdoutThread != null) {
             try {
-                mThread.join();
+                mStdoutThread.join();
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
             }
         }
         mProcess = null;
-        mThread = null;
-        Log.addEntry("Psiphon client stopped");
-    }
-
-    public synchronized boolean parseLine(String line) {
-        // TODO: this is based on temporary log line formats
-        final String socksProxy = "SOCKS-PROXY local SOCKS proxy running at address 127.0.0.1:";
-        final String httpProxy = "HTTP-PROXY local HTTP proxy running at address 127.0.0.1:";
-        final String homePage = "HOMEPAGE ";
-        final String tunnelStarted = "TUNNEL tunnel started";
-        int index;
-        if (-1 != (index = line.indexOf(socksProxy))) {
-            mLocalSocksProxyPort = Integer.parseInt(line.substring(index + homePage.length()));
-        } else if (-1 != (index = line.indexOf(httpProxy))) {
-            mLocalHttpProxyPort = Integer.parseInt(line.substring(index + homePage.length()));
-        } else if (-1 != (index = line.indexOf(homePage))) {
-            mHomePages.add(line.substring(index + homePage.length()));
-        } else if (line.contains(tunnelStarted)) {
-            return true;
-        }
-        return false;
+        mStdoutThread = null;
     }
 
     public synchronized int getLocalSocksProxyPort() {
@@ -146,4 +127,40 @@ public class Client {
             throw new Utils.PsibotError("failed to prepare client files", e);
         }
     }
+
+    private void consumeStream(InputStream inputStream) {
+        try {
+            BufferedReader reader =
+                    new BufferedReader(new InputStreamReader(inputStream));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                parseLine(line);
+                // TODO: parse and use the Go client timestamp
+                // Don't display the first 20 characters: the Go client log timestamp
+                Log.addEntry(line.substring(20));
+            }
+            reader.close();
+        } catch (IOException e) {
+            // TODO: stop service if exiting due to error?
+            Log.addEntry("Psiphon client consume error: " + e.getMessage());
+        }
+    }
+
+    private synchronized void parseLine(String line) {
+        // TODO: this is based on temporary log line formats
+        final String socksProxy = "SOCKS-PROXY local SOCKS proxy running at address 127.0.0.1:";
+        final String httpProxy = "HTTP-PROXY local HTTP proxy running at address 127.0.0.1:";
+        final String homePage = "HOMEPAGE ";
+        final String tunnelStarted = "TUNNEL tunnel started";
+        int index;
+        if (-1 != (index = line.indexOf(socksProxy))) {
+            mLocalSocksProxyPort = Integer.parseInt(line.substring(index + socksProxy.length()));
+        } else if (-1 != (index = line.indexOf(httpProxy))) {
+            mLocalHttpProxyPort = Integer.parseInt(line.substring(index + httpProxy.length()));
+        } else if (-1 != (index = line.indexOf(homePage))) {
+            mHomePages.add(line.substring(index + homePage.length()));
+        } else if (line.contains(tunnelStarted)) {
+            mTunnelStartedSignal.countDown();
+        }
+    }
 }

+ 1 - 0
AndroidApp/app/src/main/java/ca/psiphon/psibot/MainActivity.java

@@ -42,6 +42,7 @@ public class MainActivity extends Activity {
 
     @Override
     protected void onResume() {
+        super.onResume();
         startVpn();
     }
 

+ 3 - 3
AndroidApp/app/src/main/java/ca/psiphon/psibot/Service.java

@@ -62,14 +62,14 @@ public class Service extends VpnService {
         mThread = new Thread(new Runnable() {
             @Override
             public void run() {
+                CountDownLatch tunnelStartedSignal = new CountDownLatch(1);
                 SocketProtector socketProtector = new SocketProtector(Service.this);
-                Client client = new Client(Service.this);
+                Client client = new Client(Service.this, tunnelStartedSignal);
                 try {
                     socketProtector.start();
                     // TODO: what if client local proxies unbind? in this case it's better if Go client keeps its proxies up permanently.
                     // TODO: monitor tunnel messages and update notification UI when re-connecting, etc.
-                    CountDownLatch tunnelStartedSignal = new CountDownLatch(1);
-                    client.start(tunnelStartedSignal);
+                    client.start();
                     while (true) {
                         if (tunnelStartedSignal.await(100, TimeUnit.MILLISECONDS)) {
                             break;

+ 23 - 4
AndroidApp/app/src/main/java/ca/psiphon/psibot/SocketProtector.java

@@ -54,16 +54,35 @@ public class SocketProtector {
             public void run() {
                 String stoppingMessage = "socket protector stopping";
                 try {
-                    LocalSocket socket = mLocalServerSocket.accept();
-                    // TODO: need to do a read()?
-                    for (FileDescriptor fileDescriptor : socket.getAncillaryFileDescriptors()) {
-                        protectSocket(fileDescriptor);
+                    while (true) {
+                        LocalSocket socket = mLocalServerSocket.accept();
+                        try {
+                            // Client must send a '0' byte, along with the out-of-band file descriptors
+                            if (0 != socket.getInputStream().read()) {
+                                throw new Utils.PsibotError("unexpected socket protector request");
+                            }
+                            // Response of '0' indicates no error
+                            int response = 0;
+                            for (FileDescriptor fileDescriptor : socket.getAncillaryFileDescriptors()) {
+                                try {
+                                    protectSocket(fileDescriptor);
+                                } catch (Utils.PsibotError e) {
+                                    Log.addEntry("socket protector: " + e.getMessage());
+                                    response = 1;
+                                    break;
+                                }
+                            }
+                            socket.getOutputStream().write(response);
+                        } finally {
+                            socket.close();
+                        }
                     }
                 } catch (Utils.PsibotError e) {
                     stoppingMessage += ": " + e.getMessage();
                 } catch (IOException e) {
                     stoppingMessage += ": " + e.getMessage();
                 }
+                // TODO: stop service if exiting due to error?
                 Log.addEntry(stoppingMessage);
             }
         });

+ 1 - 2
AndroidApp/app/src/main/java/ca/psiphon/psibot/Tun2Socks.java

@@ -95,8 +95,7 @@ public class Tun2Socks {
 
     private native static void terminateTun2Socks();
     
-    static
-    {
+    static {
         System.loadLibrary("tun2socks");
     }
 }

+ 6 - 1
AndroidApp/app/src/main/java/ca/psiphon/psibot/Utils.java

@@ -36,7 +36,6 @@ import java.net.SocketException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.zip.ZipInputStream;
 
 
 public class Utils {
@@ -78,10 +77,16 @@ public class Utils {
     public static void writeRawResourceFile(
             Context context, int resId, File file, boolean setExecutable) throws IOException {
         file.delete();
+        // TODO: is this compression redundant?
+        /*
         InputStream zippedAsset = context.getResources().openRawResource(resId);
         ZipInputStream zipStream = new ZipInputStream(zippedAsset);
         zipStream.getNextEntry();
         Utils.copyStream(zipStream, new FileOutputStream(file));
+        */
+        Utils.copyStream(
+                context.getResources().openRawResource(resId),
+                new FileOutputStream(file));
         if (setExecutable && !file.setExecutable(true)) {
             throw new IOException("failed to set file as executable");
         }

BIN
AndroidApp/app/src/main/jniLibs/arm64-v8a/libtun2socks.so


BIN
AndroidApp/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so


BIN
AndroidApp/app/src/main/jniLibs/armeabi/libtun2socks.so


BIN
AndroidApp/app/src/main/jniLibs/mips/libtun2socks.so


BIN
AndroidApp/app/src/main/jniLibs/mips64/libtun2socks.so


BIN
AndroidApp/app/src/main/jniLibs/x86/libtun2socks.so


BIN
AndroidApp/app/src/main/jniLibs/x86_64/libtun2socks.so


+ 3 - 1
AndroidApp/app/src/main/res/raw/psiphon_config

@@ -8,5 +8,7 @@
     "LocalSocksProxyPort" : 0,
     "EgressRegion" : "",
     "TunnelProtocol" : "",
-    "ConnectionWorkerPoolSize" : 10
+    "ConnectionWorkerPoolSize" : 10,
+    "BindToDeviceServiceAddress" : "@/psibot/socketProtector",
+    "BindToDeviceDnsServer" : "8.8.4.4"
 }

BIN
AndroidApp/app/src/main/res/raw/psiphon_tunnel_core_arm


+ 0 - 2
README.md

@@ -51,7 +51,6 @@ Roadmap
 * log noise: "use of closed network connection"
 * log noise(?): 'Unsolicited response received on idle HTTP channel starting with "H"'
 * use ContextError in more places
-* build/test on Android and iOS
 * reconnection busy loop when no network available (ex. close laptop)
 
 ### TODO (future)
@@ -67,7 +66,6 @@ Roadmap
 * implement page view stats
 * implement local traffic stats (e.g., to display bytes sent/received)
 * control interface (w/ event messages)?
-* VpnService compatibility
 * upstream proxy support
 * support upgrades
   * download entire client

+ 1 - 1
psiphon/LookupIP.go

@@ -38,7 +38,7 @@ const DNS_PORT = 53
 // socket, binds it to the device, and makes an explicit DNS request
 // to the specified DNS resolver.
 func LookupIP(host string, config *DialConfig) (addrs []net.IP, err error) {
-	if config.BindToDeviceServiceAddr != "" {
+	if config.BindToDeviceServiceAddress != "" {
 		return bindLookupIP(host, config)
 	}
 	return net.LookupIP(host)

+ 1 - 1
psiphon/TCPConn_unix.go

@@ -53,7 +53,7 @@ func interruptibleTCPDial(addr string, config *DialConfig) (conn *TCPConn, err e
 		}
 	}()
 	// Note: this step is not interruptible
-	if config.BindToDeviceServiceAddr != "" {
+	if config.BindToDeviceServiceAddress != "" {
 		err = bindToDevice(socketFd, config)
 		if err != nil {
 			return nil, ContextError(err)

+ 1 - 1
psiphon/bindToDevice.go

@@ -37,7 +37,7 @@ import (
 // setsockopt(SO_BINDTODEVICE). This socket options requires root, which is
 // why this is delegated to a remote service.
 func bindToDevice(socketFd int, config *DialConfig) error {
-	addr, err := net.ResolveUnixAddr("unix", config.BindToDeviceServiceAddr)
+	addr, err := net.ResolveUnixAddr("unix", config.BindToDeviceServiceAddress)
 	if err != nil {
 		return ContextError(err)
 	}

+ 3 - 3
psiphon/conn.go

@@ -40,14 +40,14 @@ type DialConfig struct {
 
 	// BindToDevice parameters are used to exclude connections and
 	// associated DNS requests from VPN routing.
-	// When BindToDeviceServiceAddr is not blank, any underlying socket is
+	// When BindToDeviceServiceAddress is not blank, any underlying socket is
 	// submitted to the device binding service at that address before connecting.
 	// The service should bind the socket to a device so that it doesn't route
 	// through a VPN interface. This service is also used to bind UDP sockets used
 	// for DNS requests, in which case BindToDeviceDnsServer is used as the
 	// DNS server.
-	BindToDeviceServiceAddr string
-	BindToDeviceDnsServer   string
+	BindToDeviceServiceAddress string
+	BindToDeviceDnsServer      string
 }
 
 // Dialer is a custom dialer compatible with http.Transport.Dial.

+ 4 - 3
psiphon/runTunnel.go

@@ -37,7 +37,8 @@ import (
 // if there's not already an established tunnel. This function is to be used in a pool
 // of goroutines.
 func establishTunnelWorker(
-	tunnelProtocol, sessionId string,
+	config *Config,
+	sessionId string,
 	workerWaitGroup *sync.WaitGroup,
 	candidateServerEntries chan *ServerEntry,
 	broadcastStopWorkers chan struct{},
@@ -53,7 +54,7 @@ func establishTunnelWorker(
 			return
 		default:
 		}
-		tunnel, err := EstablishTunnel(tunnelProtocol, sessionId, serverEntry, pendingConns)
+		tunnel, err := EstablishTunnel(config, sessionId, serverEntry, pendingConns)
 		if err != nil {
 			// TODO: distingush case where conn is interrupted?
 			Notice(NOTICE_INFO, "failed to connect to %s: %s", serverEntry.IpAddress, err)
@@ -90,7 +91,7 @@ func establishTunnel(config *Config, sessionId string) (tunnel *Tunnel, err erro
 	for i := 0; i < config.ConnectionWorkerPoolSize; i++ {
 		workerWaitGroup.Add(1)
 		go establishTunnelWorker(
-			config.TunnelProtocol, sessionId,
+			config, sessionId,
 			workerWaitGroup, candidateServerEntries, broadcastStopWorkers,
 			pendingConns, establishedTunnels)
 	}

+ 11 - 8
psiphon/tunnel.go

@@ -77,19 +77,20 @@ func (tunnel *Tunnel) Close() {
 // the first protocol in SupportedTunnelProtocols that's also in the
 // server capabilities is used.
 func EstablishTunnel(
-	requiredProtocol, sessionId string,
+	config *Config,
+	sessionId string,
 	serverEntry *ServerEntry,
 	pendingConns *Conns) (tunnel *Tunnel, err error) {
 	// Select the protocol
 	var selectedProtocol string
 	// TODO: properly handle protocols (e.g. FRONTED-MEEK-OSSH) vs. capabilities (e.g., {FRONTED-MEEK, OSSH})
 	// for now, the code is simply assuming that MEEK capabilities imply OSSH capability.
-	if requiredProtocol != "" {
-		requiredCapability := strings.TrimSuffix(requiredProtocol, "-OSSH")
+	if config.TunnelProtocol != "" {
+		requiredCapability := strings.TrimSuffix(config.TunnelProtocol, "-OSSH")
 		if !Contains(serverEntry.Capabilities, requiredCapability) {
 			return nil, ContextError(fmt.Errorf("server does not have required capability"))
 		}
-		selectedProtocol = requiredProtocol
+		selectedProtocol = config.TunnelProtocol
 	} else {
 		// Order of SupportedTunnelProtocols is default preference order
 		for _, protocol := range SupportedTunnelProtocols {
@@ -128,10 +129,12 @@ func EstablishTunnel(
 	}
 	// Create the base transport: meek or direct connection
 	dialConfig := &DialConfig{
-		ConnectTimeout: TUNNEL_CONNECT_TIMEOUT,
-		ReadTimeout:    TUNNEL_READ_TIMEOUT,
-		WriteTimeout:   TUNNEL_WRITE_TIMEOUT,
-		PendingConns:   pendingConns,
+		ConnectTimeout:             TUNNEL_CONNECT_TIMEOUT,
+		ReadTimeout:                TUNNEL_READ_TIMEOUT,
+		WriteTimeout:               TUNNEL_WRITE_TIMEOUT,
+		PendingConns:               pendingConns,
+		BindToDeviceServiceAddress: config.BindToDeviceServiceAddress,
+		BindToDeviceDnsServer:      config.BindToDeviceDnsServer,
 	}
 	var conn Conn
 	if useMeek {