Skip to content

Commit

Permalink
Merge branch 'master' into sbruens/license-file
Browse files Browse the repository at this point in the history
  • Loading branch information
sbruens committed Sep 4, 2024
2 parents 88c9d46 + 97cf545 commit 40d6fa2
Show file tree
Hide file tree
Showing 253 changed files with 15,726 additions and 8,255 deletions.
20 changes: 3 additions & 17 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@

<link href="../client/src/www/style.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet">

<style>
:root {
/* TODO(daniellacosse): unified system for managing css variables */
--primary-green: #00bfa5;
--background-color: #263238;
--background-contrast-color: #2e3a3f;
--light-gray: rgba(255, 255, 255, 0.87);
--medium-gray: rgba(255, 255, 255, 0.54);
--dark-gray: rgba(0, 0, 0, 0.87);
--border-color: rgba(255, 255, 255, 0.12);

--server-stat-card-background: var(--background-contrast-color);
--server-stat-card-foreground: var(--medium-gray);
--server-stat-card-highlight: #ffffff;
}
</style>
<link href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Symbols+Rounded" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Symbols+Sharp" rel="stylesheet">
2 changes: 2 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import '@material/web/all.js';

/** @type { import('@storybook/web-components').Preview } */
const preview = {
parameters: {
Expand Down
2 changes: 1 addition & 1 deletion client/.glf.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"../package.json",
"./package.json"
],
"output": "./src/www/ui_components/licenses/licenses.txt",
"output": "./src/www/views/licenses_view/licenses/licenses.txt",
"lineEnding": "lf",
"overwrite": true,
"replace": {
Expand Down
2 changes: 1 addition & 1 deletion client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The client's user interface is implemented in [Polymer](https://www.polymer-proj

## Requirements for all builds

All builds require [Node](https://nodejs.org/) 18 (lts/hydrogen), and [Go](https://golang.org/) 1.20 installed in addition to other per-platform requirements.
All builds require [Node](https://nodejs.org/) 18 (lts/hydrogen), and [Go](https://golang.org/) 1.21 installed in addition to other per-platform requirements.

> 💡 NOTE: if you have `nvm` installed, run `nvm use` to switch to the correct node version!
Expand Down
39 changes: 13 additions & 26 deletions client/electron/go_vpn_tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
import {RoutingDaemon} from './routing_service';
import {VpnTunnel} from './vpn_tunnel';
import {
ShadowsocksSessionConfig,
TransportConfigJson,
TunnelStatus,
} from '../src/www/app/outline_server_repository/vpn';
import {ErrorCode} from '../src/www/model/errors';
Expand All @@ -44,9 +44,9 @@ const DNS_RESOLVERS = ['1.1.1.1', '9.9.9.9'];
// Establishes a full-system VPN with the help of Outline's routing daemon and child process
// outline-go-tun2socks. The routing service modifies the routing table so that the TAP device
// receives all device traffic. outline-go-tun2socks process TCP and UDP traffic from the TAP
// device and relays it to a Shadowsocks proxy server.
// device and relays it to an Outline proxy server.
//
// |TAP| <-> |outline-go-tun2socks| <-> |Shadowsocks proxy|
// |TAP| <-> |outline-go-tun2socks| <-> |Outline proxy|
//
// In addition to the basic lifecycle of the helper processes, this class restarts tun2socks
// on unexpected failures and network changes if necessary.
Expand All @@ -70,10 +70,10 @@ export class GoVpnTunnel implements VpnTunnel {

constructor(
private readonly routing: RoutingDaemon,
private config: ShadowsocksSessionConfig
readonly transportConfig: TransportConfigJson
) {
this.tun2socks = new GoTun2socks(config);
this.connectivityChecker = new GoTun2socks(config);
this.tun2socks = new GoTun2socks(transportConfig);
this.connectivityChecker = new GoTun2socks(transportConfig);

// This promise, tied to both helper process' exits, is key to the instance's
// lifecycle:
Expand Down Expand Up @@ -231,42 +231,37 @@ export class GoVpnTunnel implements VpnTunnel {
}

// outline-go-tun2socks is a Go program that processes IP traffic from a TUN/TAP device
// and relays it to a Shadowsocks proxy server.
// and relays it to a Outline proxy server.
class GoTun2socks {
// Resolved when Tun2socks prints "tun2socks running" to stdout
// Call `monitorStarted` to set this field
private whenStarted: Promise<void>;
private stopRequested = false;
private readonly process: ChildProcessHelper;

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

/**
* Starts tun2socks process, and waits for it to launch successfully.
* Success is confirmed when the phrase "tun2socks running" is detected in the `stdout`.
* Otherwise, an error containing a JSON-formatted message will be thrown.
* @param isUdpEnabled Indicates whether the remote Shadowsocks server supports UDP.
* @param isUdpEnabled Indicates whether the remote Outline server supports UDP.
*/
async start(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 \
// -proxyHost 127.0.0.1 -proxyPort 1080 -proxyPassword mypassword \
// -proxyCipher chacha20-ietf-poly1035
// -transport '{"host": "127.0.0.1", "port": 1080, "password": "mypassword", "cipher": "chacha20-ietf-poly1035"}' \
// [-dnsFallback] [-checkConnectivity] [-proxyPrefix]
const args: string[] = [];
args.push('-tunName', TUN2SOCKS_TAP_DEVICE_NAME);
args.push('-tunAddr', TUN2SOCKS_TAP_DEVICE_IP);
args.push('-tunGw', TUN2SOCKS_VIRTUAL_ROUTER_IP);
args.push('-tunMask', TUN2SOCKS_VIRTUAL_ROUTER_NETMASK);
args.push('-tunDNS', DNS_RESOLVERS.join(','));
args.push('-proxyHost', this.config.host || '');
args.push('-proxyPort', `${this.config.port}`);
args.push('-proxyPassword', this.config.password || '');
args.push('-proxyCipher', this.config.method || '');
args.push('-proxyPrefix', this.config.prefix || '');
args.push('-transport', JSON.stringify(this.transportConfig));
args.push('-logLevel', this.process.isDebugModeEnabled ? 'debug' : 'info');
if (!isUdpEnabled) {
args.push('-dnsFallback');
Expand Down Expand Up @@ -327,16 +322,8 @@ class GoTun2socks {
checkConnectivity() {
console.debug('[tun2socks] - checking connectivity ...');
return this.process.launch([
'-proxyHost',
this.config.host || '',
'-proxyPort',
`${this.config.port}`,
'-proxyPassword',
this.config.password || '',
'-proxyCipher',
this.config.method || '',
'-proxyPrefix',
this.config.prefix || '',
'-transport',
JSON.stringify(this.transportConfig),
'-checkConnectivity',
]);
}
Expand Down
93 changes: 48 additions & 45 deletions client/electron/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ import {autoUpdater} from 'electron-updater';
import {lookupIp} from './connectivity';
import {GoVpnTunnel} from './go_vpn_tunnel';
import {installRoutingServices, RoutingDaemon} from './routing_service';
import {TunnelStore, SerializableTunnel} from './tunnel_store';
import {TunnelStore} from './tunnel_store';
import {VpnTunnel} from './vpn_tunnel';
import {
ShadowsocksSessionConfig,
getHostFromTransportConfig,
setTransportConfigHost,
StartRequestJson,
TunnelConfigJson,
TunnelStatus,
} from '../src/www/app/outline_server_repository/vpn';
import * as errors from '../src/www/model/errors';
Expand Down Expand Up @@ -302,9 +305,9 @@ function interceptShadowsocksLink(argv: string[]) {

// Set the app to launch at startup to connect automatically in case of a shutdown while
// proxying.
async function setupAutoLaunch(args: SerializableTunnel): Promise<void> {
async function setupAutoLaunch(request: StartRequestJson): Promise<void> {
try {
await tunnelStore.save(args);
await tunnelStore.save(request);
if (isLinux) {
if (process.env.APPIMAGE) {
const outlineAutoLauncher = new autoLaunch({
Expand Down Expand Up @@ -337,53 +340,62 @@ async function tearDownAutoLaunch() {
}
}

// Factory function to create a VPNTunnel instance backed by a network statck
// Factory function to create a VPNTunnel instance backed by a network stack
// specified at build time.
function createVpnTunnel(
config: ShadowsocksSessionConfig,
async function createVpnTunnel(
tunnelConfig: TunnelConfigJson,
isAutoConnect: boolean
): VpnTunnel {
const routing = new RoutingDaemon(config.host || '', isAutoConnect);
const tunnel = new GoVpnTunnel(routing, config);
): Promise<VpnTunnel> {
// We must convert the host from a potential "hostname" to an "IP" address
// because startVpn will add a routing table entry that prefixed with this
// host (e.g. "<host>/32"), therefore <host> must be an IP address.
// TODO: make sure we resolve it in the native code
const host = getHostFromTransportConfig(tunnelConfig.transport);
if (!host) {
throw new errors.IllegalServerConfiguration('host is missing');
}
const hostIp = await lookupIp(host);
const routing = new RoutingDaemon(hostIp || '', isAutoConnect);
// Make sure the transport will use the IP we will allowlist.
const resolvedTransport =
setTransportConfigHost(tunnelConfig.transport, hostIp) ??
tunnelConfig.transport;
const tunnel = new GoVpnTunnel(routing, resolvedTransport);
routing.onNetworkChange = tunnel.networkChanged.bind(tunnel);
return tunnel;
}

// Invoked by both the start-proxying event handler and auto-connect.
async function startVpn(
config: ShadowsocksSessionConfig,
id: string,
isAutoConnect = false
) {
async function startVpn(request: StartRequestJson, isAutoConnect: boolean) {
if (currentTunnel) {
throw new Error('already connected');
}

currentTunnel = createVpnTunnel(config, isAutoConnect);
currentTunnel = await createVpnTunnel(request.config, isAutoConnect);
if (debugMode) {
currentTunnel.enableDebugMode();
}

currentTunnel.onceDisconnected.then(() => {
console.log(`disconnected from ${id}`);
console.log(`disconnected from ${request.id}`);
currentTunnel = undefined;
setUiTunnelStatus(TunnelStatus.DISCONNECTED, id);
setUiTunnelStatus(TunnelStatus.DISCONNECTED, request.id);
});

currentTunnel.onReconnecting(() => {
console.log(`reconnecting to ${id}`);
setUiTunnelStatus(TunnelStatus.RECONNECTING, id);
console.log(`reconnecting to ${request.id}`);
setUiTunnelStatus(TunnelStatus.RECONNECTING, request.id);
});

currentTunnel.onReconnected(() => {
console.log(`reconnected to ${id}`);
setUiTunnelStatus(TunnelStatus.CONNECTED, id);
console.log(`reconnected to ${request.id}`);
setUiTunnelStatus(TunnelStatus.CONNECTED, request.id);
});

// Don't check connectivity on boot: if the key was revoked or network connectivity is not ready,
// we want the system to stay "connected" so that traffic doesn't leak.
await currentTunnel.connect(!isAutoConnect);
setUiTunnelStatus(TunnelStatus.CONNECTED, id);
setUiTunnelStatus(TunnelStatus.CONNECTED, request.id);
}

// Invoked by both the stop-proxying event and quit handler.
Expand Down Expand Up @@ -439,23 +451,23 @@ function main() {
// TODO(fortuna): Start the app with the window hidden on auto-start?
setupWindow();

let tunnelAtShutdown: SerializableTunnel;
let requestAtShutdown: StartRequestJson | undefined;
try {
tunnelAtShutdown = await tunnelStore.load();
requestAtShutdown = await tunnelStore.load();
} catch (e) {
// No tunnel at shutdown, or failure - either way, no need to start.
// TODO: Instead of quitting, how about creating the system tray icon?
console.warn('Could not load active tunnel: ', e);
await tunnelStore.clear();
}
if (tunnelAtShutdown) {
if (requestAtShutdown) {
console.info(
`was connected at shutdown, reconnecting to ${tunnelAtShutdown.id}`
`was connected at shutdown, reconnecting to ${requestAtShutdown.id}`
);
setUiTunnelStatus(TunnelStatus.RECONNECTING, tunnelAtShutdown.id);
setUiTunnelStatus(TunnelStatus.RECONNECTING, requestAtShutdown.id);
try {
await startVpn(tunnelAtShutdown.config, tunnelAtShutdown.id, true);
console.log(`reconnected to ${tunnelAtShutdown.id}`);
await startVpn(requestAtShutdown, true);
console.log(`reconnected to ${requestAtShutdown.id}`);
} catch (e) {
console.error(`could not reconnect: ${e.name} (${e.message})`);
}
Expand Down Expand Up @@ -496,10 +508,7 @@ function main() {
// TODO: refactor channel name and namespace to a constant
ipcMain.handle(
'outline-ipc-start-proxying',
async (
_,
args: {id: string; name: string; config: ShadowsocksSessionConfig}
): Promise<void> => {
async (_, request: StartRequestJson): Promise<void> => {
// TODO: Rather than first disconnecting, implement a more efficient switchover (as well as
// being faster, this would help prevent traffic leaks - the Cordova clients already do
// this).
Expand All @@ -509,20 +518,14 @@ function main() {
await currentTunnel.onceDisconnected;
}

console.log(`connecting to ${args.name} (${args.id})...`);
console.log(`connecting to ${request.name} (${request.id})...`);

try {
// We must convert the host from a potential "hostname" to an "IP" address
// because startVpn will add a routing table entry that prefixed with this
// host (e.g. "<host>/32"), therefore <host> must be an IP address.
// TODO: make sure we resolve it in the native code
args.config.host = await lookupIp(args.config.host || '');

await startVpn(args.config, args.id);
console.log(`connected to ${args.name} (${args.id})`);
await setupAutoLaunch(args);
await startVpn(request, false);
console.log(`connected to ${request.name} (${request.id})`);
await setupAutoLaunch(request);
// Auto-connect requires IPs; the hostname in here has already been resolved (see above).
tunnelStore.save(args).catch(() => {
tunnelStore.save(request).catch(() => {
console.error('Failed to store tunnel.');
});
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions client/electron/linux_proxy_controller/outline_error.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ class OutlineErrorCategory : public std::error_category {
return "vpn start failure";
case static_cast<int>(ErrorCode::kInvalidServerConfiguration):
return "invalid server configuration";
case static_cast<int>(ErrorCode::kShadowsocksStartFailure):
return "shadowsocks start failure";
case static_cast<int>(ErrorCode::kClientStartFailure):
return "client start failure";
case static_cast<int>(ErrorCode::kConfigureSystemProxyFailure):
return "configure system proxy failure";
case static_cast<int>(ErrorCode::kAdminPermissionDenied):
Expand Down
2 changes: 1 addition & 1 deletion client/electron/linux_proxy_controller/outline_error.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ enum class ErrorCode {
kServerUnreachable = 5,
kVpnStartFailure = 6,
kInvalidServerConfiguration = 7,
kShadowsocksStartFailure = 8,
kClientStartFailure = 8,
kConfigureSystemProxyFailure = 9,
kAdminPermissionDenied = 10,
kUnsupportedRoutingTable = 11,
Expand Down
Loading

0 comments on commit 40d6fa2

Please sign in to comment.