Просмотр исходного кода

Updates to Psibot

* Refactor VpnService/Tun2Socks and Psiphon Core provider
  into single module, as a step towards a library shared
  across multiple Whole Device Mode apps.

* Fix: stopping/restarting Psiphon Core now works reliably.

* Fix (temporary workaround): runtime exception thrown
  in Seq.java due to reference counting problem.

* Removed some non-essential features from Psibot.
Rod Hynes 11 лет назад
Родитель
Сommit
cf9a9dce06

+ 551 - 0
AndroidApp/app/src/main/java/ca/psiphon/PsiphonVpn.java

@@ -0,0 +1,551 @@
+/*
+ * Copyright (c) 2015, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package ca.psiphon;
+
+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 org.apache.http.conn.util.InetAddressUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import go.Go;
+import go.psi.Psi;
+
+public class PsiphonVpn extends Psi.PsiphonProvider.Stub {
+
+    public interface HostService {
+        public String getAppName();
+        public VpnService getVpnService();
+        public VpnService.Builder newVpnServiceBuilder();
+        public InputStream getPsiphonConfigResource();
+        public void customizeConfigParameters(JSONObject config);
+        public void logWarning(String message);
+        public void logInfo(String message);
+    }
+
+    private final HostService mHostService;
+    private PrivateAddress mPrivateAddress;
+    private ParcelFileDescriptor mTunFd;
+    private int mLocalSocksProxyPort;
+    private boolean mRoutingThroughTunnel;
+    private Thread mTun2SocksThread;
+
+    // Only one PsiphonVpn instance may exist at a time, as the underlying
+    // go.psi.Psi and tun2socks implementations each contain global state.
+    private static PsiphonVpn mPsiphonVpn;
+
+    public static synchronized PsiphonVpn newPsiphonVpn(HostService hostService) {
+        if (mPsiphonVpn != null) {
+            mPsiphonVpn.stop();
+        }
+        mPsiphonVpn = new PsiphonVpn(hostService);
+        return mPsiphonVpn;
+    }
+
+    private PsiphonVpn(HostService hostService) {
+        Go.init(hostService.getVpnService());
+        mHostService = hostService;
+        mLocalSocksProxyPort = 0;
+        mRoutingThroughTunnel = 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() throws Exception {
+        if (mTunFd == null) {
+            // Most likely, startRouting() was not called before this function.
+            throw new Exception("startTunneling: missing tun fd");
+        }
+        startPsiphon();
+    }
+
+    public synchronized void restartPsiphon() throws Exception {
+        stopPsiphon();
+        startPsiphon();
+    }
+
+    public synchronized void stop() {
+        stopPsiphon();
+        stopVpn();
+        mLocalSocksProxyPort = 0;
+    }
+
+    //----------------------------------------------------------------------------------------------
+    // 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 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"));
+
+            mTunFd = 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 (mTunFd == 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;
+            }
+            mHostService.logInfo("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 synchronized void setLocalSocksProxyPort(int port) {
+        mLocalSocksProxyPort = port;
+    }
+
+    private synchronized void routeThroughTunnel() {
+        if (mRoutingThroughTunnel) {
+            return;
+        }
+        mRoutingThroughTunnel = true;
+        String socksServerAddress = "127.0.0.1:" + Integer.toString(mLocalSocksProxyPort);
+        String udpgwServerAddress = "127.0.0.1:" + Integer.toString(UDPGW_SERVER_PORT);
+        startTun2Socks(
+                mTunFd,
+                VPN_INTERFACE_MTU,
+                mPrivateAddress.mRouter,
+                VPN_INTERFACE_NETMASK,
+                socksServerAddress,
+                udpgwServerAddress,
+                true);
+        mTunFd = null;
+        mHostService.logInfo("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() {
+        if (mTunFd != null) {
+            try {
+                mTunFd.close();
+            } catch (IOException e) {
+            }
+            mTunFd = null;
+        }
+        stopTun2Socks();
+        mRoutingThroughTunnel = false;
+    }
+
+    //----------------------------------------------------------------------------------------------
+    // PsiphonProvider (Core support) interface implementation
+    //----------------------------------------------------------------------------------------------
+
+    @Override
+    public void Notice(String noticeJSON) {
+        handlePsiphonNotice(noticeJSON);
+    }
+
+    @Override
+    public void BindToDevice(long fileDescriptor) throws Exception {
+        if (!mHostService.getVpnService().protect((int)fileDescriptor)) {
+            throw new Exception("protect socket failed");
+        }
+    }
+
+    @Override
+    public long HasNetworkConnectivity() {
+        // TODO: change to bool return value once gobind supports that type
+        return hasNetworkConnectivity(mHostService.getVpnService()) ? 1 : 0;
+    }
+
+    //----------------------------------------------------------------------------------------------
+    // Psiphon Tunnel Core
+    //----------------------------------------------------------------------------------------------
+
+    private void startPsiphon() throws Exception {
+        stopPsiphon();
+        mHostService.logInfo("starting Psiphon");
+        try {
+            Psi.Start(
+                loadPsiphonConfig(mHostService.getVpnService()),
+                "", // TODO: supply embedded server list
+                this);
+        } catch (java.lang.Exception e) {
+            throw new Exception("failed to start Psiphon", e);
+        }
+        mHostService.logInfo("Psiphon started");
+    }
+
+    private void stopPsiphon() {
+        mHostService.logInfo("stopping Psiphon");
+        Psi.Stop();
+        mHostService.logInfo("Psiphon stopped");
+    }
+
+    private String loadPsiphonConfig(Context context)
+            throws IOException, JSONException {
+
+        // If we can obtain a DNS resolver for the active network,
+        // prefer that for DNS resolution in BindToDevice mode.
+        String dnsResolver = null;
+        try {
+            dnsResolver = getFirstActiveNetworkDnsResolver(context);
+        } catch (Exception e) {
+            mHostService.logWarning("failed to get active network DNS resolver: " + e.getMessage());
+            // Proceed with default value in config file
+        }
+
+        // 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(
+                readInputStreamToString(
+                    mHostService.getPsiphonConfigResource()));
+
+        if (dnsResolver != null) {
+            json.put("BindToDeviceDnsServer", dnsResolver);
+        }
+
+        // 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", context.getFilesDir());
+        json.put("DataStoreTempDirectory", context.getCacheDir());
+
+        mPsiphonVpn.mHostService.customizeConfigParameters(json);
+
+        if (mLocalSocksProxyPort != 0) {
+            // 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);
+        }
+
+        return json.toString();
+    }
+
+    private void handlePsiphonNotice(String noticeJSON) {
+        try {
+            JSONObject notice = new JSONObject(noticeJSON);
+            String noticeType = notice.getString("noticeType");
+            if (noticeType.equals("Tunnels")) {
+                int count = notice.getJSONObject("data").getInt("count");
+                if (count > 0) {
+                    routeThroughTunnel();
+                }
+            } else if (noticeType.equals("ListeningSocksProxyPort")) {
+                setLocalSocksProxyPort(notice.getJSONObject("data").getInt("port"));
+            /*
+            } else if (noticeType.equals("Homepage")) {
+                String homePage = notice.getJSONObject("data").getString("url");
+            */
+            }
+            String displayNotice = noticeType + " " + notice.getJSONObject("data").toString();
+            mHostService.logInfo(displayNotice);
+        } catch (JSONException e) {
+            // Ignore notice
+        }
+    }
+
+    //----------------------------------------------------------------------------------------------
+    // Tun2Socks
+    //----------------------------------------------------------------------------------------------
+
+    private void startTun2Socks(
+            final ParcelFileDescriptor vpnInterfaceFileDescriptor,
+            final int vpnInterfaceMTU,
+            final String vpnIpAddress,
+            final String vpnNetMask,
+            final String socksServerAddress,
+            final String udpgwServerAddress,
+            final boolean udpgwTransparentDNS) {
+        stopTun2Socks();
+        mTun2SocksThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                runTun2Socks(
+                        vpnInterfaceFileDescriptor.detachFd(),
+                        vpnInterfaceMTU,
+                        vpnIpAddress,
+                        vpnNetMask,
+                        socksServerAddress,
+                        udpgwServerAddress,
+                        udpgwTransparentDNS ? 1 : 0);
+            }
+        });
+        mPsiphonVpn = this;
+        mTun2SocksThread.start();
+        mHostService.logInfo("tun2socks started");
+    }
+
+    private void stopTun2Socks() {
+        if (mTun2SocksThread != null) {
+            terminateTun2Socks();
+            try {
+                mTun2SocksThread.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            mTun2SocksThread = null;
+            mHostService.logInfo("tun2socks stopped");
+        }
+    }
+
+    public static void logTun2Socks(String level, String channel, String msg) {
+        String logMsg = "tun2socks: " + level + "(" + channel + "): " + msg;
+        mPsiphonVpn.mHostService.logWarning(logMsg);
+    }
+
+    private native static int runTun2Socks(
+            int vpnInterfaceFileDescriptor,
+            int vpnInterfaceMTU,
+            String vpnIpAddress,
+            String vpnNetMask,
+            String socksServerAddress,
+            String udpgwServerAddress,
+            int udpgwTransparentDNS);
+
+    private native static void terminateTun2Socks();
+
+    static {
+        System.loadLibrary("tun2socks");
+    }
+
+    //----------------------------------------------------------------------------------------------
+    // Implementation: Network Utils
+    //----------------------------------------------------------------------------------------------
+
+    private static boolean hasNetworkConnectivity(Context context) {
+        ConnectivityManager connectivityManager =
+                (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        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<String, PrivateAddress> candidates = new HashMap<String, PrivateAddress>();
+        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<NetworkInterface> 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<InetAddress> 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<InetAddress> getActiveNetworkDnsResolvers(Context context)
+            throws Exception {
+        final String errorMessage = "getActiveNetworkDnsResolvers failed";
+        ArrayList<InetAddress> dnsAddresses = new ArrayList<InetAddress>();
+        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 (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;
+    }
+
+    //----------------------------------------------------------------------------------------------
+    // Implementation: Resource Utils
+    //----------------------------------------------------------------------------------------------
+
+    private static String readInputStreamToString(InputStream inputStream) throws IOException {
+        return new String(readInputStreamToBytes(inputStream), "UTF-8");
+    }
+
+    private static byte[] readInputStreamToBytes(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        int readCount;
+        byte[] buffer = new byte[16384];
+        while ((readCount = inputStream.read(buffer, 0, buffer.length)) != -1) {
+            outputStream.write(buffer, 0, readCount);
+        }
+        outputStream.flush();
+        inputStream.close();
+        return outputStream.toByteArray();
+    }
+
+    //----------------------------------------------------------------------------------------------
+    // 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());
+        }
+    }
+}

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

@@ -22,13 +22,10 @@ package ca.psiphon.psibot;
 import android.app.Application;
 import android.app.Application;
 import android.preference.PreferenceManager;
 import android.preference.PreferenceManager;
 
 
-import go.Go;
-
 public class App extends Application {
 public class App extends Application {
 
 
     @Override
     @Override
     public void onCreate() {
     public void onCreate() {
-        Go.init(this.getApplicationContext());
         Log.initialize();
         Log.initialize();
         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
     }
     }

+ 0 - 24
AndroidApp/app/src/main/java/ca/psiphon/psibot/Log.java

@@ -19,9 +19,6 @@
 
 
 package ca.psiphon.psibot;
 package ca.psiphon.psibot;
 
 
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
 import android.os.Handler;
 import android.os.Handler;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -95,25 +92,4 @@ public class Log {
     public synchronized static void unregisterObserver(Observer observer) {
     public synchronized static void unregisterObserver(Observer observer) {
         mObservers.remove(observer);
         mObservers.remove(observer);
     }
     }
-
-    public synchronized static void composeEmail(Context context) {
-        try {
-            StringBuilder body = new StringBuilder();
-            for (Entry entry : mEntries) {
-                body.append(entry.mTimestamp);
-                body.append(": ");
-                body.append(entry.mMessage);
-                body.append("\n");
-            }
-
-            Intent intent = new Intent(Intent.ACTION_SEND);
-            intent.setType("message/rfc822");
-            intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"feedback+psibot@psiphon.ca"});
-            intent.putExtra(Intent.EXTRA_SUBJECT, "Psibot Logs");
-            intent.putExtra(Intent.EXTRA_TEXT, body.toString());
-            context.startActivity(intent);
-        } catch (ActivityNotFoundException e) {
-            Log.addEntry("compose log email failed: " + e.getMessage());
-        }
-    }
 }
 }

+ 5 - 26
AndroidApp/app/src/main/java/ca/psiphon/psibot/MainActivity.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2014, Psiphon Inc.
+ * Copyright (c) 2015, Psiphon Inc.
  * All rights reserved.
  * All rights reserved.
  *
  *
  * This program is free software: you can redistribute it and/or modify
  * This program is free software: you can redistribute it and/or modify
@@ -43,7 +43,7 @@ public class MainActivity extends Activity {
     @Override
     @Override
     protected void onResume() {
     protected void onResume() {
         super.onResume();
         super.onResume();
-        startVpn();
+        start();
     }
     }
 
 
     @Override
     @Override
@@ -55,53 +55,32 @@ public class MainActivity extends Activity {
     @Override
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
         switch (item.getItemId()) {
-        case R.id.action_email_log:
-            Log.composeEmail(this);
-            return true;
         case R.id.action_settings:
         case R.id.action_settings:
             startActivity(new Intent(this, SettingsActivity.class));
             startActivity(new Intent(this, SettingsActivity.class));
             return true;
             return true;
-        case R.id.action_quit:
-            stopVpnService();
-            finish();
-            return true;
         }
         }
         return super.onOptionsItemSelected(item);
         return super.onOptionsItemSelected(item);
     }
     }
 
 
     protected static final int REQUEST_CODE_PREPARE_VPN = 100;
     protected static final int REQUEST_CODE_PREPARE_VPN = 100;
 
 
-    protected void startVpn() {
-        if (Service.isRunning()) {
-            return;
-        }
+    protected void start() {
         try {
         try {
             Intent intent = VpnService.prepare(this);
             Intent intent = VpnService.prepare(this);
             if (intent != null) {
             if (intent != null) {
                 startActivityForResult(intent, REQUEST_CODE_PREPARE_VPN);
                 startActivityForResult(intent, REQUEST_CODE_PREPARE_VPN);
             } else {
             } else {
-                startVpnService();
+                startService(new Intent(this, Service.class));
             }
             }
         } catch (ActivityNotFoundException e) {
         } catch (ActivityNotFoundException e) {
             Log.addEntry("prepare VPN failed: " + e.getMessage());
             Log.addEntry("prepare VPN failed: " + e.getMessage());
         }
         }
     }
     }
 
 
-    protected void startVpnService() {
-        startService(new Intent(this, Service.class));
-    }
-
-    protected void stopVpnService() {
-        // Note: Tun2Socks.stop() closes the VpnService file descriptor.
-        // This is necessary in order to stop the VpnService while running.
-        Tun2Socks.stop();
-        stopService(new Intent(this, Service.class));
-    }
-
     @Override
     @Override
     protected void onActivityResult(int request, int result, Intent data) {
     protected void onActivityResult(int request, int result, Intent data) {
         if (request == REQUEST_CODE_PREPARE_VPN && result == RESULT_OK) {
         if (request == REQUEST_CODE_PREPARE_VPN && result == RESULT_OK) {
-            startVpnService();
+            startService(new Intent(this, Service.class));
         }
         }
     }
     }
 }
 }

+ 0 - 204
AndroidApp/app/src/main/java/ca/psiphon/psibot/Psiphon.java

@@ -1,204 +0,0 @@
-/*
- * Copyright (c) 2014, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * 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 <http://www.gnu.org/licenses/>.
- *
- */
-
-package ca.psiphon.psibot;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.net.VpnService;
-import android.preference.PreferenceManager;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-
-import go.psi.Psi;
-
-public class Psiphon extends Psi.PsiphonProvider.Stub {
-
-    private final VpnService mVpnService;
-    private final CountDownLatch mTunnelStartedSignal;
-    private int mLocalSocksProxyPort;
-    private int mLocalHttpProxyPort;
-    private Set<String> mHomePages;
-
-    public Psiphon(VpnService vpnService, CountDownLatch tunnelStartedSignal) {
-        mVpnService = vpnService;
-        mTunnelStartedSignal = tunnelStartedSignal;
-    }
-
-    // PsiphonProvider.Notice
-    @Override
-    public void Notice(String noticeJSON) {
-        handleNotice(noticeJSON);
-    }
-
-    // PsiphonProvider.BindToDevice
-    @Override
-    public void BindToDevice(long fileDescriptor) throws Exception {
-        if (!mVpnService.protect((int)fileDescriptor)) {
-            throw new Exception("protect socket failed");
-        }
-    }
-
-    // PsiphonProvider.HasNetworkConnectivity
-    @Override
-    public long HasNetworkConnectivity() {
-        // TODO: change to bool return value once gobind supports that type
-        return Utils.hasNetworkConnectivity(mVpnService) ? 1 : 0;
-    }
-
-    public void start() throws Utils.PsibotError {
-        Psi.Stop();
-
-        mLocalSocksProxyPort = 0;
-        mLocalHttpProxyPort = 0;
-        mHomePages = new HashSet<String>();
-
-        // TODO: supply embedded server list
-        String embeddedServerEntryList = "";
-
-        try {
-            Psi.Start(loadConfig(mVpnService), embeddedServerEntryList, this);
-        } catch (Exception e) {
-            throw new Utils.PsibotError("failed to start Psiphon", e);
-        }
-
-        Log.addEntry("Psiphon started");
-    }
-
-    public void stop() {
-        Psi.Stop();
-        Log.addEntry("Psiphon stopped");
-    }
-
-    public synchronized int getLocalSocksProxyPort() {
-        return mLocalSocksProxyPort;
-    }
-
-    public synchronized int getLocalHttpProxyPort() {
-        return mLocalHttpProxyPort;
-    }
-
-    public synchronized Set<String> getHomePages() {
-        return mHomePages != null ? new HashSet<String>(mHomePages) : new HashSet<String>();
-    }
-
-    private String loadConfig(Context context)
-            throws IOException, JSONException, Utils.PsibotError {
-
-        // If we can obtain a DNS resolver for the active network,
-        // prefer that for DNS resolution in BindToDevice mode.
-        String dnsResolver = null;
-        try {
-            dnsResolver = Utils.getFirstActiveNetworkDnsResolver(context);
-        } catch (Utils.PsibotError e) {
-            Log.addEntry("failed to get active network DNS resolver: " + e.getMessage());
-            // Proceed with default value in config file
-        }
-
-        // Load settings from the raw resource JSON config file and
-        // update as necessary. Then write JSON to disk for the Go client.
-        String configFileContents = Utils.readInputStreamToString(
-                context.getResources().openRawResource(R.raw.psiphon_config));
-        JSONObject json = new JSONObject(configFileContents);
-
-        if (dnsResolver != null) {
-            json.put("BindToDeviceDnsServer", dnsResolver);
-        }
-
-        // 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", mVpnService.getFilesDir());
-        json.put("DataStoreTempDirectory", mVpnService.getCacheDir());
-
-        // User-specified settings.
-        // Note: currently, validation is not comprehensive, and related errors are
-        // not directly parsed.
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
-        json.put("EgressRegion",
-                preferences.getString(
-                        context.getString(R.string.preferenceEgressRegion),
-                        context.getString(R.string.preferenceEgressRegionDefaultValue)));
-        json.put("TunnelProtocol",
-                preferences.getString(
-                        context.getString(R.string.preferenceTunnelProtocol),
-                        context.getString(R.string.preferenceTunnelProtocolDefaultValue)));
-        json.put("UpstreamHttpProxyAddress",
-                preferences.getString(
-                        context.getString(R.string.preferenceUpstreamHttpProxyAddress),
-                        context.getString(R.string.preferenceUpstreamHttpProxyAddressDefaultValue)));
-        json.put("LocalHttpProxyPort",
-                Integer.parseInt(
-                        preferences.getString(
-                                context.getString(R.string.preferenceLocalHttpProxyPort),
-                                context.getString(R.string.preferenceLocalHttpProxyPortDefaultValue))));
-        json.put("LocalSocksProxyPort",
-                Integer.parseInt(
-                        preferences.getString(
-                                context.getString(R.string.preferenceLocalSocksProxyPort),
-                                context.getString(R.string.preferenceLocalSocksProxyPortDefaultValue))));
-        json.put("ConnectionWorkerPoolSize",
-                Integer.parseInt(
-                        preferences.getString(
-                                context.getString(R.string.preferenceConnectionWorkerPoolSize),
-                                context.getString(R.string.preferenceConnectionWorkerPoolSizeDefaultValue))));
-        json.put("TunnelPoolSize",
-                Integer.parseInt(
-                        preferences.getString(
-                                context.getString(R.string.preferenceTunnelPoolSize),
-                                context.getString(R.string.preferenceTunnelPoolSizeDefaultValue))));
-        json.put("PortForwardFailureThreshold",
-                Integer.parseInt(
-                        preferences.getString(
-                                context.getString(R.string.preferencePortForwardFailureThreshold),
-                                context.getString(R.string.preferencePortForwardFailureThresholdDefaultValue))));
-
-        return json.toString();
-    }
-
-    private synchronized void handleNotice(String noticeJSON) {
-        try {
-            JSONObject notice = new JSONObject(noticeJSON);
-            String noticeType = notice.getString("noticeType");
-            if (noticeType.equals("Tunnels")) {
-                int count = notice.getJSONObject("data").getInt("count");
-                if (count == 1) {
-                    mTunnelStartedSignal.countDown();
-                }
-            } else if (noticeType.equals("ListeningSocksProxyPort")) {
-                mLocalSocksProxyPort = notice.getJSONObject("data").getInt("port");
-            } else if (noticeType.equals("ListeningHttpProxyPort")) {
-                mLocalHttpProxyPort = notice.getJSONObject("data").getInt("port");
-            } else if (noticeType.equals("Homepage")) {
-                mHomePages.add(notice.getJSONObject("data").getString("url"));
-            }
-            String displayNotice = noticeType + " " + notice.getJSONObject("data").toString();
-            android.util.Log.d("PSIPHON", displayNotice);
-            Log.addEntry(displayNotice);
-        } catch (JSONException e) {
-            // Ignore notice
-        }
-    }
-}

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2014, Psiphon Inc.
+ * Copyright (c) 2015, Psiphon Inc.
  * All rights reserved.
  * All rights reserved.
  *
  *
  * This program is free software: you can redistribute it and/or modify
  * This program is free software: you can redistribute it and/or modify
@@ -24,33 +24,32 @@ import android.app.PendingIntent;
 import android.content.Intent;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences;
 import android.net.VpnService;
 import android.net.VpnService;
-import android.os.ParcelFileDescriptor;
 import android.preference.PreferenceManager;
 import android.preference.PreferenceManager;
 
 
-import java.util.Locale;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
+import org.json.JSONException;
+import org.json.JSONObject;
 
 
-public class Service extends VpnService
-        implements SharedPreferences.OnSharedPreferenceChangeListener {
+import java.io.InputStream;
 
 
-    // Note: assumes only one instance of Service
-    private static AtomicBoolean mIsRunning = new AtomicBoolean();
+import ca.psiphon.PsiphonVpn;
 
 
-    public static boolean isRunning() {
-        return mIsRunning.get();
-    }
+public class Service extends VpnService
+        implements PsiphonVpn.HostService, SharedPreferences.OnSharedPreferenceChangeListener {
 
 
-    private Thread mThread;
-    private CountDownLatch mInterruptSignal;
-    private AtomicBoolean mStopFlag;
+    private PsiphonVpn mPsiphonVpn;
 
 
     @Override
     @Override
     public void onCreate() {
     public void onCreate() {
-        mIsRunning.set(true);
+        mPsiphonVpn = PsiphonVpn.newPsiphonVpn(this);
         startForeground(R.string.foregroundServiceNotificationId, makeForegroundNotification());
         startForeground(R.string.foregroundServiceNotificationId, makeForegroundNotification());
-        startWorking();
+        try {
+            mPsiphonVpn.startRouting();
+            mPsiphonVpn.startTunneling();
+        } catch (PsiphonVpn.Exception e) {
+            Log.addEntry("failed to start Psiphon VPN: " + e.getMessage());
+            mPsiphonVpn.stop();
+            stopSelf();
+        }
         PreferenceManager.getDefaultSharedPreferences(this).
         PreferenceManager.getDefaultSharedPreferences(this).
                 registerOnSharedPreferenceChangeListener(this);
                 registerOnSharedPreferenceChangeListener(this);
     }
     }
@@ -59,179 +58,100 @@ public class Service extends VpnService
     public void onDestroy() {
     public void onDestroy() {
         PreferenceManager.getDefaultSharedPreferences(this).
         PreferenceManager.getDefaultSharedPreferences(this).
                 unregisterOnSharedPreferenceChangeListener(this);
                 unregisterOnSharedPreferenceChangeListener(this);
-        stopWorking();
+        mPsiphonVpn.stop();
         stopForeground(true);
         stopForeground(true);
-        mIsRunning.set(false);
     }
     }
 
 
     @Override
     @Override
     public synchronized void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
     public synchronized void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
-        if (!mIsRunning.get()) {
-            return;
+        try {
+            mPsiphonVpn.restartPsiphon();
+        } catch (PsiphonVpn.Exception e) {
+            Log.addEntry("failed to restart Psiphon: " + e.getMessage());
+            mPsiphonVpn.stop();
+            stopSelf();
         }
         }
-        // An interrupt without setting stop will restart Psiphon, using the newest preferences.
-        interruptWorking();
     }
     }
 
 
-    private void startWorking() {
-        stopWorking();
-        mInterruptSignal = new CountDownLatch(1);
-        mStopFlag = new AtomicBoolean(false);
-        mThread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                CountDownLatch tunnelStartedSignal = new CountDownLatch(1);
-                Psiphon psiphon = new Psiphon(Service.this, tunnelStartedSignal);
-                // An interrupt with mStopFlag=false will cause Psiphon to restart without stopping
-                // the service thread or VPN; this is to apply new preferences to the Psiphon config.
-                // (The VPN is restarted if the preferred SOCKS proxy port changes.)
-                int localSocksProxyPort = -1;
-                while (!mStopFlag.get()) {
-                    try {
-                        // TODO: monitor tunnel messages and update notification UI when re-connecting, etc.
-                        psiphon.start();
-                        while (true) {
-                            if (tunnelStartedSignal.await(100, TimeUnit.MILLISECONDS)) {
-                                break;
-                            }
-                            if (mInterruptSignal.await(0, TimeUnit.MILLISECONDS)) {
-                                throw new Utils.PsibotError("interrupted while waiting tunnel");
-                            }
-                        }
-                        // [Re]start the VPN when the local SOCKS proxy port changes. Leave the
-                        // VPN up when other preferences change; only Psiphon restarts in this case.
-                        if (psiphon.getLocalSocksProxyPort() != localSocksProxyPort) {
-                            if (localSocksProxyPort != -1) {
-                                stopVpn();
-                            }
-                            localSocksProxyPort = psiphon.getLocalSocksProxyPort();
-                            runVpn(localSocksProxyPort);
-                        }
-                        mInterruptSignal.await();
-                    } catch (Utils.PsibotError e) {
-                        Log.addEntry("Service failed: " + e.getMessage());
-                    } catch (InterruptedException e) {
-                        Thread.currentThread().interrupt();
-                    }
-                }
-                stopVpn();
-                psiphon.stop();
-                stopSelf();
-            }
-        });
-        mThread.start();
+    @Override
+    public String getAppName() {
+        return getString(R.string.app_name);
     }
     }
 
 
-    private void interruptWorking() {
-        if (mInterruptSignal == null) {
-            return;
-        }
-        CountDownLatch currentSignal = mInterruptSignal;
-        // This is the new interrupt signal for when work resumes
-        mInterruptSignal = new CountDownLatch(1);
-        // Interrupt work
-        currentSignal.countDown();
+    @Override
+    public VpnService getVpnService() {
+        return this;
     }
     }
 
 
-    private void stopWorking() {
-        if (mStopFlag != null) {
-            mStopFlag.set(true);
-        }
-        interruptWorking();
-        if (mThread != null) {
-            try {
-                mThread.join();
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
-        }
-        mInterruptSignal = null;
-        mStopFlag = null;
-        mThread = null;
+    @Override
+    public VpnService.Builder newVpnServiceBuilder() {
+        return new VpnService.Builder();
     }
     }
 
 
-    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 void runVpn(int localSocksProxyPort) throws Utils.PsibotError {
-        Log.addEntry("network type: " + Utils.getNetworkTypeName(this));
-
-        String privateIpAddress = Utils.selectPrivateAddress();
-        if (privateIpAddress == null) {
-            throw new Utils.PsibotError("no private address available");
-        }
-
-        ParcelFileDescriptor vpnInterfaceFileDescriptor = establishVpn(privateIpAddress);
-        Log.addEntry("VPN established");
-
-        String socksServerAddress = "127.0.0.1:" + Integer.toString(localSocksProxyPort);
-        String udpgwServerAddress = "127.0.0.1:" + Integer.toString(UDPGW_SERVER_PORT);
-        Tun2Socks.start(
-                this,
-                vpnInterfaceFileDescriptor,
-                VPN_INTERFACE_MTU,
-                Utils.getPrivateAddressRouter(privateIpAddress),
-                VPN_INTERFACE_NETMASK,
-                socksServerAddress,
-                udpgwServerAddress,
-                true);
-        Log.addEntry("tun2socks started");
-
-        // Note: should now 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
+    @Override
+    public InputStream getPsiphonConfigResource() {
+        return getResources().openRawResource(R.raw.psiphon_config);
     }
     }
 
 
-    private ParcelFileDescriptor establishVpn(String privateIpAddress)
-        throws Utils.PsibotError {
-
-        Locale previousLocale = Locale.getDefault();
-        ParcelFileDescriptor vpnInterfaceFileDescriptor = null;
-
-        final String errorMessage = "establishVpn failed";
+    @Override
+    public void customizeConfigParameters(JSONObject config) {
+        // User-specified settings.
+        // Note: currently, validation is not comprehensive, and related errors are
+        // not directly parsed.
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
         try {
         try {
-            String subnet = Utils.getPrivateAddressSubnet(privateIpAddress);
-            int prefixLength = Utils.getPrivateAddressPrefixLength(privateIpAddress);
-            String router = Utils.getPrivateAddressRouter(privateIpAddress);
-
-            // Set the locale to English (or probably any other language that
-            // uses Hindu-Arabic (aka Latin) numerals).
-            // We have found that VpnService.Builder does something locale-dependent
-            // internally that causes errors when the locale uses its own numerals
-            // (i.e., Farsi and Arabic).
-            Locale.setDefault(new Locale("en"));
-
-            vpnInterfaceFileDescriptor = new VpnService.Builder()
-                    .setSession(getString(R.string.app_name))
-                    .setMtu(VPN_INTERFACE_MTU)
-                    .addAddress(privateIpAddress, prefixLength)
-                    .addRoute("0.0.0.0", 0)
-                    .addRoute(subnet, prefixLength)
-                    .addDnsServer(router)
-                    .establish();
-
-            if (vpnInterfaceFileDescriptor == null) {
-                // as per http://developer.android.com/reference/android/net/VpnService.Builder.html#establish%28%29
-                throw new Utils.PsibotError(errorMessage + ": application is not prepared or is revoked");
-            }
-        } catch(IllegalArgumentException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch(IllegalStateException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch(SecurityException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } finally {
-            // Restore the original locale.
-            Locale.setDefault(previousLocale);
+            config.put("EgressRegion",
+                    preferences.getString(
+                            getString(R.string.preferenceEgressRegion),
+                            getString(R.string.preferenceEgressRegionDefaultValue)));
+            config.put("TunnelProtocol",
+                    preferences.getString(
+                            getString(R.string.preferenceTunnelProtocol),
+                            getString(R.string.preferenceTunnelProtocolDefaultValue)));
+            config.put("UpstreamHttpProxyAddress",
+                    preferences.getString(
+                            getString(R.string.preferenceUpstreamHttpProxyAddress),
+                            getString(R.string.preferenceUpstreamHttpProxyAddressDefaultValue)));
+            config.put("LocalHttpProxyPort",
+                    Integer.parseInt(
+                            preferences.getString(
+                                    getString(R.string.preferenceLocalHttpProxyPort),
+                                    getString(R.string.preferenceLocalHttpProxyPortDefaultValue))));
+            config.put("LocalSocksProxyPort",
+                    Integer.parseInt(
+                            preferences.getString(
+                                    getString(R.string.preferenceLocalSocksProxyPort),
+                                    getString(R.string.preferenceLocalSocksProxyPortDefaultValue))));
+            config.put("ConnectionWorkerPoolSize",
+                    Integer.parseInt(
+                            preferences.getString(
+                                    getString(R.string.preferenceConnectionWorkerPoolSize),
+                                    getString(R.string.preferenceConnectionWorkerPoolSizeDefaultValue))));
+            config.put("TunnelPoolSize",
+                    Integer.parseInt(
+                            preferences.getString(
+                                    getString(R.string.preferenceTunnelPoolSize),
+                                    getString(R.string.preferenceTunnelPoolSizeDefaultValue))));
+            config.put("PortForwardFailureThreshold",
+                    Integer.parseInt(
+                            preferences.getString(
+                                    getString(R.string.preferencePortForwardFailureThreshold),
+                                    getString(R.string.preferencePortForwardFailureThresholdDefaultValue))));
+        } catch (JSONException e) {
+            Log.addEntry("error setting config parameters: " + e.getMessage());
         }
         }
+    }
 
 
-        return vpnInterfaceFileDescriptor;
+    @Override
+    public void logWarning(String message) {
+        android.util.Log.w(getString(R.string.app_name), message);
+        Log.addEntry(message);
     }
     }
 
 
-    private void stopVpn() {
-        // Tun2socks closes the VPN file descriptor, which closes the VpnService session
-        Tun2Socks.stop();
-        Log.addEntry("VPN stopped");
+    @Override
+    public void logInfo(String message) {
+        android.util.Log.i(getString(R.string.app_name), message);
+        Log.addEntry(message);
     }
     }
 
 
     private Notification makeForegroundNotification() {
     private Notification makeForegroundNotification() {

+ 0 - 101
AndroidApp/app/src/main/java/ca/psiphon/psibot/Tun2Socks.java

@@ -1,101 +0,0 @@
-/*
- * Copyright (c) 2014, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * 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 <http://www.gnu.org/licenses/>.
- *
- */
-
-package ca.psiphon.psibot;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.ParcelFileDescriptor;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public class Tun2Socks {
-
-    // Note: can't run more than one tun2socks instance due to the use of
-    // global state (the lwip module, etc.) in the native code.
-
-    // Note: assumes only one instance of Tun2Socks
-    private static Thread mThread;
-    private static AtomicBoolean mIsRunning = new AtomicBoolean();
-
-    public static synchronized void start(
-            final Context context,
-            final ParcelFileDescriptor vpnInterfaceFileDescriptor,
-            final int vpnInterfaceMTU,
-            final String vpnIpAddress,
-            final String vpnNetMask,
-            final String socksServerAddress,
-            final String udpgwServerAddress,
-            final boolean udpgwTransparentDNS) {
-        stop();
-        mThread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                mIsRunning.set(true);
-                runTun2Socks(
-                        vpnInterfaceFileDescriptor.detachFd(),
-                        vpnInterfaceMTU,
-                        vpnIpAddress,
-                        vpnNetMask,
-                        socksServerAddress,
-                        udpgwServerAddress,
-                        udpgwTransparentDNS ? 1 : 0);
-            	
-                if (!mIsRunning.get()) {
-                    Log.addEntry("Tun2Socks: unexpected termination");
-                    context.stopService(new Intent(context, Service.class));
-                }
-            }
-        });
-        mThread.start();
-    }
-    
-    public static synchronized void stop() {
-        if (mThread != null) {
-            terminateTun2Socks();
-            try {
-                mThread.join();
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
-            mThread = null;
-            mIsRunning.set(false);
-        }
-    }
-        
-    public static void logTun2Socks(String level, String channel, String msg) {
-        String logMsg = "Tun2Socks: " + level + "(" + channel + "): " + msg;
-        Log.addEntry(logMsg);
-    }
-
-    private native static int runTun2Socks(
-            int vpnInterfaceFileDescriptor,
-            int vpnInterfaceMTU,
-            String vpnIpAddress,
-            String vpnNetMask,
-            String socksServerAddress,
-            String udpgwServerAddress,
-            int udpgwTransparentDNS);
-
-    private native static void terminateTun2Socks();
-    
-    static {
-        System.loadLibrary("tun2socks");
-    }
-}

+ 0 - 334
AndroidApp/app/src/main/java/ca/psiphon/psibot/Utils.java

@@ -1,334 +0,0 @@
-/*
- * Copyright (c) 2014, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * 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 <http://www.gnu.org/licenses/>.
- *
- */
-
-package ca.psiphon.psibot;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.LinkProperties;
-import android.net.NetworkInfo;
-import android.os.Build;
-
-import org.apache.http.conn.util.InetAddressUtils;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-
-public class Utils {
-
-    public static class PsibotError extends Exception {
-        private static final long serialVersionUID = 1L;
-
-        public PsibotError(String message) {
-            super(message);
-        }
-
-        public PsibotError(String message, Throwable cause) {
-            super(message + ": " + cause.getMessage());
-        }
-
-    }
-
-    public static void writeRawResourceFile(
-            Context context, int resId, File file, boolean setExecutable) throws IOException {
-        file.delete();
-        Utils.copyStream(
-                context.getResources().openRawResource(resId),
-                new FileOutputStream(file));
-        if (setExecutable && !file.setExecutable(true)) {
-            throw new IOException("failed to set file as executable");
-        }
-    }
-
-    public static void copyStream(
-            InputStream inputStream, OutputStream outputStream) throws IOException {
-        try {
-            byte[] buffer = new byte[16384];
-            int length;
-            while ((length = inputStream.read(buffer)) != -1) {
-                outputStream.write(buffer, 0 , length);
-            }
-        } finally {
-            inputStream.close();
-            outputStream.close();
-        }
-    }
-
-    public static String readInputStreamToString(InputStream inputStream) throws IOException {
-        return new String(readInputStreamToBytes(inputStream), "UTF-8");
-    }
-
-    public static byte[] readInputStreamToBytes(InputStream inputStream) throws IOException {
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        int readCount;
-        byte[] buffer = new byte[16384];
-        while ((readCount = inputStream.read(buffer, 0, buffer.length)) != -1) {
-            outputStream.write(buffer, 0, readCount);
-        }
-        outputStream.flush();
-        return outputStream.toByteArray();
-    }
-
-    public static boolean hasNetworkConnectivity(Context context) {
-        ConnectivityManager connectivityManager =
-                (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
-        return networkInfo != null && networkInfo.isConnected();
-    }
-
-    public static String getNetworkTypeName(Context context) {
-        ConnectivityManager connectivityManager =
-                (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
-        return networkInfo == null ? "" : networkInfo.getTypeName();
-    }
-
-    private static final String CANDIDATE_10_SLASH_8 = "10.0.0.1";
-    private static final String SUBNET_10_SLASH_8 = "10.0.0.0";
-    private static final int PREFIX_LENGTH_10_SLASH_8 = 8;
-    private static final String ROUTER_10_SLASH_8 = "10.0.0.2";
-
-    private static final String CANDIDATE_172_16_SLASH_12 = "172.16.0.1";
-    private static final String SUBNET_172_16_SLASH_12 = "172.16.0.0";
-    private static final int PREFIX_LENGTH_172_16_SLASH_12 = 12;
-    private static final String ROUTER_172_16_SLASH_12 = "172.16.0.2";
-
-    private static final String CANDIDATE_192_168_SLASH_16 = "192.168.0.1";        
-    private static final String SUBNET_192_168_SLASH_16 = "192.168.0.0";
-    private static final int PREFIX_LENGTH_192_168_SLASH_16 = 16;
-    private static final String ROUTER_192_168_SLASH_16 = "192.168.0.2";
-    
-    private static final String CANDIDATE_169_254_1_SLASH_24 = "169.254.1.1";        
-    private static final String SUBNET_169_254_1_SLASH_24 = "169.254.1.0";
-    private static final int PREFIX_LENGTH_169_254_1_SLASH_24 = 24;
-    private static final String ROUTER_169_254_1_SLASH_24 = "169.254.1.2";
-    
-    public static String selectPrivateAddress() {
-        // 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.
-
-        ArrayList<String> candidates = new ArrayList<String>();
-        candidates.add(CANDIDATE_10_SLASH_8);
-        candidates.add(CANDIDATE_172_16_SLASH_12);
-        candidates.add(CANDIDATE_192_168_SLASH_16);
-        candidates.add(CANDIDATE_169_254_1_SLASH_24);
-        
-        List<NetworkInterface> netInterfaces;
-        try {
-            netInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
-        } catch (SocketException e) {
-            return null;
-        }
-
-        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(CANDIDATE_10_SLASH_8);
-                    }
-                    else if (
-                        ipAddress.length() >= 6 &&
-                        ipAddress.substring(0, 6).compareTo("172.16") >= 0 && 
-                        ipAddress.substring(0, 6).compareTo("172.31") <= 0) {
-                        candidates.remove(CANDIDATE_172_16_SLASH_12);
-                    }
-                    else if (ipAddress.startsWith("192.168")) {
-                        candidates.remove(CANDIDATE_192_168_SLASH_16);
-                    }
-                }
-            }
-        }
-        
-        if (candidates.size() > 0) {
-            return candidates.get(0);
-        }
-        
-        return null;
-    }
-    
-    public static String getPrivateAddressSubnet(String privateIpAddress) {
-        if (0 == privateIpAddress.compareTo(CANDIDATE_10_SLASH_8)) {
-            return SUBNET_10_SLASH_8;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_172_16_SLASH_12)) {
-            return SUBNET_172_16_SLASH_12;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_192_168_SLASH_16)) {
-            return SUBNET_192_168_SLASH_16;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_169_254_1_SLASH_24)) {
-            return SUBNET_169_254_1_SLASH_24;
-        }
-        return null;
-    }
-    
-    public static int getPrivateAddressPrefixLength(String privateIpAddress) {
-        if (0 == privateIpAddress.compareTo(CANDIDATE_10_SLASH_8)) {
-            return PREFIX_LENGTH_10_SLASH_8;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_172_16_SLASH_12)) {
-            return PREFIX_LENGTH_172_16_SLASH_12;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_192_168_SLASH_16)) {
-            return PREFIX_LENGTH_192_168_SLASH_16;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_169_254_1_SLASH_24)) {
-            return PREFIX_LENGTH_169_254_1_SLASH_24;
-        }
-        return 0;
-    }
-    
-    public static String getPrivateAddressRouter(String privateIpAddress) {
-        if (0 == privateIpAddress.compareTo(CANDIDATE_10_SLASH_8)) {
-            return ROUTER_10_SLASH_8;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_172_16_SLASH_12)) {
-            return ROUTER_172_16_SLASH_12;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_192_168_SLASH_16)) {
-            return ROUTER_192_168_SLASH_16;
-        }
-        else if (0 == privateIpAddress.compareTo(CANDIDATE_169_254_1_SLASH_24)) {
-            return ROUTER_169_254_1_SLASH_24;
-        }
-        return null;
-    }
-
-    public static String getFirstActiveNetworkDnsResolver(Context context)
-            throws Utils.PsibotError {
-        Collection<InetAddress> dnsResolvers = Utils.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 Utils.PsibotError("no active network DNS resolver");
-    }
-
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    public static Collection<InetAddress> getActiveNetworkDnsResolvers(Context context)
-            throws Utils.PsibotError {
-        final String errorMessage = "getActiveNetworkDnsResolvers failed";
-        ArrayList<InetAddress> dnsAddresses = new ArrayList<InetAddress>();
-        try {
-            /*
-
-            Hidden API
-            - only available in Android 4.0+
-            - no guarantee will be available beyond 4.2, or on all vendor devices
-
-            core/java/android/net/ConnectivityManager.java:
-
-                /** {@hide} * /
-                public LinkProperties getActiveLinkProperties() {
-                    try {
-                        return mService.getActiveLinkProperties();
-                    } catch (RemoteException e) {
-                        return null;
-                    }
-                }
-
-            services/java/com/android/server/ConnectivityService.java:
-
-                /*
-                 * Return LinkProperties for the active (i.e., connected) default
-                 * network interface.  It is assumed that at most one default network
-                 * is active at a time. If more than one is active, it is indeterminate
-                 * which will be returned.
-                 * @return the ip properties for the active network, or {@code null} if
-                 * none is active
-                 * /
-                @Override
-                public LinkProperties getActiveLinkProperties() {
-                    return getLinkProperties(mActiveDefaultNetwork);
-                }
-
-                @Override
-                public LinkProperties getLinkProperties(int networkType) {
-                    enforceAccessPermission();
-                    if (isNetworkTypeValid(networkType)) {
-                        final NetworkStateTracker tracker = mNetTrackers[networkType];
-                        if (tracker != null) {
-                            return tracker.getLinkProperties();
-                        }
-                    }
-                    return null;
-                }
-
-            core/java/android/net/LinkProperties.java:
-
-                public Collection<InetAddress> getDnses() {
-                    return Collections.unmodifiableCollection(mDnses);
-                }
-
-            */
-
-            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 (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 Utils.PsibotError(errorMessage, e);
-        } catch (NoSuchMethodException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch (IllegalArgumentException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch (IllegalAccessException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch (InvocationTargetException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        } catch (NullPointerException e) {
-            throw new Utils.PsibotError(errorMessage, e);
-        }
-
-        return dnsAddresses;
-    }
-}

+ 14 - 2
AndroidApp/app/src/main/java/go/Seq.java

@@ -1,6 +1,5 @@
 package go;
 package go;
 
 
-import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.SparseIntArray;
 
 
@@ -208,7 +207,20 @@ public class Seq {
 		synchronized void dec(int refnum) {
 		synchronized void dec(int refnum) {
 			if (refnum > 0) {
 			if (refnum > 0) {
 				// Java objects are removed on request of Go.
 				// Java objects are removed on request of Go.
-				javaObjs.remove(refnum);
+
+				// TEMP FIX -- this can remove references for valid objects
+                // The problem appears to be that references are added when
+                // a Java object is created (only once) but removed after the
+                // object is passed to Go and then no longer referenced by Go
+                // (via runtime.SetFinalizer: https://github.com/golang/mobile/blob/b2e453e1cda693a5a97d5c97cc9f3016a64b7dfa/bind/seq/buffer.go#L110)
+                // But if the same Java object is passed to Go again, then it
+                // will no longer be in javaObjs (after Go GCs the Ref)!
+                // This temp fix keeps eternal references to Java objects,
+                // which is ok for us since we only have one. Another workaround
+                // would be to create temporary proxy objects, once per call into Go.
+
+				// javaObjs.remove(refnum);
+
 				return;
 				return;
 			}
 			}
 			int count = goObjs.get(refnum);
 			int count = goObjs.get(refnum);

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


+ 0 - 8
AndroidApp/app/src/main/res/menu/main.xml

@@ -1,16 +1,8 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:tools="http://schemas.android.com/tools"
     tools:context=".MainActivity" >
     tools:context=".MainActivity" >
-    <item android:id="@+id/action_email_log"
-        android:title="@string/action_email_log"
-        android:orderInCategory="100"
-        android:showAsAction="never" />
     <item android:id="@+id/action_settings"
     <item android:id="@+id/action_settings"
         android:title="@string/action_settings"
         android:title="@string/action_settings"
         android:orderInCategory="101"
         android:orderInCategory="101"
         android:showAsAction="never" />
         android:showAsAction="never" />
-    <item android:id="@+id/action_quit"
-        android:title="@string/action_quit"
-        android:orderInCategory="102"
-        android:showAsAction="never" />
 </menu>
 </menu>

+ 14 - 2
AndroidLibrary/java_golang/go/Seq.java

@@ -1,6 +1,5 @@
 package go;
 package go;
 
 
-import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.SparseIntArray;
 
 
@@ -208,7 +207,20 @@ public class Seq {
 		synchronized void dec(int refnum) {
 		synchronized void dec(int refnum) {
 			if (refnum > 0) {
 			if (refnum > 0) {
 				// Java objects are removed on request of Go.
 				// Java objects are removed on request of Go.
-				javaObjs.remove(refnum);
+
+				// TEMP FIX -- this can remove references for valid objects
+                // The problem appears to be that references are added when
+                // a Java object is created (only once) but removed after the
+                // object is passed to Go and then no longer referenced by Go
+                // (via runtime.SetFinalizer: https://github.com/golang/mobile/blob/b2e453e1cda693a5a97d5c97cc9f3016a64b7dfa/bind/seq/buffer.go#L110)
+                // But if the same Java object is passed to Go again, then it
+                // will no longer be in javaObjs (after Go GCs the Ref)!
+                // This temp fix keeps eternal references to Java objects,
+                // which is ok for us since we only have one. Another workaround
+                // would be to create temporary proxy objects, once per call into Go.
+
+				// javaObjs.remove(refnum);
+
 				return;
 				return;
 			}
 			}
 			int count = goObjs.get(refnum);
 			int count = goObjs.get(refnum);