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: salt prefix support #1454

Merged
merged 16 commits into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added app-debug.apk
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ parcelable ShadowsocksConfig {
int port;
String password;
String method;
@nullable byte[] prefix;
fortuna marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import java.util.logging.Logger;
import tun2socks.OutlineTunnel;
import tun2socks.Tun2socks;
import org.outline.shadowsocks.ShadowsocksConfig;


/**
* Manages the life-cycle of the system VPN, and of the tunnel that processes its traffic.
Expand Down Expand Up @@ -120,20 +122,17 @@ public synchronized void tearDownVpn() {
/**
* Connects a tunnel between a Shadowsocks proxy server and the VPN TUN interface.
*
* @param host is IP address of the SOCKS proxy server.
* @param port is the port of the SOCKS proxy server.
* @param password is the password of the Shadowsocks proxy.
* @param cipher is the encryption cipher used by the Shadowsocks proxy.
* @param client provides access to the Shadowsocks proxy.
* @param isUdpEnabled conveys the result of UDP probing. TODO: Roll this into `client`.
* @throws IllegalArgumentException if |socksServerAddress| is null.
* @throws IllegalStateException if the VPN has not been established, or the tunnel is already
* connected.
* @throws Exception when the tunnel fails to connect.
*/
public synchronized void connectTunnel(final String host, int port, final String password,
final String cipher, boolean isUdpEnabled) throws Exception {
public synchronized void connectTunnel(final shadowsocks.Client client, boolean isUdpEnabled) throws Exception {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
LOG.info("Connecting the tunnel.");
if (host == null || port <= 0 || port > 65535 || password == null || cipher == null) {
throw new IllegalArgumentException("Must provide valid Shadowsocks proxy parameters.");
if (client == null) {
throw new IllegalArgumentException("Must provide a Shadowsocks client.");
}
if (tunFd == null) {
throw new IllegalStateException("Must establish the VPN before connecting the tunnel.");
Expand All @@ -143,8 +142,7 @@ public synchronized void connectTunnel(final String host, int port, final String
}

LOG.fine("Starting tun2socks...");
tunnel = Tun2socks.connectShadowsocksTunnel(
tunFd.getFd(), host, port, password, cipher, isUdpEnabled);
tunnel = Tun2socks.connectShadowsocksTunnel(tunFd.getFd(), client, isUdpEnabled);
}

/* Disconnects a tunnel created by a previous call to |connectTunnel|. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,29 @@ public static TunnelConfig makeTunnelConfig(final String tunnelId, final JSONObj
tunnelConfig.proxy.port = config.getInt("port");
tunnelConfig.proxy.password = config.getString("password");
tunnelConfig.proxy.method = config.getString("method");
// `name` and `prefix` are optional properties.
try {
// `name` is an optional property; don't throw if it fails to parse.
tunnelConfig.name = config.getString("name");
} catch (JSONException e) {
LOG.fine("Tunnel config missing name");
}
String prefix = null;
try {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
prefix = config.getString("prefix");
LOG.fine("Activating experimental prefix support");
} catch (JSONException e) {
// pass
}
if (prefix != null) {
tunnelConfig.proxy.prefix = new byte[prefix.length()];
for (int i = 0; i < prefix.length(); i++) {
char c = prefix.charAt(i);
if ((c & 0xFF) != c) {
throw new JSONException(String.format("Prefix character '%c' is out of range", c));
}
tunnelConfig.proxy.prefix[i] = (byte)c;
}
}
return tunnelConfig;
}

Expand Down Expand Up @@ -201,13 +218,27 @@ private synchronized OutlinePlugin.ErrorCode startTunnel(
}
}

final ShadowsocksConfig proxyConfig = config.proxy;
final shadowsocks.Config configCopy = new shadowsocks.Config();
configCopy.setHost(config.proxy.host);
configCopy.setPort(config.proxy.port);
configCopy.setCipherName(config.proxy.method);
configCopy.setPassword(config.proxy.password);
configCopy.setPrefix(config.proxy.prefix);
final shadowsocks.Client client;
try {
client = new shadowsocks.Client(configCopy);
fortuna marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception e) {
LOG.log(Level.WARNING, "Invalid configuration", e);
tearDownActiveTunnel();
return OutlinePlugin.ErrorCode.ILLEGAL_SERVER_CONFIGURATION;
fortuna marked this conversation as resolved.
Show resolved Hide resolved
}

OutlinePlugin.ErrorCode errorCode = OutlinePlugin.ErrorCode.NO_ERROR;
if (!isAutoStart) {
try {
// Do not perform connectivity checks when connecting on startup. We should avoid failing
// the connection due to a network error, as network may not be ready.
errorCode = checkServerConnectivity(proxyConfig);
errorCode = checkServerConnectivity(client);
if (!(errorCode == OutlinePlugin.ErrorCode.NO_ERROR
|| errorCode == OutlinePlugin.ErrorCode.UDP_RELAY_NOT_ENABLED)) {
tearDownActiveTunnel();
Expand All @@ -233,8 +264,7 @@ private synchronized OutlinePlugin.ErrorCode startTunnel(
final boolean remoteUdpForwardingEnabled =
isAutoStart ? tunnelStore.isUdpSupported() : errorCode == OutlinePlugin.ErrorCode.NO_ERROR;
try {
vpnTunnel.connectTunnel(proxyConfig.host, proxyConfig.port, proxyConfig.password,
proxyConfig.method, remoteUdpForwardingEnabled);
vpnTunnel.connectTunnel(client, remoteUdpForwardingEnabled);
fortuna marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception e) {
LOG.log(Level.SEVERE, "Failed to connect the tunnel", e);
tearDownActiveTunnel();
Expand Down Expand Up @@ -286,10 +316,9 @@ private void stopVpnTunnel() {

// Shadowsocks

private OutlinePlugin.ErrorCode checkServerConnectivity(final ShadowsocksConfig config) {
private OutlinePlugin.ErrorCode checkServerConnectivity(final shadowsocks.Client client) {
try {
long errorCode = shadowsocks.Shadowsocks.checkConnectivity(
config.host, config.port, config.password, config.method);
long errorCode = Shadowsocks.checkConnectivity(client);
OutlinePlugin.ErrorCode result = OutlinePlugin.ErrorCode.values()[(int) errorCode];
LOG.info(String.format(Locale.ROOT, "Go connectivity check result: %s", result.name()));
return result;
Expand Down Expand Up @@ -429,6 +458,16 @@ private void storeActiveTunnel(final TunnelConfig config, boolean isUdpSupported
proxyConfig.put("port", config.proxy.port);
proxyConfig.put("password", config.proxy.password);
proxyConfig.put("method", config.proxy.method);

if (config.proxy.prefix != null) {
char[] chars = new char[config.proxy.prefix.length];
for (int i = 0; i < config.proxy.prefix.length; i++) {
// Unsigned bit width extension requires a mask in Java.
chars[i] = (char)(config.proxy.prefix[i] & 0xFF);
fortuna marked this conversation as resolved.
Show resolved Hide resolved
}
proxyConfig.put("prefix", new String(chars));
}

tunnel.put(TUNNEL_ID_KEY, config.id).put(TUNNEL_CONFIG_KEY, proxyConfig);
tunnelStore.save(tunnel);
} catch (JSONException e) {
Expand Down
9 changes: 8 additions & 1 deletion cordova-plugin-outline/apple/src/OutlineTunnel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ class OutlineTunnel: NSObject, Codable {
var port: String?
var method: String?
var password: String?
var prefix: Data?
var config: [String: String] {
let scalars = prefix?.map{Unicode.Scalar($0)}
let characters = scalars?.map{Character($0)}
let prefixStr = String(characters ?? [])
return ["host": host ?? "", "port": port ?? "", "password": password ?? "",
"method": method ?? ""]
"method": method ?? "", "prefix": prefixStr]
}

@objc
Expand All @@ -44,6 +48,9 @@ class OutlineTunnel: NSObject, Codable {
if let port = config["port"] {
self.port = String(describing: port) // Handle numeric values
}
if let prefix = config["prefix"] as? String {
self.prefix = Data(prefix.utf16.map{UInt8($0)})
}
}

func encode() -> Data? {
Expand Down
48 changes: 38 additions & 10 deletions cordova-plugin-outline/apple/vpn/PacketTunnelProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,13 @@ - (void)startTunnelWithOptions:(NSDictionary *)options
// culprit and can explicitly disconnect.
long errorCode = noError;
if (!isOnDemand) {
ShadowsocksCheckConnectivity(self.hostNetworkAddress, [self.tunnelConfig.port intValue],
self.tunnelConfig.password, self.tunnelConfig.method, &errorCode,
nil);
ShadowsocksClient* client = [self getClient];
if (client == nil) {
return completionHandler([NSError errorWithDomain:NEVPNErrorDomain
code:NEVPNErrorConfigurationInvalid
userInfo:nil]);
}
ShadowsocksCheckConnectivity(client, &errorCode, nil);
}
if (errorCode != noError && errorCode != udpRelayNotEnabled) {
[self execAppCallbackForAction:kActionStart errorCode:errorCode];
Expand Down Expand Up @@ -264,6 +268,21 @@ - (OutlineTunnel *)retrieveTunnelConfig:(NSDictionary *)config {

# pragma mark - Network

- (ShadowsocksClient*) getClient {
ShadowsocksConfig* config = [ShadowsocksConfig init];
config.host = self.hostNetworkAddress;
config.port = [self.tunnelConfig.port intValue];
config.password = self.tunnelConfig.password;
config.cipherName = self.tunnelConfig.method;
config.prefix = self.tunnelConfig.prefix;
NSError *err;
ShadowsocksClient* client = ShadowsocksNewClient(config, &err);
if (err != nil) {
DDLogInfo(@"Failed to construct client.");
}
return client;
}

- (void)connectTunnel:(NEPacketTunnelNetworkSettings *)settings
completion:(void (^)(NSError *))completionHandler {
__weak PacketTunnelProvider *weakSelf = self;
Expand Down Expand Up @@ -492,10 +511,16 @@ - (void)reconnectTunnel:(bool)configChanged {

DDLogInfo(@"Configuration or host IP address changed with the network. Reconnecting tunnel.");
self.hostNetworkAddress = activeHostNetworkAddress;
ShadowsocksClient* client = [self getClient];
if (client == nil) {
[self execAppCallbackForAction:kActionStart errorCode:illegalServerConfiguration];
[self cancelTunnelWithError:[NSError errorWithDomain:NEVPNErrorDomain
code:NEVPNErrorConfigurationInvalid
userInfo:nil]];
return;
}
long errorCode = noError;
ShadowsocksCheckConnectivity(self.hostNetworkAddress, [self.tunnelConfig.port intValue],
self.tunnelConfig.password, self.tunnelConfig.method, &errorCode,
nil);
ShadowsocksCheckConnectivity(client, &errorCode, nil);
if (errorCode != noError && errorCode != udpRelayNotEnabled) {
DDLogError(@"Connectivity checks failed. Tearing down VPN");
[self execAppCallbackForAction:kActionStart errorCode:errorCode];
Expand Down Expand Up @@ -556,10 +581,13 @@ - (BOOL)startTun2Socks:(BOOL)isUdpSupported {
[self.tunnel disconnect];
}
__weak PacketTunnelProvider *weakSelf = self;
NSError *err;
ShadowsocksClient* client = [self getClient];
if (client == nil) {
return NO;
}
NSError* err;
self.tunnel = Tun2socksConnectShadowsocksTunnel(
weakSelf, self.hostNetworkAddress, [self.tunnelConfig.port intValue],
self.tunnelConfig.password, self.tunnelConfig.method, isUdpSupported, &err);
weakSelf, client, isUdpSupported, &err);
if (err != nil) {
DDLogError(@"Failed to start tun2socks: %@", err);
return NO;
Expand Down Expand Up @@ -590,4 +618,4 @@ - (void)execAppCallbackForAction:(NSString *)action errorCode:(ErrorCode)code {
}
}

@end
@end
5 changes: 4 additions & 1 deletion src/electron/go_vpn_tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export class GoVpnTunnel implements VpnTunnel {
// Don't await here because we want to launch both binaries
this.tun2socks.start(this.isUdpEnabled);

console.log('starting routing daemon');
await this.routing.start();
}

Expand Down Expand Up @@ -228,7 +229,8 @@ class GoTun2socks {
// -tunName outline-tap0 -tunDNS 1.1.1.1,9.9.9.9 \
// -tunAddr 10.0.85.2 -tunGw 10.0.85.1 -tunMask 255.255.255.0 \
// -proxyHost 127.0.0.1 -proxyPort 1080 -proxyPassword mypassword \
// -proxyCipher chacha20-ietf-poly1035 [-dnsFallback] [-checkConnectivity]
// -proxyCipher chacha20-ietf-poly1035
// [-dnsFallback] [-checkConnectivity] [-proxyPrefix]
const args: string[] = [];
args.push('-tunName', TUN2SOCKS_TAP_DEVICE_NAME);
args.push('-tunAddr', TUN2SOCKS_TAP_DEVICE_IP);
Expand All @@ -239,6 +241,7 @@ class GoTun2socks {
args.push('-proxyPort', `${this.config.port}`);
args.push('-proxyPassword', this.config.password || '');
args.push('-proxyCipher', this.config.method || '');
args.push('-proxyPrefix', this.config.prefix || '');
args.push('-logLevel', this.process.isDebugModeEnabled ? 'debug' : 'info');
if (!isUdpEnabled) {
args.push('-dnsFallback');
Expand Down
3 changes: 2 additions & 1 deletion src/electron/routing_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ export class RoutingDaemon {
);
}));

const initialErrorHandler = () => {
const initialErrorHandler = (err: Error) => {
console.error('Routing daemon socket setup failed', err);
this.socket = null;
reject(new SystemConfigurationException('routing daemon is not running'));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function staticKeyToShadowsocksSessionConfig(staticKey: string): Shadowso
port: config.port.data,
method: config.method.data,
password: config.password.data,
prefix: config.extra['prefix'],
};
} catch (error) {
throw new errors.ServerAccessKeyInvalid(error.message || 'Failed to parse static access key.');
Expand All @@ -38,19 +39,21 @@ export function staticKeyToShadowsocksSessionConfig(staticKey: string): Shadowso
function parseShadowsocksSessionConfigJson(maybeJsonText: string): ShadowsocksSessionConfig | null {
let sessionConfig;
try {
const {method, password, server: host, server_port: port} = JSON.parse(maybeJsonText);
const {method, password, server: host, server_port: port, extra} = JSON.parse(maybeJsonText);
fortuna marked this conversation as resolved.
Show resolved Hide resolved

sessionConfig = {
method,
password,
host,
port,
prefix: extra['prefix'],
};
} catch (_) {
// It's not JSON, so return null.
return null;
}

// These are the mandatory keys.
for (const key of ['method', 'password', 'host', 'port']) {
if (sessionConfig && !sessionConfig[key]) {
throw new errors.ServerAccessKeyInvalid(
Expand Down
8 changes: 7 additions & 1 deletion src/www/app/outline_server_repository/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ function staticKeysMatch(a: string, b: string): boolean {
try {
const l = staticKeyToShadowsocksSessionConfig(a);
const r = staticKeyToShadowsocksSessionConfig(b);
return l.host === r.host && l.port === r.port && l.password === r.password && l.method === r.method;
return (
l.host === r.host &&
l.port === r.port &&
l.password === r.password &&
l.method === r.method &&
l.prefix == r.prefix
fortuna marked this conversation as resolved.
Show resolved Hide resolved
);
} catch (e) {
console.debug(`failed to parse access key for comparison`);
}
Expand Down
1 change: 1 addition & 0 deletions src/www/app/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ShadowsocksSessionConfig {
port?: number;
password?: string;
method?: string;
prefix?: string;
}

export const enum TunnelStatus {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file modified third_party/outline-go-tun2socks/android/jni/x86/libgojni.so
Binary file not shown.
Binary file not shown.
Binary file modified third_party/outline-go-tun2socks/android/tun2socks.aar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
<array>
<dict>
<key>LibraryIdentifier</key>
<string>macos-arm64_x86_64</string>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>Tun2socks.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
<string>ios</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
Expand All @@ -34,15 +33,16 @@
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>macos-arm64_x86_64</string>
<key>LibraryPath</key>
<string>Tun2socks.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<string>macos</string>
</dict>
</array>
<key>CFBundlePackageType</key>
Expand Down