-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
refactor(client/windows): move fetching logic from TypeScript to Go #2220
Changes from 2 commits
9b314ca
d049b1e
bf88ea7
3186d6b
616df93
924e33d
699c4cc
ffc66b9
0391caf
772947c
d129321
2131b2c
160402e
627562d
7ae758f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,8 +67,8 @@ 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(); | ||
jyyi1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.connectivityChecker = new GoTun2socks(); | ||
jyyi1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// This promise, tied to both helper process' exits, is key to the instance's | ||
// lifecycle: | ||
|
@@ -104,13 +104,15 @@ export class GoVpnTunnel implements VpnTunnel { | |
}); | ||
|
||
if (checkProxyConnectivity) { | ||
this.isUdpEnabled = await checkConnectivity(this.connectivityChecker); | ||
this.isUdpEnabled = await this.connectivityChecker.checkConnectivity( | ||
this.transportConfig | ||
); | ||
} | ||
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(), | ||
]); | ||
} | ||
|
@@ -152,15 +154,17 @@ 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 this.connectivityChecker.checkConnectivity( | ||
this.transportConfig | ||
); | ||
} catch (e) { | ||
console.error(`connectivity check failed: ${e}`); | ||
return; | ||
|
@@ -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. | ||
|
@@ -234,7 +238,7 @@ class GoTun2socks { | |
private stopRequested = false; | ||
private readonly process: ChildProcessHelper; | ||
|
||
constructor(private readonly transportConfig: TransportConfigJson) { | ||
constructor() { | ||
this.process = new ChildProcessHelper(pathToEmbeddedTun2socksBinary()); | ||
} | ||
|
||
|
@@ -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 \ | ||
|
@@ -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'); | ||
|
@@ -310,17 +317,39 @@ class GoTun2socks { | |
} | ||
|
||
/** | ||
* Checks connectivity and exits with the string of stdout. | ||
* Checks 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. | ||
* | ||
* @returns A boolean indicating whether UDP forwarding is supported. | ||
* @throws ProcessTerminatedExitCodeError if tun2socks failed to run successfully. | ||
*/ | ||
checkConnectivity() { | ||
async checkConnectivity(config: TransportConfigJson): Promise<boolean> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you move this to a standalone function? There's no real dependency on internal state, only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
console.debug('[tun2socks] - checking connectivity ...'); | ||
return this.process.launch([ | ||
const output = await this.process.launch([ | ||
'-transport', | ||
JSON.stringify(this.transportConfig), | ||
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 dynamic key from the given URL. | ||
* @param url The URL to be fetched. | ||
* @throws ProcessTerminatedExitCodeError if we failed to fetch the config. | ||
*/ | ||
fetchConfig(url: string): Promise<string> { | ||
jyyi1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
console.debug('[tun2socks] - fetching dynamic key ...'); | ||
return this.process.launch(['-fetchConfig', url]); | ||
} | ||
|
||
enableDebugMode() { | ||
|
@@ -329,22 +358,18 @@ class GoTun2socks { | |
} | ||
|
||
/** | ||
* 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. | ||
* Fetches a dynamic key config using native code (Go). | ||
* @param url The HTTP(s) location of the config. | ||
* @returns A string representing the config. | ||
* @throws ProcessTerminatedExitCodeError if we failed to fetch the config. | ||
*/ | ||
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; | ||
export function fetchDynamicKeyConfig( | ||
jyyi1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
url: string, | ||
debugMode: boolean = false | ||
): Promise<string> { | ||
const tun2socks = new GoTun2socks(); | ||
if (debugMode) { | ||
tun2socks.enableDebugMode(); | ||
} | ||
return true; | ||
return tun2socks.fetchConfig(url); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,7 +34,7 @@ import { | |
import {autoUpdater} from 'electron-updater'; | ||
|
||
import {lookupIp} from './connectivity'; | ||
import {GoVpnTunnel} from './go_vpn_tunnel'; | ||
import {fetchDynamicKeyConfig, GoVpnTunnel} from './go_vpn_tunnel'; | ||
import {installRoutingServices, RoutingDaemon} from './routing_service'; | ||
import {TunnelStore} from './tunnel_store'; | ||
import {VpnTunnel} from './vpn_tunnel'; | ||
|
@@ -292,7 +292,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 { | ||
|
@@ -498,6 +500,13 @@ function main() { | |
mainWindow?.webContents.send('outline-ipc-push-clipboard'); | ||
}); | ||
|
||
// Fetches dynamic key config from a remote URL. | ||
ipcMain.handle( | ||
'outline-ipc-fetch-config', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps rename this to You'll need to change in the caller as well. |
||
async (_, url: string): Promise<string> => | ||
fetchDynamicKeyConfig(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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// 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. | ||
|
||
package outline | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
|
||
"github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" | ||
) | ||
|
||
// FetchDynamicKeyResult represents the result of fetching a dynamic key. | ||
// | ||
// We use a struct instead of a tuple to preserve a strongly typed error that gobind recognizes. | ||
type FetchDynamicKeyResult struct { | ||
Key string | ||
Error *platerrors.PlatformError | ||
} | ||
|
||
// FetchDynamicKey fetches a dynamic key from the given URL. | ||
// | ||
// The function makes an HTTP GET request to the specified URL and returns the response body as a | ||
// string. If the request fails or the server returns a non-2xx status code, an error is returned. | ||
func FetchDynamicKey(url string) *FetchDynamicKeyResult { | ||
jyyi1 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope. Updated to |
||
resp, err := http.Get(url) | ||
if err != nil { | ||
return &FetchDynamicKeyResult{Error: &platerrors.PlatformError{ | ||
Code: platerrors.FetchConfigFailed, | ||
Message: "failed to fetch the URL", | ||
Details: platerrors.ErrorDetails{"url": url}, | ||
Cause: platerrors.ToPlatformError(err), | ||
}} | ||
} | ||
body, err := io.ReadAll(resp.Body) | ||
resp.Body.Close() | ||
if resp.StatusCode > 299 { | ||
return &FetchDynamicKeyResult{Error: &platerrors.PlatformError{ | ||
Code: platerrors.FetchConfigFailed, | ||
Message: "non-successful HTTP status", | ||
Details: platerrors.ErrorDetails{ | ||
"status": resp.Status, | ||
"body": string(body), | ||
}, | ||
}} | ||
} | ||
if err != nil { | ||
return &FetchDynamicKeyResult{Error: &platerrors.PlatformError{ | ||
Code: platerrors.FetchConfigFailed, | ||
Message: "failed to read the body", | ||
Details: platerrors.ErrorDetails{"url": url}, | ||
Cause: platerrors.ToPlatformError(err), | ||
}} | ||
} | ||
return &FetchDynamicKeyResult{Key: string(body)} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file should be reverted, since there's no change needed for the VPN tunnel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done