Merge "Showcase HTTP proxy usage in ToyVPN."

This commit is contained in:
Treehugger Robot
2019-01-25 13:41:00 +00:00
committed by Gerrit Code Review
5 changed files with 180 additions and 30 deletions

View File

@@ -31,6 +31,30 @@
<TextView style="@style/item" android:text="@string/secret"/> <TextView style="@style/item" android:text="@string/secret"/>
<EditText style="@style/item" android:id="@+id/secret" android:password="true"/> <EditText style="@style/item" android:id="@+id/secret" android:password="true"/>
<TextView style="@style/item" android:text="@string/proxyhost"/>
<EditText style="@style/item" android:id="@+id/proxyhost"/>
<TextView style="@style/item" android:text="@string/proxyport"/>
<EditText style="@style/item" android:id="@+id/proxyport" android:inputType="number"/>
<TextView style="@style/item" android:text="@string/packages"/>
<RadioGroup
style="@style/item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/allowed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/allowed"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/disallowed"/>
</RadioGroup>
<EditText style="@style/item" android:id="@+id/packages"/>
<Button style="@style/item" android:id="@+id/connect" android:text="@string/connect"/> <Button style="@style/item" android:id="@+id/connect" android:text="@string/connect"/>
<Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/> <Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/>

View File

@@ -23,8 +23,20 @@
<string name="secret">Shared Secret:</string> <string name="secret">Shared Secret:</string>
<string name="connect">Connect!</string> <string name="connect">Connect!</string>
<string name="disconnect">Disconnect!</string> <string name="disconnect">Disconnect!</string>
<string name="proxyhost">HTTP proxy hostname</string>
<string name="proxyport">HTTP proxy port</string>
<string name="packages">Packages (comma separated):</string>
<string name="allowed">Allow</string>
<string name="disallowed">Disallow</string>
<string name="connecting">ToyVPN is connecting...</string> <string name="connecting">ToyVPN is connecting...</string>
<string name="connected">ToyVPN is connected!</string> <string name="connected">ToyVPN is connected!</string>
<string name="disconnected">ToyVPN is disconnected!</string> <string name="disconnected">ToyVPN is disconnected!</string>
<string name="incomplete_proxy_settings">
Incomplete proxy settings. For HTTP proxy we require both hostname and port settings.
</string>
<string name="unknown_package_names">
Some of the specified package names do not correspond to any installed packages.
</string>
</resources> </resources>

View File

@@ -21,7 +21,14 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.VpnService; import android.net.VpnService;
import android.os.Bundle; import android.os.Bundle;
import android.widget.RadioButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
public class ToyVpnClient extends Activity { public class ToyVpnClient extends Activity {
public interface Prefs { public interface Prefs {
@@ -29,6 +36,10 @@ public class ToyVpnClient extends Activity {
String SERVER_ADDRESS = "server.address"; String SERVER_ADDRESS = "server.address";
String SERVER_PORT = "server.port"; String SERVER_PORT = "server.port";
String SHARED_SECRET = "shared.secret"; String SHARED_SECRET = "shared.secret";
String PROXY_HOSTNAME = "proxyhost";
String PROXY_PORT = "proxyport";
String ALLOW = "allow";
String PACKAGES = "packages";
} }
@Override @Override
@@ -36,22 +47,64 @@ public class ToyVpnClient extends Activity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.form); setContentView(R.layout.form);
final TextView serverAddress = (TextView) findViewById(R.id.address); final TextView serverAddress = findViewById(R.id.address);
final TextView serverPort = (TextView) findViewById(R.id.port); final TextView serverPort = findViewById(R.id.port);
final TextView sharedSecret = (TextView) findViewById(R.id.secret); final TextView sharedSecret = findViewById(R.id.secret);
final TextView proxyHost = findViewById(R.id.proxyhost);
final TextView proxyPort = findViewById(R.id.proxyport);
final RadioButton allowed = findViewById(R.id.allowed);
final TextView packages = findViewById(R.id.packages);
final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, MODE_PRIVATE); final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, MODE_PRIVATE);
serverAddress.setText(prefs.getString(Prefs.SERVER_ADDRESS, "")); serverAddress.setText(prefs.getString(Prefs.SERVER_ADDRESS, ""));
serverPort.setText(prefs.getString(Prefs.SERVER_PORT, "")); int serverPortPrefValue = prefs.getInt(Prefs.SERVER_PORT, 0);
serverPort.setText(String.valueOf(serverPortPrefValue == 0 ? "" : serverPortPrefValue));
sharedSecret.setText(prefs.getString(Prefs.SHARED_SECRET, "")); sharedSecret.setText(prefs.getString(Prefs.SHARED_SECRET, ""));
proxyHost.setText(prefs.getString(Prefs.PROXY_HOSTNAME, ""));
int proxyPortPrefValue = prefs.getInt(Prefs.PROXY_PORT, 0);
proxyPort.setText(proxyPortPrefValue == 0 ? "" : String.valueOf(proxyPortPrefValue));
allowed.setChecked(prefs.getBoolean(Prefs.ALLOW, true));
packages.setText(String.join(", ", prefs.getStringSet(
Prefs.PACKAGES, Collections.emptySet())));
findViewById(R.id.connect).setOnClickListener(v -> { findViewById(R.id.connect).setOnClickListener(v -> {
if (!checkProxyConfigs(proxyHost.getText().toString(),
proxyPort.getText().toString())) {
return;
}
final Set<String> packageSet =
Arrays.stream(packages.getText().toString().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
if (!checkPackages(packageSet)) {
return;
}
int serverPortNum;
try {
serverPortNum = Integer.parseInt(serverPort.getText().toString());
} catch (NumberFormatException e) {
serverPortNum = 0;
}
int proxyPortNum;
try {
proxyPortNum = Integer.parseInt(proxyPort.getText().toString());
} catch (NumberFormatException e) {
proxyPortNum = 0;
}
prefs.edit() prefs.edit()
.putString(Prefs.SERVER_ADDRESS, serverAddress.getText().toString()) .putString(Prefs.SERVER_ADDRESS, serverAddress.getText().toString())
.putString(Prefs.SERVER_PORT, serverPort.getText().toString()) .putInt(Prefs.SERVER_PORT, serverPortNum)
.putString(Prefs.SHARED_SECRET, sharedSecret.getText().toString()) .putString(Prefs.SHARED_SECRET, sharedSecret.getText().toString())
.putString(Prefs.PROXY_HOSTNAME, proxyHost.getText().toString())
.putInt(Prefs.PROXY_PORT, proxyPortNum)
.putBoolean(Prefs.ALLOW, allowed.isChecked())
.putStringSet(Prefs.PACKAGES, packageSet)
.commit(); .commit();
Intent intent = VpnService.prepare(ToyVpnClient.this); Intent intent = VpnService.prepare(ToyVpnClient.this);
if (intent != null) { if (intent != null) {
startActivityForResult(intent, 0); startActivityForResult(intent, 0);
@@ -64,6 +117,26 @@ public class ToyVpnClient extends Activity {
}); });
} }
private boolean checkProxyConfigs(String proxyHost, String proxyPort) {
final boolean hasIncompleteProxyConfigs = proxyHost.isEmpty() != proxyPort.isEmpty();
if (hasIncompleteProxyConfigs) {
Toast.makeText(this, R.string.incomplete_proxy_settings, Toast.LENGTH_SHORT).show();
}
return !hasIncompleteProxyConfigs;
}
private boolean checkPackages(Set<String> packageNames) {
final boolean hasCorrectPackageNames = packageNames.isEmpty() ||
getPackageManager().getInstalledPackages(0).stream()
.map(pi -> pi.packageName)
.collect(Collectors.toSet())
.containsAll(packageNames);
if (!hasCorrectPackageNames) {
Toast.makeText(this, R.string.unknown_package_names, Toast.LENGTH_SHORT).show();
}
return hasCorrectPackageNames;
}
@Override @Override
protected void onActivityResult(int request, int result, Intent data) { protected void onActivityResult(int request, int result, Intent data) {
if (result == RESULT_OK) { if (result == RESULT_OK) {

View File

@@ -19,8 +19,11 @@ package com.example.android.toyvpn;
import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.US_ASCII;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.pm.PackageManager;
import android.net.ProxyInfo;
import android.net.VpnService; import android.net.VpnService;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import java.io.FileInputStream; import java.io.FileInputStream;
@@ -31,6 +34,7 @@ import java.net.SocketAddress;
import java.net.SocketException; import java.net.SocketException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel; import java.nio.channels.DatagramChannel;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class ToyVpnConnection implements Runnable { public class ToyVpnConnection implements Runnable {
@@ -83,14 +87,34 @@ public class ToyVpnConnection implements Runnable {
private PendingIntent mConfigureIntent; private PendingIntent mConfigureIntent;
private OnEstablishListener mOnEstablishListener; private OnEstablishListener mOnEstablishListener;
// Proxy settings
private String mProxyHostName;
private int mProxyHostPort;
// Allowed/Disallowed packages for VPN usage
private final boolean mAllow;
private final Set<String> mPackages;
public ToyVpnConnection(final VpnService service, final int connectionId, public ToyVpnConnection(final VpnService service, final int connectionId,
final String serverName, final int serverPort, final byte[] sharedSecret) { final String serverName, final int serverPort, final byte[] sharedSecret,
final String proxyHostName, final int proxyHostPort, boolean allow,
final Set<String> packages) {
mService = service; mService = service;
mConnectionId = connectionId; mConnectionId = connectionId;
mServerName = serverName; mServerName = serverName;
mServerPort= serverPort; mServerPort= serverPort;
mSharedSecret = sharedSecret; mSharedSecret = sharedSecret;
if (!TextUtils.isEmpty(proxyHostName)) {
mProxyHostName = proxyHostName;
}
if (proxyHostPort > 0) {
// The port value is always an integer due to the configured inputType.
mProxyHostPort = proxyHostPort;
}
mAllow = allow;
mPackages = packages;
} }
/** /**
@@ -309,11 +333,23 @@ public class ToyVpnConnection implements Runnable {
// Create a new interface using the builder and save the parameters. // Create a new interface using the builder and save the parameters.
final ParcelFileDescriptor vpnInterface; final ParcelFileDescriptor vpnInterface;
for (String packageName : mPackages) {
try {
if (mAllow) {
builder.addAllowedApplication(packageName);
} else {
builder.addDisallowedApplication(packageName);
}
} catch (PackageManager.NameNotFoundException e){
Log.w(getTag(), "Package not available: " + packageName, e);
}
}
builder.setSession(mServerName).setConfigureIntent(mConfigureIntent);
if (!TextUtils.isEmpty(mProxyHostName)) {
builder.setHttpProxy(ProxyInfo.buildDirectProxy(mProxyHostName, mProxyHostPort));
}
synchronized (mService) { synchronized (mService) {
vpnInterface = builder vpnInterface = builder.establish();
.setSession(mServerName)
.setConfigureIntent(mConfigureIntent)
.establish();
if (mOnEstablishListener != null) { if (mOnEstablishListener != null) {
mOnEstablishListener.onEstablish(vpnInterface); mOnEstablishListener.onEstablish(vpnInterface);
} }

View File

@@ -17,8 +17,9 @@
package com.example.android.toyvpn; package com.example.android.toyvpn;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.VpnService; import android.net.VpnService;
@@ -30,6 +31,8 @@ import android.util.Pair;
import android.widget.Toast; import android.widget.Toast;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -101,17 +104,15 @@ public class ToyVpnService extends VpnService implements Handler.Callback {
final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE); final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE);
final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, ""); final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, "");
final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes(); final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes();
final int port; final boolean allow = prefs.getBoolean(ToyVpnClient.Prefs.ALLOW, true);
try { final Set<String> packages =
port = Integer.parseInt(prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, "")); prefs.getStringSet(ToyVpnClient.Prefs.PACKAGES, Collections.emptySet());
} catch (NumberFormatException e) { final int port = prefs.getInt(ToyVpnClient.Prefs.SERVER_PORT, 0);
Log.e(TAG, "Bad port: " + prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, null), e); final String proxyHost = prefs.getString(ToyVpnClient.Prefs.PROXY_HOSTNAME, "");
return; final int proxyPort = prefs.getInt(ToyVpnClient.Prefs.PROXY_PORT, 0);
}
// Kick off a connection.
startConnection(new ToyVpnConnection( startConnection(new ToyVpnConnection(
this, mNextConnectionId.getAndIncrement(), server, port, secret)); this, mNextConnectionId.getAndIncrement(), server, port, secret,
proxyHost, proxyPort, allow, packages));
} }
private void startConnection(final ToyVpnConnection connection) { private void startConnection(final ToyVpnConnection connection) {
@@ -121,13 +122,11 @@ public class ToyVpnService extends VpnService implements Handler.Callback {
// Handler to mark as connected once onEstablish is called. // Handler to mark as connected once onEstablish is called.
connection.setConfigureIntent(mConfigureIntent); connection.setConfigureIntent(mConfigureIntent);
connection.setOnEstablishListener(new ToyVpnConnection.OnEstablishListener() { connection.setOnEstablishListener(tunInterface -> {
public void onEstablish(ParcelFileDescriptor tunInterface) {
mHandler.sendEmptyMessage(R.string.connected); mHandler.sendEmptyMessage(R.string.connected);
mConnectingThread.compareAndSet(thread, null); mConnectingThread.compareAndSet(thread, null);
setConnection(new Connection(thread, tunInterface)); setConnection(new Connection(thread, tunInterface));
}
}); });
thread.start(); thread.start();
} }
@@ -159,7 +158,13 @@ public class ToyVpnService extends VpnService implements Handler.Callback {
} }
private void updateForegroundNotification(final int message) { private void updateForegroundNotification(final int message) {
startForeground(1, new Notification.Builder(this) final String NOTIFICATION_CHANNEL_ID = "ToyVpn";
NotificationManager mNotificationManager = (NotificationManager) getSystemService(
NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannel(new NotificationChannel(
NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
NotificationManager.IMPORTANCE_DEFAULT));
startForeground(1, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_vpn) .setSmallIcon(R.drawable.ic_vpn)
.setContentText(getString(message)) .setContentText(getString(message))
.setContentIntent(mConfigureIntent) .setContentIntent(mConfigureIntent)