From 75426bb9e25d11fa84636481aac59094eec41b26 Mon Sep 17 00:00:00 2001 From: Nathan Freitas Date: Fri, 3 Apr 2015 12:27:59 -0400 Subject: [PATCH] Improve VPN service support - fix network switching handling We now refresh the VPN and tun2socks interfaces when the network type switches, and we do so in a way that does not cause traffic to leak. The new interface is established before we close the old one. --- res/values/arrays.xml | 13 +- res/values/strings.xml | 13 +- .../torproject/android/OrbotConstants.java | 2 +- .../torproject/android/OrbotMainActivity.java | 180 ++++++++++++++---- .../android/service/TorService.java | 134 +++++++++---- .../android/vpn/OrbotVpnService.java | 157 +++++++++------ 6 files changed, 359 insertions(+), 140 deletions(-) diff --git a/res/values/arrays.xml b/res/values/arrays.xml index fb0a7c31..00eb3a43 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -36,9 +36,16 @@ ru - - - + + + + Obfs4 (Recommended) + Obfs3 + ScrambleSuit + Tunnel through Azure + Tunnel through Amazon + Tunnel through Google + diff --git a/res/values/strings.xml b/res/values/strings.xml index 92e1e35b..e76ce289 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -315,15 +315,16 @@ Bridges Updated - "Restart Orbot to use these bridges: " + Please restart Orbot to enable the changes QR Codes - If your mobile network actively blocks Tor, you can use a Tor Bridge to access the network.\n\nYou can get a bridge address from https://bridges.torproject.org or scan a bridge QR code from a friend.\n\nAnother way to get bridges is to send an email to bridges@torproject.org. Please note that you must send the email using an address from one of the following email providers: Riseup, Gmail or Yahoo. + If your mobile network actively blocks Tor, you can use a Tor Bridge to access the network.\n\nYou can get a bridge address from https://bridges.torproject.org, by emailing bridges@torproject.org, or by scanning a bridge QR code. Bridge Mode - Get Bridges + Email + Web Activate @@ -332,4 +333,10 @@ You can enable all apps on your device to run through the Tor network using the VPN feature of Android.\n\n*WARNING* This is a new, experimental feature and in some cases may not start automatically, or may stop. It should NOT be used for anonymity, and ONLY used for getting through firewalls and filters. Send Email + + You must can a bridge address by email, web or by scanning a bridge QR code. Once you have this address, please paste it into the \"Bridges\" preference in Orbot\'s setting and restart. + + Install Orweb + + Standard Browser diff --git a/src/org/torproject/android/OrbotConstants.java b/src/org/torproject/android/OrbotConstants.java index 745ace44..fa67d928 100644 --- a/src/org/torproject/android/OrbotConstants.java +++ b/src/org/torproject/android/OrbotConstants.java @@ -16,7 +16,7 @@ public interface OrbotConstants { //path to check Tor against public final static String URL_TOR_CHECK = "https://check.torproject.org"; - public final static String URL_TOR_BRIDGES = "https://bridges.torproject.org"; + public final static String URL_TOR_BRIDGES = "https://bridges.torproject.org/bridges?transport="; public final static String NEWLINE = "\n"; diff --git a/src/org/torproject/android/OrbotMainActivity.java b/src/org/torproject/android/OrbotMainActivity.java index be05d693..2e22fb25 100644 --- a/src/org/torproject/android/OrbotMainActivity.java +++ b/src/org/torproject/android/OrbotMainActivity.java @@ -661,6 +661,8 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon setResult(RESULT_OK); mBtnBridges.setChecked(true); + + enableBridges(true); } private boolean showWizard = true; @@ -707,7 +709,7 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon .setIcon(R.drawable.onion32) .setTitle(R.string.install_apps_) .setMessage(R.string.it_doesn_t_seem_like_you_have_orweb_installed_want_help_with_that_or_should_we_just_open_the_browser_) - .setPositiveButton(android.R.string.ok, new Dialog.OnClickListener () + .setPositiveButton(R.string.install_orweb, new Dialog.OnClickListener () { @Override @@ -720,7 +722,7 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon } }) - .setNegativeButton(android.R.string.no, new Dialog.OnClickListener () + .setNegativeButton(R.string.standard_browser, new Dialog.OnClickListener () { @Override @@ -860,41 +862,56 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon new AlertDialog.Builder(this) .setTitle(R.string.bridge_mode) .setView(view) - .setNeutralButton(R.string.get_bridges, new Dialog.OnClickListener () - { - - @Override - public void onClick(DialogInterface dialog, int which) { - - //openBrowser(URL_TOR_BRIDGES); - - sendGetBridgeEmail(); - } - - - }) - .setPositiveButton(R.string.activate, new Dialog.OnClickListener () - { - - @Override - public void onClick(DialogInterface dialog, int which) { - - enableBridges (true); - - } - - - }) - .setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() + .setItems(R.array.bridge_options, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // The 'which' argument contains the index position + // of the selected item + + switch (which) + { + case 0: //obfs 4; + showGetBridgePrompt("obfs4"); + + break; + case 1: //obfs3 + showGetBridgePrompt("obfs3"); + + break; + case 2: //scramblesuit + showGetBridgePrompt("scramblesuit"); + + break; + case 3: //azure + mPrefs.edit().putString(OrbotConstants.PREF_BRIDGES_LIST,"2").commit(); + enableBridges(true); + + break; + case 4: //amazon + mPrefs.edit().putString(OrbotConstants.PREF_BRIDGES_LIST,"1").commit(); + enableBridges(true); + + break; + case 5: //google + mPrefs.edit().putString(OrbotConstants.PREF_BRIDGES_LIST,"0").commit(); + enableBridges(true); + + break; + + } + + } + }).setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - mBtnBridges.setChecked(false); + //mBtnBridges.setChecked(false); } }) .show(); + + } else { @@ -903,13 +920,72 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon } - private void sendGetBridgeEmail () + private void showGetBridgePrompt (final String type) + { + LayoutInflater li = LayoutInflater.from(this); + View view = li.inflate(R.layout.layout_diag, null); + + TextView versionName = (TextView)view.findViewById(R.id.diaglog); + versionName.setText(R.string.you_must_get_a_bridge_address_by_email_web_or_from_a_friend_once_you_have_this_address_please_paste_it_into_the_bridges_preference_in_orbot_s_setting_and_restart_); + + new AlertDialog.Builder(this) + .setTitle(R.string.bridge_mode) + .setView(view) + .setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) { + + //mBtnBridges.setChecked(false); + + } + }) + .setNeutralButton(R.string.get_bridges_email, new Dialog.OnClickListener () + { + + @Override + public void onClick(DialogInterface dialog, int which) { + + + sendGetBridgeEmail(type); + + } + + + }) + .setPositiveButton(R.string.get_bridges_web, new Dialog.OnClickListener () + { + + @Override + public void onClick(DialogInterface dialog, int which) { + + openBrowser(URL_TOR_BRIDGES + type); + + } + + + }).show(); + } + + private void sendGetBridgeEmail (String type) { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("message/rfc822"); intent.putExtra(Intent.EXTRA_EMAIL , new String[]{"bridges@torproject.org"}); - intent.putExtra(Intent.EXTRA_SUBJECT, "Tor Bridge Request"); - + + if (type != null) + { + intent.putExtra(Intent.EXTRA_SUBJECT, "get transport " + type); + intent.putExtra(Intent.EXTRA_TEXT, "get transport " + type); + + } + else + { + intent.putExtra(Intent.EXTRA_SUBJECT, "get bridges"); + intent.putExtra(Intent.EXTRA_TEXT, "get bridges"); + + } + startActivity(Intent.createChooser(intent, getString(R.string.send_email))); } @@ -917,10 +993,45 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon { Editor edit = mPrefs.edit(); - edit.putBoolean("pref_bridges_enabled", enable); + edit.putBoolean(OrbotConstants.PREF_BRIDGES_ENABLED, enable); edit.commit(); updateSettings(); + + if (torStatus == TorServiceConstants.STATUS_ON) + { + String bridgeList = mPrefs.getString(OrbotConstants.PREF_BRIDGES_LIST,null); + if (bridgeList != null && bridgeList.length() > 0) + { + try + { + //do auto restart + stopTor (); + + mHandler.postDelayed(new Runnable () { + + public void run () + { + try + { + startTor(); + } + catch (Exception e) + { + Log.e(TAG,"can't start orbot",e); + } + } + }, 2000); + } + catch (Exception e) + { + Log.e(TAG,"can't stop orbot",e); + } + } + + } + + } public void promptStartVpnService () @@ -1160,7 +1271,7 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon //here we update the UI which is a bit sloppy and mixed up code wise //might be best to just call updateStatus() instead of directly manipulating UI in this method - yep makes sense imgStatus.setImageResource(R.drawable.torstarting); - // lblStatus.setText(getString(R.string.status_starting_up)); + lblStatus.setText(getString(R.string.status_starting_up)); //we send a message here to the progressDialog i believe, but we can clarify that shortly Message msg = mHandler.obtainMessage(TorServiceConstants.ENABLE_TOR_MSG); @@ -1168,7 +1279,6 @@ public class OrbotMainActivity extends Activity implements OrbotConstants, OnLon mHandler.sendMessage(msg); - } //now we stop Tor! amazing! diff --git a/src/org/torproject/android/service/TorService.java b/src/org/torproject/android/service/TorService.java index 9529d6dd..cae16975 100644 --- a/src/org/torproject/android/service/TorService.java +++ b/src/org/torproject/android/service/TorService.java @@ -96,7 +96,7 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon private int mPortHTTP = 8118; private int mPortSOCKS = 9050; - private int mVpnProxyPort = 7231; + private int mVpnProxyPort = 9099; private static final int NOTIFY_ID = 1; private static final int TRANSPROXY_NOTIFY_ID = 2; @@ -1475,6 +1475,18 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon } + public void refreshVpnProxy () { + + debug ("refreshing VPN Proxy"); + + Intent intent = new Intent(TorService.this, OrbotVpnService.class); + intent.setAction("refresh"); + startService(intent); + + } + + + public void clearVpnProxy () { debug ("clearing VPN Proxy"); @@ -1911,6 +1923,31 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon return false; } + public void setTorNetworkEnabled (final boolean isEnabled) + { + + + //it is possible to not have a connection yet, and someone might try to newnym + if (conn != null) + { + new Thread () + { + public void run () + { + try { + + conn.setConf("DisableNetwork", isEnabled ? "0" : "1"); + + } + catch (Exception ioe){ + debug("error requesting newnym: " + ioe.getLocalizedMessage()); + } + } + }.start(); + } + + } + public void newIdentity () { //it is possible to not have a connection yet, and someone might try to newnym @@ -2048,52 +2085,75 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo netInfo = cm.getActiveNetworkInfo(); + boolean newConnectivityState = false; + if(netInfo != null && netInfo.isConnected()) { // WE ARE CONNECTED: DO SOMETHING - mConnectivity = true; + newConnectivityState = true; } else { // WE ARE NOT: DO SOMETHING ELSE - mConnectivity = false; + newConnectivityState = false; } - if (doNetworKSleep) + //is this a change in state? + if (mConnectivity != newConnectivityState) { - try { - updateConfiguration("DisableNetwork", mConnectivity ? "0" : "1", false); - - if (mCurrentStatus != STATUS_OFF) - { - if (!mConnectivity) - { - logNotice(context.getString(R.string.no_network_connectivity_putting_tor_to_sleep_)); - showToolbarNotification(getString(R.string.no_internet_connection_tor),NOTIFY_ID,R.drawable.ic_stat_tor_off); - - } - else - { - logNotice(context.getString(R.string.network_connectivity_is_good_waking_tor_up_)); - showToolbarNotification(getString(R.string.status_activated),NOTIFY_ID,R.drawable.ic_stat_tor); - - if (mHasRoot && mEnableTransparentProxy && mTransProxyNetworkRefresh) - { - - Shell shell = Shell.startRootShell(); - - disableTransparentProxy(shell); - enableTransparentProxy(shell); - - shell.close(); - } - - } - } - - } catch (Exception e) { - logException ("error updating state after network restart",e); - } + + if (doNetworKSleep) + { + try { + + setTorNetworkEnabled (mConnectivity); + + if (mCurrentStatus != STATUS_OFF) + { + if (!mConnectivity) + { + logNotice(context.getString(R.string.no_network_connectivity_putting_tor_to_sleep_)); + showToolbarNotification(getString(R.string.no_internet_connection_tor),NOTIFY_ID,R.drawable.ic_stat_tor_off); + + } + else + { + logNotice(context.getString(R.string.network_connectivity_is_good_waking_tor_up_)); + showToolbarNotification(getString(R.string.status_activated),NOTIFY_ID,R.drawable.ic_stat_tor); + + if (mHasRoot && mEnableTransparentProxy && mTransProxyNetworkRefresh) + { + + Shell shell = Shell.startRootShell(); + + disableTransparentProxy(shell); + enableTransparentProxy(shell); + + shell.close(); + } + + } + } + + saveConfiguration(); + + } catch (Exception e) { + logException ("error updating state after network restart",e); + } + } + + if (mUseVPN && mConnectivity && (mCurrentStatus != STATUS_OFF)) //we need to turn on VPN here so the proxy is running + { + setTorNetworkEnabled (false); + refreshVpnProxy(); + setTorNetworkEnabled (true); + + + } } + mConnectivity = newConnectivityState; + + + } }; diff --git a/src/org/torproject/android/vpn/OrbotVpnService.java b/src/org/torproject/android/vpn/OrbotVpnService.java index 21275627..b4959370 100644 --- a/src/org/torproject/android/vpn/OrbotVpnService.java +++ b/src/org/torproject/android/vpn/OrbotVpnService.java @@ -57,6 +57,10 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { private final static int VPN_MTU = 1500; + private final static boolean isLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + + private boolean isRestart = false; + @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -76,7 +80,7 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { // Stop the previous session by interrupting the thread. if (mThreadVPN == null || (!mThreadVPN.isAlive())) { - boolean isLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + if (!isLollipop) startSocksBypass(); @@ -89,6 +93,13 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { if (mHandler != null) mHandler.postDelayed(new Runnable () { public void run () { stopSelf(); }}, 1000); } + else if (action.equals("refresh")) + { + if (!isLollipop) + startSocksBypass(); + + setupTun2Socks(); + } return START_NOT_STICKY; @@ -102,6 +113,12 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { { try { + + if (mSocksProxyServer != null) + { + stopSocksBypass (); + } + mSocksProxyServer = new ProxyServer(new ServerAuthenticatorNone(null, null)); ProxyServer.setVpnService(OrbotVpnService.this); mSocksProxyServer.start(mSocksProxyPort, 5, InetAddress.getLocalHost()); @@ -115,10 +132,20 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { } + private void stopSocksBypass () + { + + if (mSocksProxyServer != null){ + mSocksProxyServer.stop(); + mSocksProxyServer = null; + } + + + } + @Override public void onDestroy() { stopVPN(); - } private void stopVPN () @@ -126,10 +153,7 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { Tun2Socks.Stop(); - if (mSocksProxyServer != null){ - mSocksProxyServer.stop(); - mSocksProxyServer = null; - } + stopSocksBypass (); if (mInterface != null){ try @@ -167,54 +191,64 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { public void run () { - if (mInterface == null) - { - try + try + { + + // Set the locale to English (or probably any other language that^M + // uses Hindu-Arabic (aka Latin) numerals).^M + // We have found that VpnService.Builder does something locale-dependent^M + // internally that causes errors when the locale uses its own numerals^M + // (i.e., Farsi and Arabic).^M + Locale.setDefault(new Locale("en")); + + //String localhost = InetAddress.getLocalHost().getHostAddress(); + + String vpnName = "OrbotVPN"; + String virtualGateway = "10.0.0.1"; + String virtualIP = "10.0.0.2"; + String virtualNetMask = "255.255.255.0"; + String localSocks = "127.0.0.1" + ':' + TorServiceConstants.PORT_SOCKS_DEFAULT; + String localDNS = "10.0.0.1" + ':' + TorServiceConstants.TOR_DNS_PORT_DEFAULT; + + + Builder builder = new Builder(); + + builder.setMtu(VPN_MTU); + builder.addAddress(virtualGateway,28); + builder.setSession(vpnName); + builder.addRoute("0.0.0.0",0); + // builder.addDnsServer("8.8.8.8"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - - // Set the locale to English (or probably any other language that^M - // uses Hindu-Arabic (aka Latin) numerals).^M - // We have found that VpnService.Builder does something locale-dependent^M - // internally that causes errors when the locale uses its own numerals^M - // (i.e., Farsi and Arabic).^M - Locale.setDefault(new Locale("en")); - - //String localhost = InetAddress.getLocalHost().getHostAddress(); - - String vpnName = "OrbotVPN"; - String virtualGateway = "10.0.0.1"; - String virtualIP = "10.0.0.2"; - String virtualNetMask = "255.255.255.0"; - String localSocks = "127.0.0.1" + ':' + TorServiceConstants.PORT_SOCKS_DEFAULT; - String localDNS = "10.0.0.1" + ':' + TorServiceConstants.TOR_DNS_PORT_DEFAULT; - - - Builder builder = new Builder(); - - builder.setMtu(VPN_MTU); - builder.addAddress(virtualGateway,28); - builder.setSession(vpnName); - builder.addRoute("0.0.0.0",0); - // builder.addDnsServer("8.8.8.8"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - { - doLollipopAppRouting(builder); - } - - // Create a new interface using the builder and save the parameters. - mInterface = builder.setSession(mSessionName) - .setConfigureIntent(mConfigureIntent) - .establish(); - - Tun2Socks.Start(mInterface, VPN_MTU, virtualIP, virtualNetMask, localSocks , localDNS , true); + doLollipopAppRouting(builder); } - catch (Exception e) + + // Create a new interface using the builder and save the parameters. + ParcelFileDescriptor newInterface = builder.setSession(mSessionName) + .setConfigureIntent(mConfigureIntent) + .establish(); + + if (mInterface != null) { - Log.d(TAG,"tun2Socks has stopped",e); + isRestart = true; + + Tun2Socks.Stop(); + mInterface.close(); + } - } - } + + + mInterface = newInterface; + + Tun2Socks.Start(mInterface, VPN_MTU, virtualIP, virtualNetMask, localSocks , localDNS , true); + } + catch (Exception e) + { + Log.d(TAG,"tun2Socks has stopped",e); + } + } + }; mThreadVPN.start(); @@ -222,22 +256,23 @@ public class OrbotVpnService extends VpnService implements Handler.Callback { @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void doLollipopAppRouting (Builder builder) throws NameNotFoundException - { - - - builder.addDisallowedApplication("org.torproject.android"); - - + { + builder.addDisallowedApplication("org.torproject.android"); } @Override public void onRevoke() { - SharedPreferences prefs = TorServiceUtils.getSharedPrefs(getApplicationContext()); - prefs.edit().putBoolean("pref_vpn", false).commit(); - - stopVPN(); - + if (!isRestart) + { + SharedPreferences prefs = TorServiceUtils.getSharedPrefs(getApplicationContext()); + prefs.edit().putBoolean("pref_vpn", false).commit(); + stopVPN(); + + } + + isRestart = false; + super.onRevoke(); }