Skip to content

Latest commit

 

History

History

mobileproxy

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

MobileProxy: Local Proxy Library for Mobile Apps

This package enables the use Go Mobile to generate a mobile library to run a local proxy and configure your app networking libraries.

Content app without MobileProxy: image

Content app with MobileProxy: image

The integration typically consists of the following steps:

  1. Build the mobile native library using Go Mobile.
  2. Add the library to your application.
  3. Configure and run MobileProxy within your app.
  4. Update your networking code to proxy traffic through the local MobileProxy.

Build the Go Mobile binaries with go build

From the x/ directory:

go build -o "$(pwd)/out/" golang.org/x/mobile/cmd/gomobile golang.org/x/mobile/cmd/gobind

Build the iOS and Android libraries with gomobile bind

PATH="$(pwd)/out:$PATH" gomobile bind -ldflags='-s -w' -target=ios -iosversion=11.0 -o "$(pwd)/out/mobileproxy.xcframework" github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
PATH="$(pwd)/out:$PATH" gomobile bind -ldflags='-s -w' -target=android -androidapi=21 -o "$(pwd)/out/mobileproxy.aar" github.com/Jigsaw-Code/outline-sdk/x/mobileproxy

Note: Gomobile expects gobind to be in the PATH, that's why we need to prebuild it, and set up the PATH accordingly.

The -ldflags='-s -w' flag strips debug symbols to reduce the size of the output library.

See our Github Test Action for how we build the Mobileproxy in our tests.

Sample iOS generated Code

The header file below is an example of the Objective-C interface that Go Mobile generates.

Warning: this example may diverge from what is actually generated by the current code. Use the coed you generate instead.

Mobileproxy.objc.h:

// Objective-C API for talking to github.com/Jigsaw-Code/outline-sdk/x/mobileproxy Go package.
//   gobind -lang=objc github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
//
// File is generated by gobind. Do not edit.

#ifndef __Mobileproxy_H__
#define __Mobileproxy_H__

@import Foundation;
#include "ref.h"
#include "Universe.objc.h"


@class MobileproxyProxy;
@class MobileproxyStreamDialer;
@class MobileproxyStringList;
@protocol MobileproxyLogWriter;
@class MobileproxyLogWriter;

@protocol MobileproxyLogWriter <NSObject>
- (BOOL)writeString:(NSString* _Nullable)s n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error;
@end

/**
 * Proxy enables you to get the actual address bound by the server and stop the service when no longer needed.
 */
@interface MobileproxyProxy : NSObject <goSeqRefInterface> {
}
@property(strong, readonly) _Nonnull id _ref;

- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
- (nonnull instancetype)init;
/**
 * Address returns the IP and port the server is bound to.
 */
- (NSString* _Nonnull)address;
/**
 * Host returns the IP the server is bound to.
 */
- (NSString* _Nonnull)host;
/**
 * Port returns the port the server is bound to.
 */
- (long)port;
/**
 * Stop gracefully stops the proxy service, waiting for at most timeout seconds before forcefully closing it.
The function takes a timeoutSeconds number instead of a [time.Duration] so it's compatible with Go Mobile.
 */
- (void)stop:(long)timeoutSeconds;
@end

/**
 * StreamDialer encapsulates the logic to create stream connections (like TCP).
 */
@interface MobileproxyStreamDialer : NSObject <goSeqRefInterface> {
}
@property(strong, readonly) _Nonnull id _ref;

- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
/**
 * NewStreamDialerFromConfig creates a [StreamDialer] based on the given config.
The config format is specified in https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config#hdr-Config_Format.
 */
- (nullable instancetype)initFromConfig:(NSString* _Nullable)transportConfig;
// skipped field StreamDialer.StreamDialer with unsupported type: github.com/Jigsaw-Code/outline-sdk/transport.StreamDialer

// skipped method StreamDialer.DialStream with unsupported parameter or return types

@end

/**
 * StringList allows us to pass a list of strings to the Go Mobile functions, since Go Mobiule doesn't
support slices as parameters.
 */
@interface MobileproxyStringList : NSObject <goSeqRefInterface> {
}
@property(strong, readonly) _Nonnull id _ref;

- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
- (nonnull instancetype)init;
/**
 * Append adds the string value to the end of the list.
 */
- (void)append:(NSString* _Nullable)value;
@end

/**
 * NewListFromLines creates a StringList by splitting the input string on new lines.
 */
FOUNDATION_EXPORT MobileproxyStringList* _Nullable MobileproxyNewListFromLines(NSString* _Nullable lines);

/**
 * NewSmartStreamDialer automatically selects a DNS and TLS strategy to use, and return a [StreamDialer]
that will use the selected strategy.
It uses testDomain to find a strategy that works when accessing those domains.
The strategies to search are given in the searchConfig. An example can be found in
https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.json
 */
FOUNDATION_EXPORT MobileproxyStreamDialer* _Nullable MobileproxyNewSmartStreamDialer(MobileproxyStringList* _Nullable testDomains, NSString* _Nullable searchConfig, id<MobileproxyLogWriter> _Nullable logWriter, NSError* _Nullable* _Nullable error);

/**
 * NewStderrLogWriter creates a [LogWriter] that writes to the standard error output.
 */
FOUNDATION_EXPORT id<MobileproxyLogWriter> _Nullable MobileproxyNewStderrLogWriter(void);

/**
 * NewStreamDialerFromConfig creates a [StreamDialer] based on the given config.
The config format is specified in https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config#hdr-Config_Format.
 */
FOUNDATION_EXPORT MobileproxyStreamDialer* _Nullable MobileproxyNewStreamDialerFromConfig(NSString* _Nullable transportConfig, NSError* _Nullable* _Nullable error);

/**
 * RunProxy runs a local web proxy that listens on localAddress, and handles proxy requests by
establishing connections to requested destination using the [StreamDialer].
 */
FOUNDATION_EXPORT MobileproxyProxy* _Nullable MobileproxyRunProxy(NSString* _Nullable localAddress, MobileproxyStreamDialer* _Nullable dialer, NSError* _Nullable* _Nullable error);

@class MobileproxyLogWriter;

/**
 * LogWriter is used as a sink for logging.
 */
@interface MobileproxyLogWriter : NSObject <goSeqRefInterface, MobileproxyLogWriter> {
}
@property(strong, readonly) _Nonnull id _ref;

- (nonnull instancetype)initWithRef:(_Nonnull id)ref;
- (BOOL)writeString:(NSString* _Nullable)s n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error;
@end

#endif
Sample Android generated Code

The files below are examples of the Java interface that Go Mobile generates.

Warning: this example may diverge from what is actually generated by the current code. Use the coed you generate instead.

LogWriter.java:

// Code generated by gobind. DO NOT EDIT.

// Java class mobileproxy.LogWriter is a proxy for talking to a Go program.
//
//   autogenerated by gobind -lang=java github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
package mobileproxy;

import go.Seq;

/**
 * LogWriter is used as a sink for logging.
 */
public interface LogWriter {
	public long writeString(String s) throws Exception;
	
}

StreamDialer.java:

// Code generated by gobind. DO NOT EDIT.

// Java class mobileproxy.StreamDialer is a proxy for talking to a Go program.
//
//   autogenerated by gobind -lang=java github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
package mobileproxy;

import go.Seq;

/**
 * StreamDialer encapsulates the logic to create stream connections (like TCP).
 */
public final class StreamDialer implements Seq.Proxy {
	static { Mobileproxy.touch(); }
	
	private final int refnum;
	
	@Override public final int incRefnum() {
	      Seq.incGoRef(refnum, this);
	      return refnum;
	}
	
	/**
	 * NewStreamDialerFromConfig creates a [StreamDialer] based on the given config.
	The config format is specified in https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config#hdr-Config_Format.
	 */
	public StreamDialer(String transportConfig) {
		this.refnum = __NewStreamDialerFromConfig(transportConfig);
		Seq.trackGoRef(refnum, this);
	}
	
	private static native int __NewStreamDialerFromConfig(String transportConfig);
	
	StreamDialer(int refnum) { this.refnum = refnum; Seq.trackGoRef(refnum, this); }
	
	// skipped field StreamDialer.StreamDialer with unsupported type: github.com/Jigsaw-Code/outline-sdk/transport.StreamDialer
	
	// skipped method StreamDialer.DialStream with unsupported parameter or return types
	
	@Override public boolean equals(Object o) {
		if (o == null || !(o instanceof StreamDialer)) {
		    return false;
		}
		StreamDialer that = (StreamDialer)o;
		// skipped field StreamDialer.StreamDialer with unsupported type: github.com/Jigsaw-Code/outline-sdk/transport.StreamDialer
		
		return true;
	}
	
	@Override public int hashCode() {
	    return java.util.Arrays.hashCode(new Object[] {});
	}
	
	@Override public String toString() {
		StringBuilder b = new StringBuilder();
		b.append("StreamDialer").append("{");
		return b.append("}").toString();
	}
}

Mobileproxy.java:

// Code generated by gobind. DO NOT EDIT.

// Java class mobileproxy.Mobileproxy is a proxy for talking to a Go program.
//
//   autogenerated by gobind -lang=java github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
package mobileproxy;

import go.Seq;

public abstract class Mobileproxy {
	static {
		Seq.touch(); // for loading the native library
		_init();
	}
	
	private Mobileproxy() {} // uninstantiable
	
	// touch is called from other bound packages to initialize this package
	public static void touch() {}
	
	private static native void _init();
	
	private static final class proxyLogWriter implements Seq.Proxy, LogWriter {
		private final int refnum;
		
		@Override public final int incRefnum() {
		      Seq.incGoRef(refnum, this);
		      return refnum;
		}
		
		proxyLogWriter(int refnum) { this.refnum = refnum; Seq.trackGoRef(refnum, this); }
		
		public native long writeString(String s) throws Exception;
	}
	
	
	/**
	 * NewListFromLines creates a StringList by splitting the input string on new lines.
	 */
	public static native StringList newListFromLines(String lines);
	/**
	 * NewSmartStreamDialer automatically selects a DNS and TLS strategy to use, and return a [StreamDialer]
	that will use the selected strategy.
	It uses testDomain to find a strategy that works when accessing those domains.
	The strategies to search are given in the searchConfig. An example can be found in
	https://github.com/Jigsaw-Code/outline-sdk/x/examples/smart-proxy/config.json
	 */
	public static native StreamDialer newSmartStreamDialer(StringList testDomains, String searchConfig, LogWriter logWriter) throws Exception;
	/**
	 * NewStderrLogWriter creates a [LogWriter] that writes to the standard error output.
	 */
	public static native LogWriter newStderrLogWriter();
	/**
	 * NewStreamDialerFromConfig creates a [StreamDialer] based on the given config.
	The config format is specified in https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config#hdr-Config_Format.
	 */
	public static native StreamDialer newStreamDialerFromConfig(String transportConfig) throws Exception;
	/**
	 * RunProxy runs a local web proxy that listens on localAddress, and handles proxy requests by
	establishing connections to requested destination using the [StreamDialer].
	 */
	public static native Proxy runProxy(String localAddress, StreamDialer dialer) throws Exception;
}

Proxy.java:

// Code generated by gobind. DO NOT EDIT.

// Java class mobileproxy.Proxy is a proxy for talking to a Go program.
//
//   autogenerated by gobind -lang=java github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
package mobileproxy;

import go.Seq;

/**
 * Proxy enables you to get the actual address bound by the server and stop the service when no longer needed.
 */
public final class Proxy implements Seq.Proxy {
	static { Mobileproxy.touch(); }
	
	private final int refnum;
	
	@Override public final int incRefnum() {
	      Seq.incGoRef(refnum, this);
	      return refnum;
	}
	
	Proxy(int refnum) { this.refnum = refnum; Seq.trackGoRef(refnum, this); }
	
	public Proxy() { this.refnum = __New(); Seq.trackGoRef(refnum, this); }
	
	private static native int __New();
	
	/**
	 * Address returns the IP and port the server is bound to.
	 */
	public native String address();
	/**
	 * Host returns the IP the server is bound to.
	 */
	public native String host();
	/**
	 * Port returns the port the server is bound to.
	 */
	public native long port();
	/**
	 * Stop gracefully stops the proxy service, waiting for at most timeout seconds before forcefully closing it.
	The function takes a timeoutSeconds number instead of a [time.Duration] so it&#39;s compatible with Go Mobile.
	 */
	public native void stop(long timeoutSeconds);
	@Override public boolean equals(Object o) {
		if (o == null || !(o instanceof Proxy)) {
		    return false;
		}
		Proxy that = (Proxy)o;
		return true;
	}
	
	@Override public int hashCode() {
	    return java.util.Arrays.hashCode(new Object[] {});
	}
	
	@Override public String toString() {
		StringBuilder b = new StringBuilder();
		b.append("Proxy").append("{");
		return b.append("}").toString();
	}
}

StringList.java:

// Code generated by gobind. DO NOT EDIT.

// Java class mobileproxy.StringList is a proxy for talking to a Go program.
//
//   autogenerated by gobind -lang=java github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
package mobileproxy;

import go.Seq;

/**
 * StringList allows us to pass a list of strings to the Go Mobile functions, since Go Mobiule doesn&#39;t
support slices as parameters.
 */
public final class StringList implements Seq.Proxy {
	static { Mobileproxy.touch(); }
	
	private final int refnum;
	
	@Override public final int incRefnum() {
	      Seq.incGoRef(refnum, this);
	      return refnum;
	}
	
	StringList(int refnum) { this.refnum = refnum; Seq.trackGoRef(refnum, this); }
	
	public StringList() { this.refnum = __New(); Seq.trackGoRef(refnum, this); }
	
	private static native int __New();
	
	/**
	 * Append adds the string value to the end of the list.
	 */
	public native void append(String value);
	@Override public boolean equals(Object o) {
		if (o == null || !(o instanceof StringList)) {
		    return false;
		}
		StringList that = (StringList)o;
		return true;
	}
	
	@Override public int hashCode() {
	    return java.util.Arrays.hashCode(new Object[] {});
	}
	
	@Override public String toString() {
		StringBuilder b = new StringBuilder();
		b.append("StringList").append("{");
		return b.append("}").toString();
	}
}

Add the library to your mobile project

To add the library to your mobile project, see Go Mobile's Building and deploying to iOS and Building and deploying to Android.

Using the basic local proxy forwarder

You need to call the RunProxy function passing the local address to use, and the transport configuration.

On Android, you can have the following Kotlin code:

// Use port zero to let the system pick an open port for you.
val dialer = StreamDialer("split:3")

val proxy = Mobileproxy.runProxy("localhost:0", dialer)
// Configure your networking library using proxy.host() and proxy.port() or proxy.address().
// ...
// Stop running the proxy.
proxy.stop()

Using the smart local proxy forwarder ("Smart Proxy")

The Smart Proxy can automatically try multiple strategies to unblock access to the test domains you specify. You need to specify a strategy config in JSON format (example).

On Android, the Kotlin code would look like this:

// Use port zero to let the system pick an open port for you.
val testDomains = Mobileproxy.newListFromLines("www.youtube.com\ni.ytimg.com")
val strategiesConfig = "..."  // Config JSON.
val dialer = Mobileproxy.newSmartStreamDialer(testDomains, strategiesConfig, Mobileproxy.newStderrLogWriter())

val proxy = Mobileproxy.runProxy("localhost:0", dialer)
// Configure your networking library using proxy.host() and proxy.port() or proxy.address().
// ...
// Stop running the proxy.
proxy.stop()

Configure your HTTP client or networking library

You need to configure your networking library to use the local proxy. How you do it depends on the networking library you are using.

Dart/Flutter HttpClient

Set the proxy with the HttpClient.findProxy function.

Dart example:

  HttpClient client = HttpClient();
  client.findProxy = (Uri uri) {
    return "PROXY " + proxy.address();
  };

OkHttp (Android only)

Set the proxy with OkHttpClient.Builder.proxy.

Kotlin example:

val proxyConfig = Proxy(Proxy.Type.HTTP, InetSocketAddress(proxy.host(), proxy.port()))
val client = OkHttpClient.Builder().proxy(proxyConfig).build()

JVM (Java, Kotlin)

In the JVM, you can configure the proxy to use with system properties:

System.setProperty("http.proxyHost", proxy.host())
System.setProperty("http.proxyPort", String.valueOf(proxy.port()))
System.setProperty("https.proxyHost", proxy.host())
System.setProperty("https.proxyPort", String.valueOf(proxy.port()))

Note that this may not fully work on Android, since it will only affect the JVM, not native code. You should also make sure you set this early in your code.

Web View

Android

On Android, you can easily apply a proxy configuration to all the web views in your application with the androidx.webview library like so:

ProxyController.getInstance()
		.setProxyOverride(
				ProxyConfig.Builder()
						.addProxyRule(this.proxy!!.address())
						.build(),
				{}, // execution context for the following callback - do anything needed here once the proxy is applied, like refreshing web views 
				{} // callback to be called once the ProxyConfig is applied
		)

iOS

As of iOS 17, you can add a proxy configuration to a WKWebView via its WKWebsiteDataStore property.

let configuration = WKWebViewConfiguration()

let endpoint = NWEndpoint.hostPort(
		host: NWEndpoint.Host(proxyHost),
		port: NWEndpoint.Port(proxyPort)!
)
let proxyConfig = ProxyConfiguration.init(httpCONNECTProxy: endpoint)

let websiteDataStore = WKWebsiteDataStore.default()
websiteDataStore.proxyConfigurations = [proxyConfig]

// Other webview configuration options... see https://developer.apple.com/documentation/webkit/wkwebviewconfiguration

let webview = WKWebView(
	configuration: configuration,
)

// use this webview as you would normally!

Clean up

rm -rf ./out/