Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Backend that uses outline-sdk to replace tun2socks #485

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
/Android/app/build/
/Android/app/obj/
/Android/app/release/
/Android/tun2socks/build/
/Android/backend/build/
/captures
10 changes: 8 additions & 2 deletions Android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,35 @@ repositories {

dependencies {
implementation 'com.google.guava:guava:31.0.1-android'

// Third-party test dependencies.
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.json:json:20180813'
testImplementation 'org.mockito:mockito-core:2.13.0'

// Required for instrumented tests
androidTestImplementation 'androidx.annotation:annotation:1.3.0'
androidTestImplementation 'androidx.test:runner:1.4.0'

// UI libraries.
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation "androidx.viewpager2:viewpager2:1.0.0"

// For Firebase Analytics, etc.
implementation 'com.google.firebase:firebase-analytics:19.0.2' // Last version for API <19
implementation 'com.google.firebase:firebase-perf:20.0.4'
implementation 'com.google.firebase:firebase-crashlytics:18.2.6'
implementation 'com.google.firebase:firebase-crashlytics-ndk:18.2.6'
implementation 'com.google.firebase:firebase-config:21.0.1'
// For go-tun2socks
implementation project(":tun2socks")

// For Golang based backend
implementation project(path: ':backend', configuration: 'aarBinary')
}

// For Firebase Analytics
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.firebase-perf'
40 changes: 21 additions & 19 deletions Android/app/src/main/java/app/intra/net/go/GoIntraListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,26 @@
import app.intra.net.dns.DnsPacket;
import app.intra.net.doh.Transaction;
import app.intra.net.doh.Transaction.Status;
import app.intra.sys.firebase.AnalyticsWrapper;
import app.intra.sys.IntraVpnService;
import app.intra.sys.firebase.AnalyticsWrapper;
import com.google.firebase.perf.FirebasePerformance;
import com.google.firebase.perf.metrics.HttpMetric;
import doh.Doh;
import doh.Token;
import intra.TCPSocketSummary;
import intra.UDPSocketSummary;
import intra.DoHQueryStats;
import intra.DoHToken;
import intra.EventListener;
import intra.Intra;
import intra.TCPRetryStats;
import intra.TCPSocketStats;
import intra.UDPSocketStats;
import java.net.ProtocolException;
import java.util.Calendar;
import split.RetryStats;

/**
* 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 intra.Listener {
public class GoIntraListener implements EventListener {

// 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
Expand All @@ -52,15 +54,15 @@ public class GoIntraListener implements intra.Listener {
}

@Override
public void onTCPSocketClosed(TCPSocketSummary summary) {
public void onTCPSocketClosed(TCPSocketStats summary) {
analytics.logTCP(
summary.getUploadBytes(),
summary.getDownloadBytes(),
summary.getServerPort(),
summary.getSynack(),
summary.getDuration());

RetryStats retry = summary.getRetry();
TCPRetryStats retry = summary.getRetry();
if (retry != null) {
// Connection was eligible for split-retry.
if (retry.getSplit() == 0) {
Expand All @@ -78,7 +80,7 @@ public void onTCPSocketClosed(TCPSocketSummary summary) {
}

@Override
public void onUDPSocketClosed(UDPSocketSummary summary) {
public void onUDPSocketClosed(UDPSocketStats summary) {
long totalBytes = summary.getUploadBytes() + summary.getDownloadBytes();
if (totalBytes < UDP_THRESHOLD_BYTES) {
return;
Expand All @@ -91,28 +93,28 @@ public void onUDPSocketClosed(UDPSocketSummary summary) {

private static final LongSparseArray<Status> goStatusMap = new LongSparseArray<>();
static {
goStatusMap.put(Doh.Complete, Status.COMPLETE);
goStatusMap.put(Doh.SendFailed, Status.SEND_FAIL);
goStatusMap.put(Doh.HTTPError, Status.HTTP_ERROR);
goStatusMap.put(Doh.BadQuery, Status.INTERNAL_ERROR); // TODO: Add a BAD_QUERY Status
goStatusMap.put(Doh.BadResponse, Status.BAD_RESPONSE);
goStatusMap.put(Doh.InternalError, Status.INTERNAL_ERROR);
goStatusMap.put(Intra.DoHStatusComplete, Status.COMPLETE);
goStatusMap.put(Intra.DoHStatusSendFailed, Status.SEND_FAIL);
goStatusMap.put(Intra.DoHStatusHTTPError, Status.HTTP_ERROR);
goStatusMap.put(Intra.DoHStatusBadQuery, Status.INTERNAL_ERROR); // TODO: Add a BAD_QUERY Status
goStatusMap.put(Intra.DoHStatusBadResponse, Status.BAD_RESPONSE);
goStatusMap.put(Intra.DoHStatusInternalError, Status.INTERNAL_ERROR);
}

// Wrapping HttpMetric into a doh.Token allows us to get paired query and response notifications
// from Go without reverse-binding any Java APIs into Go. Pairing these notifications is
// required by the structure of the HttpMetric API (which does not have any other way to record
// latency), and reverse binding is worth avoiding, especially because it's not compatible with
// the Go module system (https://github.com/golang/go/issues/27234).
private class Metric implements doh.Token {
private static class Metric implements DoHToken {
final HttpMetric metric;
Metric(String url) {
metric = FirebasePerformance.getInstance().newHttpMetric(url, "POST");
}
}

@Override
public Token onQuery(String url) {
public DoHToken onQuery(String url) {
Metric m = new Metric(url);
m.metric.start();
return m;
Expand All @@ -123,7 +125,7 @@ private static int len(byte[] a) {
}

@Override
public void onResponse(Token token, doh.Summary summary) {
public void onResponse(DoHToken token, DoHQueryStats summary) {
if (summary.getHTTPStatus() != 0 && token != null) {
// HTTP transaction completed. Report performance metrics.
Metric m = (Metric)token;
Expand Down
22 changes: 6 additions & 16 deletions Android/app/src/main/java/app/intra/net/go/GoProber.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
import android.os.Build.VERSION_CODES;
import app.intra.net.doh.Prober;
import app.intra.sys.VpnController;
import doh.Transport;
import protect.Protector;
import tun2socks.Tun2socks;
import intra.Intra;
import intra.SocketProtector;

/**
* Implements a Probe using the Go-based DoH client.
Expand All @@ -38,22 +37,13 @@ public GoProber(Context context) {
@Override
public void probe(String url, Callback callback) {
new Thread(() -> {
String dohIPs = GoVpnAdapter.getIpString(context, url);
String dohIPs = GoVpnAdapter.getDoHServerFallbackIPsString(context, url);
try {
// Protection isn't needed for Lollipop+, or if the VPN is not active.
Protector protector = VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP ? null :
SocketProtector protector = VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP ? null :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get rid of the protector, it complicates things and is no longer needed.

I took a look in the Play Store, and only 0.2% of the installed audience is below Lollipop (Android 5): https://docs.google.com/spreadsheets/d/15zsfgMhJ7TsLumE6S5ZykETwMBvrBptFllX0Z02gFPw/edit?usp=sharing&resourcekey=0-inA8jaG0XS58zXNXU9YOzQ

The global stats say 0.66%: https://apilevels.com/

It's not worth the trouble.

Let's make that change separately though. Change, then move.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we remove the protector, we should also update the minSdkVersion to 21 (Lollipop), to make sure older phones don't get the new version that will break.

VpnController.getInstance().getIntraVpnService();
Transport transport = Tun2socks.newDoHTransport(url, dohIPs, protector, null, null);
if (transport == null) {
callback.onCompleted(false);
return;
}
byte[] response = transport.query(QUERY_DATA);
if (response != null && response.length > 0) {
callback.onCompleted(true);
return;
}
callback.onCompleted(false);
Intra.probeDoHServer(url, dohIPs, protector);
callback.onCompleted(true);
} catch (Exception e) {
callback.onCompleted(false);
}
Expand Down
81 changes: 53 additions & 28 deletions Android/app/src/main/java/app/intra/net/go/GoVpnAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@
import app.intra.sys.firebase.AnalyticsWrapper;
import app.intra.sys.firebase.LogWrapper;
import app.intra.sys.firebase.RemoteConfig;
import doh.Transport;
import intra.IPDevice;
import intra.Intra;
import intra.IntraDevice;
import intra.SocketProtector;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import protect.Protector;
import tun2socks.Tun2socks;

/**
* This is a VpnAdapter that captures all traffic and routes it through a go-tun2socks instance with
Expand Down Expand Up @@ -87,7 +88,8 @@ String make(String template) {
private ParcelFileDescriptor tunFd;

// The Intra session object from go-tun2socks. Initially null.
private intra.Tunnel tunnel;
private IPDevice tun;
private IntraDevice device;
private GoIntraListener listener;

public static GoVpnAdapter establish(@NonNull IntraVpnService vpnService) {
Expand All @@ -108,22 +110,21 @@ public synchronized void start() {
}

private void connectTunnel() {
if (tunnel != null) {
if (device != null && tun != null) {
return;
}
// VPN parameters
final String fakeDns = FAKE_DNS_IP + ":" + DNS_DEFAULT_PORT;

// Strip leading "/" from ip:port string.
listener = new GoIntraListener(vpnService);
String dohURL = PersistentState.getServerUrl(vpnService);

try {
LogWrapper.log(Log.INFO, LOG_TAG, "Starting go-tun2socks");
Transport transport = makeDohTransport(dohURL);
// connectIntraTunnel makes a copy of the file descriptor.
tunnel = Tun2socks.connectIntraTunnel(tunFd.getFd(), fakeDns,
transport, getProtector(), listener);
device = makeIntraDevice(fakeDns);
// newTunFromFile makes a copy of the file descriptor
tun = Intra.newTunFromFile(tunFd.getFd());
Intra.bridgeAsync(tun, device);
} catch (Exception e) {
LogWrapper.logException(e);
VpnController.getInstance().onConnectionStateChanged(vpnService, IntraVpnService.State.FAILING);
Expand All @@ -135,6 +136,26 @@ private void connectTunnel() {
}
}

private void disconnectTunnel() {
if (device != null) {
try {
device.close();
} catch (Exception e) {
LogWrapper.logException(e);
}
}
device = null;

if (tun != null) {
try {
tun.close();
} catch (Exception e) {
LogWrapper.logException(e);
}
}
tun = null;
}

// Set up failure reporting with Choir.
private void enableChoir() {
CountryCode countryCode = new CountryCode(vpnService);
Expand All @@ -149,7 +170,7 @@ private void enableChoir() {
}
String file = vpnService.getFilesDir() + File.separator + CHOIR_FILENAME;
try {
tunnel.enableSNIReporter(file, "intra.metrics.gstatic.com", country);
device.enableSNIReporter(file, "intra.metrics.gstatic.com", country);
} catch (Exception e) {
// Choir setup failure is logged but otherwise ignored, because it does not prevent Intra
// from functioning correctly.
Expand Down Expand Up @@ -178,7 +199,7 @@ private static ParcelFileDescriptor establishVpn(IntraVpnService vpnService) {
}
}

private @Nullable Protector getProtector() {
private @Nullable SocketProtector getProtector() {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
// We don't need socket protection in these versions because the call to
// "addDisallowedApplication" effectively protects all sockets in this app.
Expand All @@ -188,9 +209,8 @@ private static ParcelFileDescriptor establishVpn(IntraVpnService vpnService) {
}

public synchronized void close() {
if (tunnel != null) {
tunnel.disconnect();
}
disconnectTunnel();

if (tunFd != null) {
try {
tunFd.close();
Expand All @@ -201,21 +221,21 @@ public synchronized void close() {
tunFd = null;
}

private doh.Transport makeDohTransport(@Nullable String url) throws Exception {
@NonNull String realUrl = PersistentState.expandUrl(vpnService, url);
String dohIPs = getIpString(vpnService, realUrl);
String host = new URL(realUrl).getHost();
private IntraDevice makeIntraDevice(String fakeDns) throws Exception {
String dohUrl = getDoHServerUrl(vpnService);
String dohIPs = getDoHServerFallbackIPsString(vpnService, dohUrl);
String host = new URL(dohUrl).getHost();
long startTime = SystemClock.elapsedRealtime();
final doh.Transport transport;
final IntraDevice device;
try {
transport = Tun2socks.newDoHTransport(realUrl, dohIPs, getProtector(), null, listener);
device = new IntraDevice(fakeDns, dohUrl, dohIPs, getProtector(), listener);
} catch (Exception e) {
AnalyticsWrapper.get(vpnService).logBootstrapFailed(host);
throw e;
}
int delta = (int) (SystemClock.elapsedRealtime() - startTime);
AnalyticsWrapper.get(vpnService).logBootstrap(host, delta);
return transport;
return device;
}

/**
Expand All @@ -228,7 +248,7 @@ public synchronized void updateDohUrl() {
// Adapter is closed.
return;
}
if (tunnel == null) {
if (device == null || tun == null) {
// Attempt to re-create the tunnel. Creation may have failed originally because the DoH
// server could not be reached. This will update the DoH URL as well.
connectTunnel();
Expand All @@ -238,19 +258,24 @@ public synchronized void updateDohUrl() {
// is called on network changes, and it's important to switch to a fresh transport because the
// old transport may be using sockets on a deleted interface, which may block until they time
// out.
String url = PersistentState.getServerUrl(vpnService);
String dohUrl = getDoHServerUrl(vpnService);
String dohFallbackIPs = getDoHServerFallbackIPsString(vpnService, dohUrl);
try {
tunnel.setDNS(makeDohTransport(url));
device.updateDoHServer(dohUrl, dohFallbackIPs);
} catch (Exception e) {
LogWrapper.logException(e);
tunnel.disconnect();
tunnel = null;
disconnectTunnel();
VpnController.getInstance().onConnectionStateChanged(vpnService, IntraVpnService.State.FAILING);
}
}

private static String getDoHServerUrl(Context context) {
final @Nullable String url = PersistentState.getServerUrl(context);
return PersistentState.expandUrl(context, url);
}

// Returns the known IPs for this URL as a string containing a comma-separated list.
static String getIpString(Context context, String url) {
static String getDoHServerFallbackIPsString(Context context, String url) {
Resources res = context.getResources();
String[] urls = res.getStringArray(R.array.urls);
String[] ips = res.getStringArray(R.array.ips);
Expand Down
4 changes: 2 additions & 2 deletions Android/app/src/main/java/app/intra/sys/IntraVpnService.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@
import app.intra.sys.firebase.AnalyticsWrapper;
import app.intra.sys.firebase.LogWrapper;
import app.intra.ui.MainActivity;
import intra.SocketProtector;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import protect.Protector;

public class IntraVpnService extends VpnService implements NetworkListener,
SharedPreferences.OnSharedPreferenceChangeListener, Protector {
SharedPreferences.OnSharedPreferenceChangeListener, SocketProtector {

/**
* null: There is no connection
Expand Down
Loading