Skip to content

Commit

Permalink
refactor(client/windows): move fetching logic from TypeScript to Go (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jyyi1 authored Nov 1, 2024
1 parent cf00b35 commit b8ac24a
Show file tree
Hide file tree
Showing 15 changed files with 417 additions and 71 deletions.
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/vpn';

/**
* 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]);
}
68 changes: 21 additions & 47 deletions client/electron/go_vpn_tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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';
Expand Down Expand Up @@ -49,7 +50,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 +68,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 +85,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 +104,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 +155,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 +179,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 +240,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 +250,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 +265,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 +318,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;
}
11 changes: 10 additions & 1 deletion client/electron/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import {autoUpdater} from 'electron-updater';

import {lookupIp} from './connectivity';
import {fetchResource} from './go_helpers';
import {GoVpnTunnel} from './go_vpn_tunnel';
import {installRoutingServices, RoutingDaemon} from './routing_service';
import {TunnelStore} from './tunnel_store';
Expand Down Expand Up @@ -292,7 +293,9 @@ function interceptShadowsocksLink(argv: string[]) {
if (mainWindow) {
// The system adds a trailing slash to the intercepted URL (before the fragment).
// Remove it before sending to the UI.
url = `${protocol}${url.substring(protocol.length).replace(/\/$/g, '')}`;
url = `${protocol}${url
.substring(protocol.length)
.replace(/\/$/g, '')}`;
// TODO: refactor channel name and namespace to a constant
mainWindow.webContents.send('outline-ipc-add-server', url);
} else {
Expand Down Expand Up @@ -498,6 +501,12 @@ function main() {
mainWindow?.webContents.send('outline-ipc-push-clipboard');
});

// Fetches a resource (usually the dynamic key config) from a remote URL.
ipcMain.handle(
'outline-ipc-fetch-resource',
async (_, url: string): Promise<string> => fetchResource(url, debugMode)
);

// Connects to a proxy server specified by a config.
//
// If any issues occur, an Error will be thrown, which you can try-catch around
Expand Down
35 changes: 32 additions & 3 deletions client/go/outline/electron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,45 @@ var args struct {
checkConnectivity *bool
dnsFallback *bool
version *bool

fetchUrl *string
}

var version string // Populated at build time through `-X main.version=...`

// This app sets up a local network stack to handle requests from a tun device.
// By default, this app sets up a local network stack to handle requests from a tun device.
//
// If the app runs successfully, it exits with code 0.
// If there's an error, it exits with code 1 and prints a detailed error message in JSON format to stderr.
//
// The app also prints logs, but these are not meant to be read by the parent process.
//
// This app has two extra modes:
//
// - Connectivity Check: If you run the app with `-checkConnectivity`, it will test the proxy's connectivity
// and exit with the result printed out to standard output.
// - Fetch Resource: If you run the app with `-fetchUrl`, it will fetch the content from the specified
// URL and exit with the content printed out to standard output.
func main() {
// VPN routing configs
args.tunAddr = flag.String("tunAddr", "10.0.85.2", "TUN interface IP address")
args.tunGw = flag.String("tunGw", "10.0.85.1", "TUN interface gateway")
args.tunMask = flag.String("tunMask", "255.255.255.0", "TUN interface network mask; prefixlen for IPv6")
args.tunDNS = flag.String("tunDNS", "1.1.1.1,9.9.9.9,208.67.222.222", "Comma-separated list of DNS resolvers for the TUN interface (Windows only)")
args.tunName = flag.String("tunName", "tun0", "TUN interface name")
args.transportConfig = flag.String("transport", "", "A JSON object containing the transport config, UTF8-encoded")
args.logLevel = flag.String("logLevel", "info", "Logging level: debug|info|warn|error|none")
args.dnsFallback = flag.Bool("dnsFallback", false, "Enable DNS fallback over TCP (overrides the UDP handler).")

// Proxy transport config
args.transportConfig = flag.String("transport", "", "A JSON object containing the transport config, UTF8-encoded")

// Check connectivity of transportConfig and exit
args.checkConnectivity = flag.Bool("checkConnectivity", false, "Check the proxy TCP and UDP connectivity and exit.")

// Fetch content of the given URL value and exit
args.fetchUrl = flag.String("fetchUrl", "", "Fetch the content from the given URL and exit.")

// Misc
args.logLevel = flag.String("logLevel", "info", "Logging level: debug|info|warn|error|none")
args.version = flag.Bool("version", false, "Print the version and exit.")

flag.Parse()
Expand All @@ -97,6 +117,15 @@ func main() {
os.Exit(exitCodeSuccess)
}

if *args.fetchUrl != "" {
result := outline.FetchResource(*args.fetchUrl)
if result.Error != nil {
printErrorAndExit(result.Error, exitCodeFailure)
}
fmt.Println(result.Content)
os.Exit(exitCodeSuccess)
}

setLogLevel(*args.logLevel)

if len(*args.transportConfig) == 0 {
Expand Down
Loading

0 comments on commit b8ac24a

Please sign in to comment.