Skip to content

Commit

Permalink
Merge branch 'master' into junyi/fix-windows-product-name
Browse files Browse the repository at this point in the history
  • Loading branch information
jyyi1 authored Sep 20, 2024
2 parents dd4e1c1 + e2d987a commit 4dba58d
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 84 deletions.
47 changes: 22 additions & 25 deletions client/electron/go_vpn_tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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 ...');
Expand All @@ -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;
}
35 changes: 24 additions & 11 deletions client/electron/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -43,7 +46,7 @@ export class ProcessTerminatedSignalError extends Error {
export class ChildProcessHelper {
private readonly processName: string;
private childProcess?: ChildProcess;
private waitProcessToExit?: Promise<void>;
private waitProcessToExit?: Promise<string>;

/**
* Whether to enable verbose logging for the process. Must be called before launch().
Expand All @@ -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<void> {
async launch(args: string[], returnStdOut: boolean = true): Promise<string> {
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<string>((resolve, reject) => {
let stdErrJSON = '';
let stdOutStr = '';
const onExit = (code?: number, signal?: string) => {
if (this.childProcess) {
this.childProcess.removeAllListeners();
Expand All @@ -84,15 +92,20 @@ export class ChildProcessHelper {

logExit(this.processName, code, signal);
if (code === 0) {
resolve();
resolve(stdOutStr);
} else if (code) {
reject(new ProcessTerminatedExitCodeError(code, stdErrJSON));
} else {
reject(new ProcessTerminatedSignalError(signal ?? 'unknown'));
}
};

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
Expand Down Expand Up @@ -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<void> {
async stop(): Promise<string> {
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) {
Expand Down
80 changes: 33 additions & 47 deletions client/go/outline/electron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
Expand All @@ -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.
Expand All @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion client/go/outline/neterrors/neterrors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4dba58d

Please sign in to comment.