Skip to content

Commit

Permalink
Enable A/B testing of go-tun2socks
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Schwartz committed Aug 16, 2019
1 parent 583b583 commit d220d31
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 55 deletions.
6 changes: 5 additions & 1 deletion Android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ android {

defaultConfig {
applicationId "app.intra"
minSdkVersion 14
// The Java code is compatible back to SDK version 14, but gomobile requires
// a minSdkVersion of 15.
minSdkVersion 15
targetSdkVersion 28
versionCode 40
versionName "1.1.9"
Expand Down Expand Up @@ -131,6 +133,8 @@ dependencies {
implementation 'org.slf4j:slf4j-android:1.7.25'
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
// For go-tun2socks
implementation project(":tun2socks")
}
// For Firebase Analytics
apply plugin: 'com.google.gms.google-services'
Expand Down
14 changes: 7 additions & 7 deletions Android/app/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<issue
id="NewApi"
message="Multi-catch with these reflection exceptions requires API level 19 (current min is 14) because they get compiled to the common but new super type `ReflectiveOperationException`. As a workaround either create individual catch statements, or catch `Exception`."
message="Multi-catch with these reflection exceptions requires API level 19 (current min is 15) because they get compiled to the common but new super type `ReflectiveOperationException`. As a workaround either create individual catch statements, or catch `Exception`."
errorLine1=" } catch (InstantiationException | IllegalAccessException e) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
Expand All @@ -21,7 +21,7 @@

<issue
id="NewApi"
message="Call requires API level 24 (current min is 14): `new java.lang.Exception`"
message="Call requires API level 24 (current min is 15): `new java.lang.Exception`"
errorLine1=" super(message, cause, enableSuppression, writableStackTrace);"
errorLine2=" ~~~~~">
<location
Expand All @@ -32,7 +32,7 @@

<issue
id="NewApi"
message="Call requires API level 24 (current min is 14): `new java.lang.RuntimeException`"
message="Call requires API level 24 (current min is 15): `new java.lang.RuntimeException`"
errorLine1=" super(message, cause, enableSuppression, writableStackTrace);"
errorLine2=" ~~~~~">
<location
Expand All @@ -43,7 +43,7 @@

<issue
id="NewApi"
message="Call requires API level 24 (current min is 14): `new java.lang.Exception`"
message="Call requires API level 24 (current min is 15): `new java.lang.Exception`"
errorLine1=" super(message, cause, enableSuppression, writableStackTrace);"
errorLine2=" ~~~~~">
<location
Expand All @@ -54,7 +54,7 @@

<issue
id="NewApi"
message="Call requires API level 19 (current min is 14): `java.net.InetSocketAddress#getHostString`"
message="Call requires API level 19 (current min is 15): `java.net.InetSocketAddress#getHostString`"
errorLine1=" this.remoteServerHost = address.getHostString();"
errorLine2=" ~~~~~~~~~~~~~">
<location
Expand All @@ -65,7 +65,7 @@

<issue
id="UnusedAttribute"
message="Attribute `layoutDirection` is only used in API level 17 and higher (current min is 14)"
message="Attribute `layoutDirection` is only used in API level 17 and higher (current min is 15)"
errorLine1=" android:layoutDirection=&quot;ltr&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
Expand All @@ -76,7 +76,7 @@

<issue
id="UnusedAttribute"
message="Attribute `importantForAutofill` is only used in API level 26 and higher (current min is 14)"
message="Attribute `importantForAutofill` is only used in API level 26 and higher (current min is 15)"
errorLine1=" android:importantForAutofill=&quot;no&quot;/>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
Expand Down
10 changes: 5 additions & 5 deletions Android/app/src/main/java/app/intra/net/VpnAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
* Abstract class representing a VPN Adapter, for use by IntraVpnService. For our purposes, a
* VpnAdapter is just a thread that can safely be stopped at any time.
*/
public abstract class VpnAdapter extends Thread {
protected static final int VPN_INTERFACE_MTU = 32767;
public abstract class VpnAdapter {
// This value must match the hardcoded MTU in outline-go-tun2socks.
// TODO: Make outline-go-tun2socks's MTU configurable.
protected static final int VPN_INTERFACE_MTU = 1500;
protected static final int DNS_DEFAULT_PORT = 53;

public VpnAdapter(String name) {
super(name);
}
public abstract void start();

/**
* Perform a safe shutdown.
Expand Down
93 changes: 93 additions & 0 deletions Android/app/src/main/java/app/intra/net/socks/GoIntraListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2019 Jigsaw Operations LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package app.intra.net.socks;

import android.content.Context;
import android.os.Bundle;
import app.intra.sys.Names;
import com.google.firebase.analytics.FirebaseAnalytics;
import intra.RetryStats;
import intra.TCPSocketSummary;
import intra.UDPSocketSummary;

/**
* This is a callback class that is passed to our go-tun2socks code. Go calls this class's methods
* when a socket has concluded, with performance metrics for that socket, and this class forwards
* those metrics to Firebase.
*/
public class GoIntraListener implements tunnel.IntraListener {

// UDP is often used for one-off messages and pings. The relative overhead of reporting metrics
// on these short messages would be large, so we only report metrics on sockets that transfer at
// least this many bytes.
private static final int UDP_THRESHOLD_BYTES = 10000;

private final Context context;
GoIntraListener(Context context) {
this.context = context;
}

@Override
public void onTCPSocketClosed(TCPSocketSummary summary) {
// We had a functional socket long enough to record statistics.
// Report the BYTES event :
// - UPLOAD: total bytes uploaded over the lifetime of a socket
// - DOWNLOAD: total bytes downloaded
// - PORT: TCP port number (i.e. protocol type)
// - TCP_HANDSHAKE_MS: TCP handshake latency in milliseconds
// - DURATION: socket lifetime in seconds
// - TODO: FIRST_BYTE_MS: Time between socket open and first byte from server, in milliseconds.

Bundle bytesEvent = new Bundle();
bytesEvent.putLong(Names.UPLOAD.name(), summary.getUploadBytes());
bytesEvent.putLong(Names.DOWNLOAD.name(), summary.getDownloadBytes());
bytesEvent.putInt(Names.PORT.name(), summary.getServerPort());
bytesEvent.putInt(Names.TCP_HANDSHAKE_MS.name(), summary.getSynack());
bytesEvent.putInt(Names.DURATION.name(), summary.getDuration());
FirebaseAnalytics.getInstance(context).logEvent(Names.BYTES.name(), bytesEvent);

RetryStats retry = summary.getRetry();
if (retry != null) {
// Prepare an EARLY_RESET event to collect metrics on success rates for splitting:
// - BYTES : Amount uploaded before reset
// - CHUNKS : Number of upload writes before reset
// - TIMEOUT : Whether the initial connection failed with a timeout.
// - SPLIT : Number of bytes included in the first retry segment
// - RETRY : 1 if retry succeeded, otherwise 0
Bundle resetEvent = new Bundle();
resetEvent.putInt(Names.BYTES.name(), retry.getBytes());
resetEvent.putInt(Names.CHUNKS.name(), retry.getChunks());
resetEvent.putInt(Names.TIMEOUT.name(), retry.getTimeout() ? 1 : 0);
resetEvent.putInt(Names.SPLIT.name(), retry.getSplit());
boolean success = summary.getDownloadBytes() > 0;
resetEvent.putInt(Names.RETRY.name(), success ? 1 : 0);
FirebaseAnalytics.getInstance(context).logEvent(Names.EARLY_RESET.name(), resetEvent);
}
}

@Override
public void onUDPSocketClosed(UDPSocketSummary summary) {
long totalBytes = summary.getUploadBytes() + summary.getDownloadBytes();
if (totalBytes < UDP_THRESHOLD_BYTES) {
return;
}
Bundle event = new Bundle();
event.putLong(Names.UPLOAD.name(), summary.getUploadBytes());
event.putLong(Names.DOWNLOAD.name(), summary.getDownloadBytes());
event.putLong(Names.DURATION.name(), summary.getDuration());
FirebaseAnalytics.getInstance(context).logEvent(Names.UDP.name(), event);
}
}
147 changes: 147 additions & 0 deletions Android/app/src/main/java/app/intra/net/socks/GoVpnAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
Copyright 2019 Jigsaw Operations LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package app.intra.net.socks;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.annotation.NonNull;
import app.intra.net.VpnAdapter;
import app.intra.net.socks.TLSProbe.Result;
import app.intra.sys.IntraVpnService;
import app.intra.sys.LogWrapper;
import java.io.IOException;
import java.util.Locale;
import tun2socks.IntraTunnel;
import tun2socks.Tun2socks;

/**
* This is a VpnAdapter that captures all traffic and routes it through a go-tun2socks instance with
* custom logic for Intra.
*/
public class GoVpnAdapter extends VpnAdapter {
private static final String LOG_TAG = "SocksVpnAdapter";

// IPv4 VPN constants
private static final String IPV4_TEMPLATE = "10.111.222.%d";
private static final int IPV4_PREFIX_LENGTH = 24;

// The VPN service and tun2socks must agree on the layout of the network. By convention, we
// assign the following values to the final byte of an address within a subnet.
private enum LanIp {
GATEWAY(1), ROUTER(2), DNS(3);

// Value of the final byte, to be substituted into the template.
private final int value;

LanIp(int value) {
this.value = value;
}

String make(String template) {
return String.format(Locale.ROOT, template, value);
}
}

// Service context in which the VPN is running.
private final Context context;

// DNS resolver running on localhost.
private final LocalhostResolver resolver;

// TUN device representing the VPN.
private final ParcelFileDescriptor tunFd;

// The Intra session object from go-tun2socks. Initially null.
private IntraTunnel tunnel;

public static GoVpnAdapter establish(@NonNull IntraVpnService vpnService) {
LocalhostResolver resolver = LocalhostResolver.get(vpnService);
if (resolver == null) {
return null;
}
ParcelFileDescriptor tunFd = establishVpn(vpnService);
if (tunFd == null) {
return null;
}
return new GoVpnAdapter(vpnService, tunFd, resolver);
}

private GoVpnAdapter(Context context, ParcelFileDescriptor tunFd, LocalhostResolver resolver) {
this.context = context;
this.resolver = resolver;
this.tunFd = tunFd;
}

@Override
public synchronized void start() {
resolver.start();

// VPN parameters
final String fakeDnsIp = LanIp.DNS.make(IPV4_TEMPLATE);
final String fakeDns = fakeDnsIp + ":" + DNS_DEFAULT_PORT;

Result r = TLSProbe.run(context);
LogWrapper.log(Log.INFO, LOG_TAG, "TLS probe result: " + r.name());
boolean alwaysSplitHttps = r == Result.TLS_FAILED;

String trueDns = resolver.getAddress().toString().substring(1);
GoIntraListener listener = new GoIntraListener(context);

try {
LogWrapper.log(Log.INFO, LOG_TAG, "Starting go-tun2socks");
tunnel = Tun2socks.connectIntraTunnel(tunFd.getFd(), fakeDns, trueDns, trueDns, alwaysSplitHttps, listener);
} catch (Exception e) {
LogWrapper.logException(e);
tunnel = null;
close();
}
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static ParcelFileDescriptor establishVpn(IntraVpnService vpnService) {
try {
return vpnService.newBuilder()
.setSession("Intra go-tun2socks VPN")
.setMtu(VPN_INTERFACE_MTU)
.addAddress(LanIp.GATEWAY.make(IPV4_TEMPLATE), IPV4_PREFIX_LENGTH)
.addRoute("0.0.0.0", 0)
.addDnsServer(LanIp.DNS.make(IPV4_TEMPLATE))
.addDisallowedApplication(vpnService.getPackageName())
.establish();
} catch (Exception e) {
LogWrapper.logException(e);
return null;
}
}

@Override
public synchronized void close() {
if (tunnel != null) {
tunnel.disconnect();
}
try {
tunFd.close();
} catch (IOException e) {
LogWrapper.logException(e);
}
if (resolver != null) {
resolver.shutdown();
}
}
}
Loading

0 comments on commit d220d31

Please sign in to comment.