Skip to content

Commit

Permalink
Merge branch 'master' into use-native-process-kill
Browse files Browse the repository at this point in the history
  • Loading branch information
jyyi1 authored Nov 16, 2024
2 parents 93cc038 + 4f62943 commit 911661b
Show file tree
Hide file tree
Showing 34 changed files with 1,068 additions and 551 deletions.
2 changes: 2 additions & 0 deletions client/electron/app_paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const isWindows = os.platform() === 'win32';
/**
* Get the unpacked asar folder path.
* - For AppImage, `/tmp/.mount_OutlinXXXXXX/resources/app.asar.unpacked/`
* - For Debian, `/opt/Outline/resources/app.asar.unpacked`
* - For Windows, `C:\Program Files (x86)\Outline\`
* @returns A string representing the path of the unpacked asar folder.
*/
Expand All @@ -32,6 +33,7 @@ function unpackedAppPath() {
/**
* Get the parent directory path of the current application binary.
* - For AppImage, `/tmp/.mount_OutlinXXXXX/resources/app.asar`
* - For Debian, `/opt/Outline/resources/app.asar`
* - For Windows, `C:\Program Files (x86)\Outline\`
* @returns A string representing the path of the application directory.
*/
Expand Down
25 changes: 25 additions & 0 deletions client/electron/debian/after_install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash

# Copyright 2024 The Outline Authors
#
# 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
#
# http://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.

# Dependencies:
# - libcap2-bin: setcap

set -eux

# Grant specific capabilities so Outline can run without root permisssion
# - cap_net_admin: configure network interfaces, set up routing tables, etc.
# - cap_dac_override: modify network configuration files owned by root
/usr/sbin/setcap cap_net_admin,cap_dac_override+eip /opt/Outline/Outline
19 changes: 17 additions & 2 deletions client/electron/electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"output": "output/client/electron/build"
},
"extraMetadata": {
"name": "outline-client",
"main": "output/client/electron/index.js"
},
"files": [
Expand All @@ -15,21 +16,35 @@
"output/client/electron",
"!output/client/electron/build"
],

"deb": {
"depends": [
"gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3",
"libcap2-bin"
],
"afterInstall": "client/electron/debian/after_install.sh"
},
"linux": {
"category": "Network",
"executableName": "Outline",
"files": [
"client/electron/linux_proxy_controller/dist",
"client/electron/icons/png",
"client/output/build/linux"
],
"icon": "client/electron/icons/png",
"target": {
"maintainer": "Jigsaw LLC",
"target": [{
"arch": [
"x64"
],
"target": "AppImage"
}
}, {
"arch": "x64",
"target": "deb"
}]
},

"nsis": {
"include": "client/electron/custom_install_steps.nsh",
"perMachine": true
Expand Down
78 changes: 78 additions & 0 deletions client/electron/go_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2024 The Outline Authors
//
// 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
//
// http://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.

/**
* @file Contains helper functions implemented in Go.
* This file provides a communication channel between TypeScript and Go,
* allowing users to call Go functions from TypeScript.
*/

import {pathToEmbeddedTun2socksBinary} from './app_paths';
import {ChildProcessHelper} from './process';
import {TransportConfigJson} from '../src/www/app/outline_server_repository/config';

/**
* Verifies the UDP connectivity of the server specified in `config`.
* Checks whether proxy server is reachable, whether the network and proxy support UDP forwarding
* and validates the proxy credentials.
*
* @param config The transport configuration in JSON.
* @param debugMode Optional. Whether to forward logs to stdout. Defaults to false.
* @returns A boolean indicating whether UDP forwarding is supported.
* @throws Error if TCP connection cannot be established.
* @throws ProcessTerminatedExitCodeError if tun2socks failed to run.
*/
export async function checkUDPConnectivity(
config: TransportConfigJson,
debugMode: boolean = false
): Promise<boolean> {
const tun2socks = new ChildProcessHelper(pathToEmbeddedTun2socksBinary());
tun2socks.isDebugModeEnabled = debugMode;

console.debug('[tun2socks] - checking connectivity ...');
const output = await tun2socks.launch([
'-transport',
JSON.stringify(config),
'-checkConnectivity',
]);

// Only parse the first line, because sometimes Windows Crypto API adds warnings to stdout.
const outObj = JSON.parse(output.split('\n')[0]);
if (outObj.tcp) {
throw new Error(outObj.tcp);
}
if (outObj.udp) {
return false;
}
return true;
}

/**
* Fetches a resource from the given URL.
*
* @param url The URL of the resource to fetch.
* @param debugMode Optional. Whether to forward logs to stdout. Defaults to false.
* @returns A Promise that resolves to the fetched content as a string.
* @throws ProcessTerminatedExitCodeError if tun2socks failed to run.
*/
export function fetchResource(
url: string,
debugMode: boolean = false
): Promise<string> {
const tun2socks = new ChildProcessHelper(pathToEmbeddedTun2socksBinary());
tun2socks.isDebugModeEnabled = debugMode;

console.debug('[tun2socks] - fetching resource ...');
return tun2socks.launch(['-fetchUrl', url]);
}
74 changes: 23 additions & 51 deletions client/electron/go_vpn_tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ import {platform} from 'os';
import {powerMonitor} from 'electron';

import {pathToEmbeddedTun2socksBinary} from './app_paths';
import {checkUDPConnectivity} from './go_helpers';
import {ChildProcessHelper, ProcessTerminatedSignalError} from './process';
import {RoutingDaemon} from './routing_service';
import {VpnTunnel} from './vpn_tunnel';
import {
TransportConfigJson,
TunnelStatus,
} from '../src/www/app/outline_server_repository/vpn';
import {TransportConfigJson} from '../src/www/app/outline_server_repository/config';
import {TunnelStatus} from '../src/www/app/outline_server_repository/vpn';

const isLinux = platform() === 'linux';
const isWindows = platform() === 'win32';
Expand All @@ -49,7 +48,7 @@ const DNS_RESOLVERS = ['1.1.1.1', '9.9.9.9'];
// about the others.
export class GoVpnTunnel implements VpnTunnel {
private readonly tun2socks: GoTun2socks;
private readonly connectivityChecker: GoTun2socks;
private isDebugMode = false;

// See #resumeListener.
private disconnected = false;
Expand All @@ -67,8 +66,7 @@ export class GoVpnTunnel implements VpnTunnel {
private readonly routing: RoutingDaemon,
readonly transportConfig: TransportConfigJson
) {
this.tun2socks = new GoTun2socks(transportConfig);
this.connectivityChecker = new GoTun2socks(transportConfig);
this.tun2socks = new GoTun2socks();

// This promise, tied to both helper process' exits, is key to the instance's
// lifecycle:
Expand All @@ -85,8 +83,8 @@ export class GoVpnTunnel implements VpnTunnel {
// Turns on verbose logging for the managed processes. Must be called before launching the
// processes
enableDebugMode() {
this.isDebugMode = true;
this.tun2socks.enableDebugMode();
this.connectivityChecker.enableDebugMode();
}

// Fulfills once all three helpers have started successfully.
Expand All @@ -104,13 +102,16 @@ export class GoVpnTunnel implements VpnTunnel {
});

if (checkProxyConnectivity) {
this.isUdpEnabled = await checkConnectivity(this.connectivityChecker);
this.isUdpEnabled = await checkUDPConnectivity(
this.transportConfig,
this.isDebugMode
);
}
console.log(`UDP support: ${this.isUdpEnabled}`);

console.log('starting routing daemon');
await Promise.all([
this.tun2socks.start(this.isUdpEnabled),
this.tun2socks.start(this.transportConfig, this.isUdpEnabled),
this.routing.start(),
]);
}
Expand Down Expand Up @@ -152,15 +153,18 @@ export class GoVpnTunnel implements VpnTunnel {

console.log('restarting tun2socks after resume');
await Promise.all([
this.tun2socks.start(this.isUdpEnabled),
this.tun2socks.start(this.transportConfig, this.isUdpEnabled),
this.updateUdpSupport(), // Check if UDP support has changed; if so, silently restart.
]);
}

private async updateUdpSupport() {
const wasUdpEnabled = this.isUdpEnabled;
try {
this.isUdpEnabled = await checkConnectivity(this.connectivityChecker);
this.isUdpEnabled = await checkUDPConnectivity(
this.transportConfig,
this.isDebugMode
);
} catch (e) {
console.error(`connectivity check failed: ${e}`);
return;
Expand All @@ -173,7 +177,7 @@ export class GoVpnTunnel implements VpnTunnel {

// Restart tun2socks.
await this.tun2socks.stop();
await this.tun2socks.start(this.isUdpEnabled);
await this.tun2socks.start(this.transportConfig, this.isUdpEnabled);
}

// Use #onceDisconnected to be notified when the tunnel terminates.
Expand Down Expand Up @@ -234,7 +238,7 @@ class GoTun2socks {
private stopRequested = false;
private readonly process: ChildProcessHelper;

constructor(private readonly transportConfig: TransportConfigJson) {
constructor() {
this.process = new ChildProcessHelper(pathToEmbeddedTun2socksBinary());
}

Expand All @@ -244,7 +248,10 @@ class GoTun2socks {
* Otherwise, an error containing a JSON-formatted message will be thrown.
* @param isUdpEnabled Indicates whether the remote Outline server supports UDP.
*/
async start(isUdpEnabled: boolean): Promise<void> {
async start(
config: TransportConfigJson,
isUdpEnabled: boolean
): Promise<void> {
// ./tun2socks.exe \
// -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 \
Expand All @@ -256,7 +263,7 @@ class GoTun2socks {
args.push('-tunGw', TUN2SOCKS_VIRTUAL_ROUTER_IP);
args.push('-tunMask', TUN2SOCKS_VIRTUAL_ROUTER_NETMASK);
args.push('-tunDNS', DNS_RESOLVERS.join(','));
args.push('-transport', JSON.stringify(this.transportConfig));
args.push('-transport', JSON.stringify(config));
args.push('-logLevel', this.process.isDebugModeEnabled ? 'debug' : 'info');
if (!isUdpEnabled) {
args.push('-dnsFallback');
Expand Down Expand Up @@ -309,42 +316,7 @@ class GoTun2socks {
return this.process.stop();
}

/**
* Checks connectivity and exits with the string of stdout.
*
* @throws ProcessTerminatedExitCodeError if tun2socks failed to run successfully.
*/
checkConnectivity() {
console.debug('[tun2socks] - checking connectivity ...');
return this.process.launch([
'-transport',
JSON.stringify(this.transportConfig),
'-checkConnectivity',
]);
}

enableDebugMode() {
this.process.isDebugModeEnabled = true;
}
}

/**
* Leverages the GoTun2socks binary to check connectivity to the server specified in `config`.
* Checks whether proxy server is reachable, whether the network and proxy support UDP forwarding
* and validates the proxy credentials.
*
* @returns A boolean indicating whether UDP forwarding is supported.
* @throws Error if the server is not reachable or if the process fails to start.
*/
async function checkConnectivity(tun2socks: GoTun2socks) {
const output = await tun2socks.checkConnectivity();
// Only parse the first line, because sometimes Windows Crypto API adds warnings to stdout.
const outObj = JSON.parse(output.split('\n')[0]);
if (outObj.tcp) {
throw new Error(outObj.tcp);
}
if (outObj.udp) {
return false;
}
return true;
}
Loading

0 comments on commit 911661b

Please sign in to comment.