From 0df9244e0c7182ab7eb68ff08400aba24ceaff5c Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Mon, 16 Sep 2024 17:31:44 +0300 Subject: [PATCH 1/2] fix(docs): update `errors.ts` reference (#2209) --- client/go/outline/neterrors/neterrors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/go/outline/neterrors/neterrors.go b/client/go/outline/neterrors/neterrors.go index 2246d66e525..089da1d2005 100644 --- a/client/go/outline/neterrors/neterrors.go +++ b/client/go/outline/neterrors/neterrors.go @@ -24,7 +24,7 @@ func (e Error) Number() int { return int(e) } -// Outline error codes. Must be kept in sync with definitions in https://github.com/Jigsaw-Code/outline-apps/blob/master/src/www/model/errors.ts +// Outline error codes. Must be kept in sync with definitions in https://github.com/Jigsaw-Code/outline-apps/blob/master/client/src/www/model/errors.ts const ( NoError Error = 0 Unexpected Error = 1 From e2d987a9fbbea6c14d4feec9887c336d2b89e6c2 Mon Sep 17 00:00:00 2001 From: "J. Yi" <93548144+jyyi1@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:07:16 -0400 Subject: [PATCH 2/2] refactor(client): use new Go API for electron (#2205) Refactored the electron client to leverage the new exported Outline Go API for checking server connectivity and creating the client instance. Additionally, modified the connectivity test to output both TCPErrorJson and UDPErrorJson to stdout instead of exiting with a specific error code. --- client/electron/go_vpn_tunnel.ts | 47 ++++++++---------- client/electron/process.ts | 35 +++++++++---- client/go/outline/electron/main.go | 80 ++++++++++++------------------ 3 files changed, 79 insertions(+), 83 deletions(-) diff --git a/client/electron/go_vpn_tunnel.ts b/client/electron/go_vpn_tunnel.ts index 49351310147..9a6948f0e5b 100755 --- a/client/electron/go_vpn_tunnel.ts +++ b/client/electron/go_vpn_tunnel.ts @@ -17,18 +17,13 @@ import {platform} from 'os'; import {powerMonitor} from 'electron'; import {pathToEmbeddedTun2socksBinary} from './app_paths'; -import { - ChildProcessHelper, - ProcessTerminatedExitCodeError, - ProcessTerminatedSignalError, -} from './process'; +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 {ErrorCode} from '../src/www/model/errors'; const isLinux = platform() === 'linux'; const isWindows = platform() === 'win32'; @@ -297,7 +292,7 @@ class GoTun2socks { this.monitorStarted().then(() => (restarting = true)); try { lastError = null; - await this.process.launch(args); + await this.process.launch(args, false); console.info('[tun2socks] - exited with no errors'); } catch (e) { console.error('[tun2socks] - terminated due to:', e); @@ -315,9 +310,9 @@ class GoTun2socks { } /** - * Checks connectivity and exits with an error code as defined in `errors.ErrorCode`. - * If exit code is not zero, a `ProcessTerminatedExitCodeError` might be thrown. - * -tun* and -dnsFallback options have no effect on this mode. + * Checks connectivity and exits with the string of stdout. + * + * @throws ProcessTerminatedExitCodeError if tun2socks failed to run successfully. */ checkConnectivity() { console.debug('[tun2socks] - checking connectivity ...'); @@ -333,21 +328,23 @@ class GoTun2socks { } } -// Leverages the outline-go-tun2socks 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. Resolves with a boolean indicating whether UDP -// forwarding is supported. Throws if the checks fail or if the process fails to start. +/** + * 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) { - try { - await tun2socks.checkConnectivity(); - return true; - } catch (e) { - console.error('connectivity check error:', e); - if (e instanceof ProcessTerminatedExitCodeError) { - if (e.exitCode === ErrorCode.UDP_RELAY_NOT_ENABLED) { - return false; - } - } - throw e; + 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; } diff --git a/client/electron/process.ts b/client/electron/process.ts index 2ff39998ddc..6e40db6bb15 100755 --- a/client/electron/process.ts +++ b/client/electron/process.ts @@ -20,7 +20,10 @@ import process from 'node:process'; * A child process is terminated abnormally, caused by a non-zero exit code. */ export class ProcessTerminatedExitCodeError extends Error { - constructor(readonly exitCode: number, errJSON: string) { + constructor( + readonly exitCode: number, + errJSON: string + ) { super(errJSON); } } @@ -43,7 +46,7 @@ export class ProcessTerminatedSignalError extends Error { export class ChildProcessHelper { private readonly processName: string; private childProcess?: ChildProcess; - private waitProcessToExit?: Promise; + private waitProcessToExit?: Promise; /** * Whether to enable verbose logging for the process. Must be called before launch(). @@ -60,18 +63,23 @@ export class ChildProcessHelper { * Start the process with the given args and wait for the process to exit. If `isDebugModeEnabled` * is `true`, the process is started in verbose mode if supported. * - * If the process does not exist normally (i.e., exit code !== 0 or received a signal), it will + * If the process does not exit normally (i.e., exit code !== 0 or received a signal), it will * throw either `ProcessTerminatedExitCodeError` or `ProcessTerminatedSignalError`. * + * It the process exits normally, it will return the stdout string. + * * @param args The args for the process */ - async launch(args: string[]): Promise { + async launch(args: string[], returnStdOut: boolean = true): Promise { if (this.childProcess) { - throw new Error(`subprocess ${this.processName} has already been launched`); + throw new Error( + `subprocess ${this.processName} has already been launched` + ); } this.childProcess = spawn(this.path, args); - return (this.waitProcessToExit = new Promise((resolve, reject) => { + return (this.waitProcessToExit = new Promise((resolve, reject) => { let stdErrJSON = ''; + let stdOutStr = ''; const onExit = (code?: number, signal?: string) => { if (this.childProcess) { this.childProcess.removeAllListeners(); @@ -84,7 +92,7 @@ export class ChildProcessHelper { logExit(this.processName, code, signal); if (code === 0) { - resolve(); + resolve(stdOutStr); } else if (code) { reject(new ProcessTerminatedExitCodeError(code, stdErrJSON)); } else { @@ -92,7 +100,12 @@ export class ChildProcessHelper { } }; - this.childProcess?.stdout?.on('data', data => this.stdOutListener?.(data)); + this.childProcess?.stdout?.on('data', data => { + this.stdOutListener?.(data); + if (returnStdOut) { + stdOutStr += data?.toString() ?? ''; + } + }); this.childProcess?.stderr?.on('data', (data?: string | Buffer) => { if (this.isDebugModeEnabled) { // This will be captured by Sentry @@ -121,13 +134,13 @@ export class ChildProcessHelper { * If the process does not exist normally (i.e., exit code !== 0 or received a signal), it will * throw either `ProcessTerminatedExitCodeError` or `ProcessTerminatedSignalError`. */ - async stop(): Promise { + async stop(): Promise { if (!this.childProcess) { // Never started. - return; + return ''; } this.childProcess.kill(); - return await this.waitProcessToExit; + return (await this.waitProcessToExit) ?? ''; } set onStdOut(listener: ((data?: string | Buffer) => void) | null) { diff --git a/client/go/outline/electron/main.go b/client/go/outline/electron/main.go index 42e029c4734..e4df2cdf13c 100644 --- a/client/go/outline/electron/main.go +++ b/client/go/outline/electron/main.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "flag" "fmt" "io" @@ -26,7 +27,6 @@ import ( "time" "github.com/Jigsaw-Code/outline-apps/client/go/outline" - "github.com/Jigsaw-Code/outline-apps/client/go/outline/neterrors" "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" "github.com/Jigsaw-Code/outline-apps/client/go/outline/tun2socks" _ "github.com/eycorsican/go-tun2socks/common/log/simple" // Register a simple logger. @@ -38,9 +38,8 @@ import ( // tun2socks exit codes. Must be kept in sync with definitions in "go_vpn_tunnel.ts" // TODO: replace exit code with structured JSON output const ( - exitCodeSuccess = 0 - exitCodeFailure = 1 - exitCodeNoUDPConnectivity = 4 + exitCodeSuccess = 0 + exitCodeFailure = 1 ) const ( @@ -51,6 +50,12 @@ const ( var logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) +// The result JSON containing two error strings when "--checkConnectivity". +type CheckConnectivityResult struct { + TCPErrorJson string `json:"tcp"` + UDPErrorJson string `json:"udp"` +} + var args struct { tunAddr *string tunGw *string @@ -97,13 +102,24 @@ func main() { if len(*args.transportConfig) == 0 { printErrorAndExit(platerrors.PlatformError{Code: platerrors.IllegalConfig, Message: "transport config missing"}, exitCodeFailure) } - client, err := outline.NewClient(*args.transportConfig) - if err != nil { - printErrorAndExit(err, exitCodeFailure) + clientResult := outline.NewClientAndReturnError(*args.transportConfig) + if clientResult.Error != nil { + printErrorAndExit(clientResult.Error, exitCodeFailure) } + client := clientResult.Client if *args.checkConnectivity { - checkConnectivityAndExit(client) + result := outline.CheckTCPAndUDPConnectivity(client) + output := CheckConnectivityResult{ + TCPErrorJson: marshalErrorToJSON(result.TCPError), + UDPErrorJson: marshalErrorToJSON(result.UDPError), + } + jsonBytes, err := json.Marshal(output) + if err != nil { + printErrorAndExit(err, exitCodeFailure) + } + fmt.Println(string(jsonBytes)) + os.Exit(exitCodeSuccess) } // Open TUN device @@ -169,50 +185,20 @@ func setLogLevel(level string) { logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slvl})) } -func printErrorAndExit(e error, exitCode int) { +func marshalErrorToJSON(e error) string { pe := platerrors.ToPlatformError(e) + if pe == nil { + return "" + } errJson, err := platerrors.MarshalJSONString(pe) if err != nil { // TypeScript's PlatformError can unmarshal a raw string - errJson = string(pe.Code) + return string(pe.Code) } - fmt.Fprintln(os.Stderr, errJson) - os.Exit(exitCode) + return errJson } -// checkConnectivity checks whether the remote Outline server supports TCP or UDP, -// and converts the neterrors to a PlatformError. -// TODO: remove this function once we migrated CheckConnectivity to return a PlatformError. -func checkConnectivityAndExit(c *outline.Client) { - connErrCode, err := outline.CheckConnectivity(c) - if err != nil { - printErrorAndExit(platerrors.PlatformError{ - Code: platerrors.InternalError, - Message: "failed to check connectivity", - Cause: platerrors.ToPlatformError(err), - }, exitCodeFailure) - } - switch connErrCode { - case neterrors.NoError.Number(): - os.Exit(exitCodeSuccess) - case neterrors.AuthenticationFailure.Number(): - printErrorAndExit(platerrors.PlatformError{ - Code: platerrors.Unauthenticated, - Message: "authentication failed", - }, exitCodeFailure) - case neterrors.Unreachable.Number(): - printErrorAndExit(platerrors.PlatformError{ - Code: platerrors.ProxyServerUnreachable, - Message: "cannot connect to Outline server", - }, exitCodeFailure) - case neterrors.UDPConnectivity.Number(): - printErrorAndExit(platerrors.PlatformError{ - Code: platerrors.ProxyServerUDPUnsupported, - Message: "Outline server does not support UDP", - }, exitCodeNoUDPConnectivity) - } - printErrorAndExit(platerrors.PlatformError{ - Code: platerrors.InternalError, - Message: "failed to check connectivity", - }, exitCodeFailure) +func printErrorAndExit(e error, exitCode int) { + fmt.Fprintln(os.Stderr, marshalErrorToJSON(e)) + os.Exit(exitCode) }