Skip to content

Commit

Permalink
Add gatewayIp and gatewayIndex to routing daemon response
Browse files Browse the repository at this point in the history
  • Loading branch information
jyyi1 committed Feb 5, 2025
1 parent 5b5f8d6 commit e30f9bb
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 103 deletions.
66 changes: 45 additions & 21 deletions client/electron/go_vpn_tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export class GoVpnTunnel implements VpnTunnel {

private isUdpEnabled = false;

private gatewayAdapterIp?: string;
private gatewayAdapterIndex?: string;

private readonly onAllHelpersStopped: Promise<void>;
private resolveAllHelpersStopped: () => void;

Expand Down Expand Up @@ -109,21 +112,31 @@ export class GoVpnTunnel implements VpnTunnel {
console.log(`UDP support: ${this.isUdpEnabled}`);

console.log('starting routing daemon');
await Promise.all([
this.tun2socks.start(this.transportConfig, this.isUdpEnabled),
this.routing.start(),
]);
const gateway = await this.routing.start();
this.gatewayAdapterIp = gateway?.gatewayIp;
this.gatewayAdapterIndex = gateway?.gatewayIndex;
await this.startTun2socks();
}

networkChanged(status: TunnelStatus) {
networkChanged(
status: TunnelStatus,
gatewayIp?: string,
gatewayIndex?: string
) {
if (status === TunnelStatus.CONNECTED) {
if (gatewayIp) {
this.gatewayAdapterIp = gatewayIp;
}
if (gatewayIndex) {
this.gatewayAdapterIndex = gatewayIndex;
}
if (this.reconnectedListener) {
this.reconnectedListener();
}

// Test whether UDP availability has changed; since it won't change 99% of the time, do this
// *after* we've informed the client we've reconnected.
this.updateUdpSupport();
this.updateUdpAndRestartTun2socks();
} else if (status === TunnelStatus.RECONNECTING) {
if (this.reconnectingListener) {
this.reconnectingListener();
Expand Down Expand Up @@ -151,32 +164,32 @@ export class GoVpnTunnel implements VpnTunnel {
}

console.log('restarting tun2socks after resume');
await Promise.all([
this.tun2socks.start(this.transportConfig, this.isUdpEnabled),
this.updateUdpSupport(), // Check if UDP support has changed; if so, silently restart.
]);
await this.updateUdpAndRestartTun2socks();
}

private startTun2socks(): Promise<void> {
return this.tun2socks.start(
this.transportConfig,
this.isUdpEnabled,
this.gatewayAdapterIp,
this.gatewayAdapterIndex
);
}

private async updateUdpSupport() {
const wasUdpEnabled = this.isUdpEnabled;
private async updateUdpAndRestartTun2socks() {
try {
this.isUdpEnabled = await checkUDPConnectivity(
this.transportConfig,
this.isDebugMode
);
console.log(`UDP support now ${this.isUdpEnabled}`);
} catch (e) {
console.error(`connectivity check failed: ${e}`);
return;
}
if (this.isUdpEnabled === wasUdpEnabled) {
return;
}

console.log(`UDP support change: now ${this.isUdpEnabled}`);

// Restart tun2socks.
await this.tun2socks.stop();
await this.tun2socks.start(this.transportConfig, this.isUdpEnabled);
await this.startTun2socks();
}

// Use #onceDisconnected to be notified when the tunnel terminates.
Expand Down Expand Up @@ -247,7 +260,12 @@ 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(transportConfig: string, isUdpEnabled: boolean): Promise<void> {
async start(
transportConfig: string,
isUdpEnabled: boolean,
gatewayIp?: string,
gatewayIndex?: string
): 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 @@ -264,6 +282,12 @@ class GoTun2socks {
if (!isUdpEnabled) {
args.push('-dnsFallback');
}
if (gatewayIp) {
args.push('-gatewayIp', gatewayIp);
}
if (gatewayIndex) {
args.push('-gatewayIndex', gatewayIndex);
}

const whenProcessEnded = this.launchWithAutoRestart(args);

Expand All @@ -284,7 +308,7 @@ class GoTun2socks {
}

private async launchWithAutoRestart(args: string[]): Promise<void> {
console.debug('[tun2socks] - starting to route network traffic ...');
console.debug('[tun2socks] - starting to route network traffic ...', args);
let restarting = false;
let lastError: Error | null = null;
do {
Expand Down
5 changes: 0 additions & 5 deletions client/electron/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,6 @@ async function createVpnTunnel(
}
const hostIp = await lookupIp(host);
const routing = new RoutingDaemon(hostIp || '', isAutoConnect);
// Make sure the transport will use the IP we will allowlist.
// HACK: We do a simple string replacement in the config here. This may not always work with general configs
// but it works for simple configs.
// TODO: Remove the need to allowlisting the host IP.
tunnelConfig.transport = tunnelConfig.transport.replaceAll(host, hostIp);
const tunnel = new GoVpnTunnel(routing, tunnelConfig.transport);
routing.onNetworkChange = tunnel.networkChanged.bind(tunnel);
return tunnel;
Expand Down
169 changes: 94 additions & 75 deletions client/electron/routing_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ interface RoutingServiceResponse {
statusCode: RoutingServiceStatusCode;
errorMessage?: string;
connectionStatus: TunnelStatus;
gatewayIp?: string;
gatewayAdapterIndex?: string;
}

enum RoutingServiceAction {
Expand Down Expand Up @@ -80,7 +82,11 @@ export class RoutingDaemon {
this.fulfillDisconnect = F;
});

private networkChangeListener?: (status: TunnelStatus) => void;
private networkChangeListener?: (
status: TunnelStatus,
gatewayIp?: string,
gatewayIndex?: string
) => void;

constructor(
private proxyAddress: string,
Expand All @@ -90,80 +96,85 @@ export class RoutingDaemon {
// Fulfills once a connection is established with the routing daemon *and* it has successfully
// configured the system's routing table.
async start() {
return new Promise<void>((fulfill, reject) => {
const newSocket = (this.socket = createConnection(SERVICE_NAME, () => {
newSocket.removeListener('error', initialErrorHandler);
const cleanup = () => {
newSocket.removeAllListeners();
return new Promise<{gatewayIp?: string; gatewayIndex?: string}>(
(fulfill, reject) => {
const newSocket = (this.socket = createConnection(SERVICE_NAME, () => {
newSocket.removeListener('error', initialErrorHandler);
const cleanup = () => {
newSocket.removeAllListeners();
this.socket = null;
this.fulfillDisconnect();
};
newSocket.once('close', cleanup);
newSocket.once('error', cleanup);

newSocket.once('data', data => {
const message = this.parseRoutingServiceResponse(data);
if (
!message ||
message.action !== RoutingServiceAction.CONFIGURE_ROUTING ||
message.statusCode !== RoutingServiceStatusCode.SUCCESS
) {
// NOTE: This will rarely occur because the connectivity tests
// performed when the user clicks "CONNECT" should detect when
// the system is offline and that, currently, is pretty much
// the only time the routing service will fail.
reject(
new Error(
message
? message.errorMessage
: 'empty routing service response'
)
);
newSocket.end();
return;
}

newSocket.on('data', this.dataHandler.bind(this));

// Potential race condition: this routing daemon might already be stopped by the tunnel
// when one of the dependencies (ss-local/tun2socks) exited
// TODO(junyi): better handling this case in the next installation logic fix
if (this.stopping) {
cleanup();
newSocket.destroy();
const perr = new PlatformError(
GoErrorCode.ROUTING_SERVICE_NOT_RUNNING,
'routing daemon service stopped before started'
);
reject(new Error(perr.toJSON()));
} else {
fulfill({
gatewayIp: message.gatewayIp,
gatewayIndex: message.gatewayAdapterIndex,
});
}
});

newSocket.write(
JSON.stringify({
action: RoutingServiceAction.CONFIGURE_ROUTING,
parameters: {
proxyIp: this.proxyAddress,
isAutoConnect: this.isAutoConnect,
},
} as RoutingServiceRequest)
);
}));

const initialErrorHandler = (err: Error) => {
console.error('Routing daemon socket setup failed', err);
this.socket = null;
this.fulfillDisconnect();
const perr = new PlatformError(
GoErrorCode.ROUTING_SERVICE_NOT_RUNNING,
'routing daemon is not running',
{cause: err}
);
reject(new Error(perr.toJSON()));
};
newSocket.once('close', cleanup);
newSocket.once('error', cleanup);

newSocket.once('data', data => {
const message = this.parseRoutingServiceResponse(data);
if (
!message ||
message.action !== RoutingServiceAction.CONFIGURE_ROUTING ||
message.statusCode !== RoutingServiceStatusCode.SUCCESS
) {
// NOTE: This will rarely occur because the connectivity tests
// performed when the user clicks "CONNECT" should detect when
// the system is offline and that, currently, is pretty much
// the only time the routing service will fail.
reject(
new Error(
message
? message.errorMessage
: 'empty routing service response'
)
);
newSocket.end();
return;
}

newSocket.on('data', this.dataHandler.bind(this));

// Potential race condition: this routing daemon might already be stopped by the tunnel
// when one of the dependencies (ss-local/tun2socks) exited
// TODO(junyi): better handling this case in the next installation logic fix
if (this.stopping) {
cleanup();
newSocket.destroy();
const perr = new PlatformError(
GoErrorCode.ROUTING_SERVICE_NOT_RUNNING,
'routing daemon service stopped before started'
);
reject(new Error(perr.toJSON()));
} else {
fulfill();
}
});

newSocket.write(
JSON.stringify({
action: RoutingServiceAction.CONFIGURE_ROUTING,
parameters: {
proxyIp: this.proxyAddress,
isAutoConnect: this.isAutoConnect,
},
} as RoutingServiceRequest)
);
}));

const initialErrorHandler = (err: Error) => {
console.error('Routing daemon socket setup failed', err);
this.socket = null;
const perr = new PlatformError(
GoErrorCode.ROUTING_SERVICE_NOT_RUNNING,
'routing daemon is not running',
{cause: err}
);
reject(new Error(perr.toJSON()));
};
newSocket.once('error', initialErrorHandler);
});
newSocket.once('error', initialErrorHandler);
}
);
}

private dataHandler(data: Buffer) {
Expand All @@ -174,7 +185,11 @@ export class RoutingDaemon {
switch (message.action) {
case RoutingServiceAction.STATUS_CHANGED:
if (this.networkChangeListener) {
this.networkChangeListener(message.connectionStatus);
this.networkChangeListener(
message.connectionStatus,
message.gatewayIp,
message.gatewayAdapterIndex
);
}
break;
case RoutingServiceAction.RESET_ROUTING:
Expand Down Expand Up @@ -253,7 +268,11 @@ export class RoutingDaemon {
}

set onNetworkChange(
newListener: ((status: TunnelStatus) => void) | undefined
newListener: (
status: TunnelStatus,
gatewayIp?: string,
gatewayIndex?: string
) => void | undefined
) {
this.networkChangeListener = newListener;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<HintPath>..\..\..\..\..\third_party\newtonsoft\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
Expand Down Expand Up @@ -89,4 +89,4 @@
<WCFMetadata Include="Connected Services\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
</Project>
Binary file not shown.

0 comments on commit e30f9bb

Please sign in to comment.