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:
The integration typically consists of the following steps:
- Build the mobile native library using Go Mobile.
- Add the library to your application.
- Configure and run MobileProxy within your app.
- 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'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'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();
}
}
To add the library to your mobile project, see Go Mobile's Building and deploying to iOS and Building and deploying to Android.
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()
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()
You need to configure your networking library to use the local proxy. How you do it depends on the networking library you are using.
Set the proxy with the HttpClient.findProxy
function.
Dart example:
HttpClient client = HttpClient();
client.findProxy = (Uri uri) {
return "PROXY " + proxy.address();
};
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()
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.
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
)
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!
rm -rf ./out/