exit node as client
This commit is contained in:
289
proxy-exit.js
289
proxy-exit.js
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* PSK Proxy Exit-Node (Server)
|
||||
* PSK Proxy Exit-Node (Client)
|
||||
*
|
||||
* Listens for a TLS-PSK tunnel connection from the proxy relay.
|
||||
* Connects to the relay server as a client.
|
||||
* Receives OPEN(host,port) to create outbound TCP connections to remote servers,
|
||||
* then forwards DATA/CLOSE frames bidirectionally.
|
||||
*
|
||||
* Also supports UDP relaying for SOCKS5 UDP ASSOCIATE via new UDP_* frames.
|
||||
* Also supports UDP relaying for SOCKS5 UDP ASSOCIATE via UDP_* frames.
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
@@ -17,11 +17,12 @@ const dgram = require('dgram');
|
||||
const { program } = require('commander');
|
||||
|
||||
program
|
||||
.requiredOption('-P, --relay-port <port>', 'Port for proxy relay TLS-PSK tunnel connections')
|
||||
.requiredOption('-H, --host <host>', 'Host to bind to (e.g., 0.0.0.0)')
|
||||
.requiredOption('-H, --relay-host <host>', 'Relay server host to connect to')
|
||||
.requiredOption('-P, --relay-port <port>', 'Relay server port for exit connections')
|
||||
.requiredOption('--psk-file <path>', 'Path to PSK key file')
|
||||
.requiredOption('--identity <identity>', 'Expected PSK identity from relay')
|
||||
.requiredOption('--identity <identity>', 'PSK identity to use when connecting to relay')
|
||||
.option('--connect-timeout <ms>', 'Timeout for outbound TCP connect (ms)', '10000')
|
||||
.option('--reconnect-delay <ms>', 'Delay before reconnecting to relay (ms)', '2000')
|
||||
.parse();
|
||||
|
||||
const options = program.opts();
|
||||
@@ -35,6 +36,7 @@ try {
|
||||
}
|
||||
|
||||
const OUT_CONNECT_TIMEOUT = parseInt(options.connectTimeout, 10) || 10000;
|
||||
const RECONNECT_DELAY = parseInt(options.reconnectDelay, 10) || 2000;
|
||||
|
||||
// Message Types
|
||||
const MSG_TYPES = {
|
||||
@@ -136,20 +138,7 @@ function buildUdpPayload(host, port, data) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
function pskCallback(socket, identity) {
|
||||
console.log(`Relay client identity: ${identity}`);
|
||||
|
||||
if (identity !== options.identity) {
|
||||
console.warn(`PSK identity mismatch. Expected '${options.identity}', got '${identity}'.`);
|
||||
// Abort the connection by returning a falsy value.
|
||||
// For TLS 1.2, this should cause the handshake to fail.
|
||||
return null;
|
||||
}
|
||||
|
||||
// For TLS 1.2, the callback should return the PSK as a Buffer.
|
||||
return Buffer.from(pskKey, 'hex');
|
||||
}
|
||||
|
||||
// Global state
|
||||
let relaySocket = null;
|
||||
const upstreamConns = new Map(); // connectionId -> net.Socket
|
||||
|
||||
@@ -277,149 +266,161 @@ function ensureUdpSocketsForAssoc(connectionId) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
const server = tls.createServer(
|
||||
{
|
||||
pskCallback,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
},
|
||||
(socket) => {
|
||||
console.log('Proxy relay connected');
|
||||
relaySocket = socket;
|
||||
function connectToRelay() {
|
||||
console.log(`Connecting to relay server ${options.relayHost}:${options.relayPort} via TLS-PSK...`);
|
||||
|
||||
socket.setNoDelay(true);
|
||||
socket.setKeepAlive(true, 30000);
|
||||
const pskCb = () => ({
|
||||
identity: options.identity,
|
||||
psk: Buffer.from(pskKey, 'hex'),
|
||||
});
|
||||
|
||||
const reader = createMessageReader();
|
||||
const sock = tls.connect(
|
||||
{
|
||||
host: options.relayHost,
|
||||
port: parseInt(options.relayPort, 10),
|
||||
pskCallback: pskCb,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
checkServerIdentity: () => undefined,
|
||||
},
|
||||
() => {
|
||||
console.log('Connected to relay server');
|
||||
}
|
||||
);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
reader(data, (type, connectionId, payload) => {
|
||||
if (type === MSG_TYPES.OPEN) {
|
||||
const spec = parseOpenPayload(payload);
|
||||
if (!spec) {
|
||||
console.warn(`Invalid OPEN payload for connection ${connectionId}`);
|
||||
writeMessage(relaySocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
return;
|
||||
sock.setNoDelay(true);
|
||||
sock.setKeepAlive(true, 30000);
|
||||
|
||||
relaySocket = sock;
|
||||
const reader = createMessageReader();
|
||||
|
||||
sock.on('data', (data) => {
|
||||
reader(data, (type, connectionId, payload) => {
|
||||
if (type === MSG_TYPES.OPEN) {
|
||||
const spec = parseOpenPayload(payload);
|
||||
if (!spec) {
|
||||
console.warn(`Invalid OPEN payload for connection ${connectionId}`);
|
||||
writeMessage(relaySocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
return;
|
||||
}
|
||||
|
||||
const { host, port } = spec;
|
||||
// Create outbound TCP connection
|
||||
const upstream = net.createConnection({ host, port });
|
||||
upstream.setNoDelay(true);
|
||||
upstream.setKeepAlive(true, 30000);
|
||||
|
||||
let connected = false;
|
||||
const connectTimer = setTimeout(() => {
|
||||
if (!connected) {
|
||||
upstream.destroy(new Error('Connect timeout'));
|
||||
}
|
||||
}, OUT_CONNECT_TIMEOUT);
|
||||
|
||||
const { host, port } = spec;
|
||||
// Create outbound TCP connection
|
||||
const upstream = net.createConnection({ host, port });
|
||||
upstream.setNoDelay(true);
|
||||
upstream.setKeepAlive(true, 30000);
|
||||
upstream.once('connect', () => {
|
||||
connected = true;
|
||||
clearTimeout(connectTimer);
|
||||
upstreamConns.set(connectionId, upstream);
|
||||
// Notify open success
|
||||
writeMessage(relaySocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
});
|
||||
|
||||
let connected = false;
|
||||
const connectTimer = setTimeout(() => {
|
||||
if (!connected) {
|
||||
upstream.destroy(new Error('Connect timeout'));
|
||||
}
|
||||
}, OUT_CONNECT_TIMEOUT);
|
||||
upstream.on('data', (chunk) => {
|
||||
// Queue data with framing and backpressure-aware sending
|
||||
enqueueToRelay(connectionId, chunk);
|
||||
});
|
||||
|
||||
upstream.once('connect', () => {
|
||||
connected = true;
|
||||
clearTimeout(connectTimer);
|
||||
upstreamConns.set(connectionId, upstream);
|
||||
// Notify open success
|
||||
writeMessage(relaySocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
});
|
||||
|
||||
upstream.on('data', (chunk) => {
|
||||
// Queue data with framing and backpressure-aware sending
|
||||
enqueueToRelay(connectionId, chunk);
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(connectTimer);
|
||||
if (upstreamConns.get(connectionId) === upstream) {
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
// Drop any pending TX data for this connection
|
||||
txQueues.delete(connectionId);
|
||||
writeMessage(relaySocket, MSG_TYPES.CLOSE, connectionId);
|
||||
};
|
||||
|
||||
upstream.on('error', (err) => {
|
||||
if (!connected) {
|
||||
clearTimeout(connectTimer);
|
||||
writeMessage(relaySocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
} else {
|
||||
// Upstream error after established
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
upstream.on('close', () => {
|
||||
cleanup();
|
||||
});
|
||||
} else if (type === MSG_TYPES.DATA) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream && !upstream.destroyed) {
|
||||
upstream.write(payload);
|
||||
}
|
||||
} else if (type === MSG_TYPES.CLOSE) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream) {
|
||||
upstream.destroy();
|
||||
const cleanup = () => {
|
||||
clearTimeout(connectTimer);
|
||||
if (upstreamConns.get(connectionId) === upstream) {
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
// Drop any pending TX data for this connection
|
||||
txQueues.delete(connectionId);
|
||||
writeMessage(relaySocket, MSG_TYPES.CLOSE, connectionId);
|
||||
};
|
||||
|
||||
// UDP handling
|
||||
} else if (type === MSG_TYPES.UDP_OPEN) {
|
||||
try {
|
||||
ensureUdpSocketsForAssoc(connectionId);
|
||||
writeMessage(relaySocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
} catch (e) {
|
||||
console.warn(`Failed to create UDP association for ${connectionId}: ${e.message}`);
|
||||
writeMessage(relaySocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
upstream.on('error', (err) => {
|
||||
if (!connected) {
|
||||
clearTimeout(connectTimer);
|
||||
writeMessage(relaySocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
} else {
|
||||
// Upstream error after established
|
||||
cleanup();
|
||||
}
|
||||
} else if (type === MSG_TYPES.UDP_SEND) {
|
||||
const parsed = parseUdpPayload(payload);
|
||||
if (!parsed) {
|
||||
console.warn(`Invalid UDP_SEND payload for ${connectionId}`);
|
||||
return;
|
||||
}
|
||||
const { host, port, data } = parsed;
|
||||
const entry = udpAssoc.get(connectionId) || ensureUdpSocketsForAssoc(connectionId);
|
||||
// Choose v6 socket if IPv6 literal detected
|
||||
const isV6 = host.includes(':');
|
||||
const sock = isV6 ? entry.udp6 : entry.udp4;
|
||||
try {
|
||||
sock.send(data, port, host);
|
||||
} catch (e) {
|
||||
console.warn(`UDP send failed (conn ${connectionId}) to ${host}:${port} - ${e.message}`);
|
||||
}
|
||||
} else if (type === MSG_TYPES.UDP_CLOSE) {
|
||||
const entry = udpAssoc.get(connectionId);
|
||||
if (entry) {
|
||||
try { entry.udp4 && entry.udp4.close(); } catch (_) {}
|
||||
try { entry.udp6 && entry.udp6.close(); } catch (_) {}
|
||||
udpAssoc.delete(connectionId);
|
||||
}
|
||||
} else {
|
||||
// ignore unknown types
|
||||
});
|
||||
|
||||
upstream.on('close', () => {
|
||||
cleanup();
|
||||
});
|
||||
} else if (type === MSG_TYPES.DATA) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream && !upstream.destroyed) {
|
||||
upstream.write(payload);
|
||||
}
|
||||
} else if (type === MSG_TYPES.CLOSE) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream) {
|
||||
upstream.destroy();
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('Proxy relay disconnected');
|
||||
relaySocket = null;
|
||||
closeAllUpstreams();
|
||||
closeAllUdp();
|
||||
// UDP handling
|
||||
} else if (type === MSG_TYPES.UDP_OPEN) {
|
||||
try {
|
||||
ensureUdpSocketsForAssoc(connectionId);
|
||||
writeMessage(relaySocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
} catch (e) {
|
||||
console.warn(`Failed to create UDP association for ${connectionId}: ${e.message}`);
|
||||
writeMessage(relaySocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
}
|
||||
} else if (type === MSG_TYPES.UDP_SEND) {
|
||||
const parsed = parseUdpPayload(payload);
|
||||
if (!parsed) {
|
||||
console.warn(`Invalid UDP_SEND payload for ${connectionId}`);
|
||||
return;
|
||||
}
|
||||
const { host, port, data } = parsed;
|
||||
const entry = udpAssoc.get(connectionId) || ensureUdpSocketsForAssoc(connectionId);
|
||||
// Choose v6 socket if IPv6 literal detected
|
||||
const isV6 = host.includes(':');
|
||||
const sock = isV6 ? entry.udp6 : entry.udp4;
|
||||
try {
|
||||
sock.send(data, port, host);
|
||||
} catch (e) {
|
||||
console.warn(`UDP send failed (conn ${connectionId}) to ${host}:${port} - ${e.message}`);
|
||||
}
|
||||
} else if (type === MSG_TYPES.UDP_CLOSE) {
|
||||
const entry = udpAssoc.get(connectionId);
|
||||
if (entry) {
|
||||
try { entry.udp4 && entry.udp4.close(); } catch (_) {}
|
||||
try { entry.udp6 && entry.udp6.close(); } catch (_) {}
|
||||
udpAssoc.delete(connectionId);
|
||||
}
|
||||
} else {
|
||||
// ignore unknown types
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Relay socket error:', err.message);
|
||||
});
|
||||
}
|
||||
);
|
||||
sock.on('close', () => {
|
||||
console.log(`Disconnected from relay server. Retrying in ${RECONNECT_DELAY}ms...`);
|
||||
relaySocket = null;
|
||||
closeAllUpstreams();
|
||||
closeAllUdp();
|
||||
setTimeout(connectToRelay, RECONNECT_DELAY);
|
||||
});
|
||||
|
||||
server.listen(parseInt(options.relayPort, 10), options.host, () => {
|
||||
console.log(`PSK Proxy Exit-Node listening on ${options.host}:${options.relayPort}`);
|
||||
});
|
||||
sock.on('error', (err) => {
|
||||
console.error('Relay connection error:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Start connection to the relay server
|
||||
connectToRelay();
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Shutting down...');
|
||||
try { server.close(); } catch (_) {}
|
||||
if (relaySocket) try { relaySocket.destroy(); } catch (_) {}
|
||||
closeAllUpstreams();
|
||||
closeAllUdp();
|
||||
process.exit(0);
|
||||
|
Reference in New Issue
Block a user