diff --git a/package-lock.json b/package-lock.json index 81d7694..6a81fa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "psk-proxy-tunnel", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "psk-proxy-tunnel", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { "commander": "^14.0.0" }, "bin": { "psk-proxy-client": "proxy-client.js", - "psk-proxy-server": "proxy-server.js" + "psk-proxy-exit": "proxy-exit.js", + "psk-proxy-relay": "proxy-server.js" }, "devDependencies": { "pkg": "^5.8.1" diff --git a/proxy-exit.js b/proxy-exit.js index e13eede..20bbd4b 100644 --- a/proxy-exit.js +++ b/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 for proxy relay TLS-PSK tunnel connections') - .requiredOption('-H, --host ', 'Host to bind to (e.g., 0.0.0.0)') + .requiredOption('-H, --relay-host ', 'Relay server host to connect to') + .requiredOption('-P, --relay-port ', 'Relay server port for exit connections') .requiredOption('--psk-file ', 'Path to PSK key file') - .requiredOption('--identity ', 'Expected PSK identity from relay') + .requiredOption('--identity ', 'PSK identity to use when connecting to relay') .option('--connect-timeout ', 'Timeout for outbound TCP connect (ms)', '10000') + .option('--reconnect-delay ', '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); diff --git a/proxy-server.js b/proxy-server.js index e8b1197..a2930b8 100644 --- a/proxy-server.js +++ b/proxy-server.js @@ -3,9 +3,11 @@ /** * PSK Proxy Relay-Node (Server) * - * Listens for a TLS-PSK tunnel connection from the proxy client. - * Establishes a single TLS-PSK tunnel connection to an exit node. - * Relays frames between the client and the exit node. + * Exposes two ports: + * 1. Client port: Listens for TLS-PSK tunnel connections from proxy clients + * 2. Exit port: Listens for TLS-PSK tunnel connections from exit nodes + * + * Relays frames between connected clients and exits. */ const tls = require('tls'); @@ -13,12 +15,12 @@ const fs = require('fs'); const { program } = require('commander'); program - .requiredOption('-P, --tunnel-port ', 'Port for proxy client TLS-PSK tunnel connections') + .requiredOption('-C, --client-port ', 'Port for proxy client TLS-PSK tunnel connections') + .requiredOption('-E, --exit-port ', 'Port for exit node TLS-PSK tunnel connections') .requiredOption('-H, --host ', 'Host to bind to (e.g., 0.0.0.0)') .requiredOption('--psk-file ', 'Path to PSK key file for client and exit connections') - .requiredOption('--exit-host ', 'Exit node host') - .requiredOption('--exit-port ', 'Exit node port') - .requiredOption('--exit-identity ', 'PSK identity for the exit node') + .requiredOption('--client-identity ', 'Expected PSK identity for client connections') + .requiredOption('--exit-identity ', 'Expected PSK identity for exit connections') .parse(); const options = program.opts(); @@ -80,60 +82,31 @@ function writeMessage(socket, type, connectionId, data = Buffer.alloc(0)) { socket.write(buf); } -function connectToExitNode() { - console.log(`Connecting to exit node ${options.exitHost}:${options.exitPort}...`); - - const pskCb = () => ({ - identity: options.exitIdentity, - psk: Buffer.from(pskKey, 'hex'), - }); - - const sock = tls.connect( - { - host: options.exitHost, - port: parseInt(options.exitPort, 10), - pskCallback: pskCb, - ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256', - checkServerIdentity: () => undefined, - }, - () => { - console.log('Connected to exit node'); - } - ); - - sock.setNoDelay(true); - sock.setKeepAlive(true, 30000); - - exitSocket = sock; - exitReader = createMessageReader((type, connId, data) => { - // Forward messages from exit to client - if (clientSocket && !clientSocket.destroyed) { - writeMessage(clientSocket, type, connId, data); - } - }); - - sock.on('data', (data) => exitReader(data)); - - sock.on('close', () => { - console.log('Disconnected from exit node. Retrying in 2s...'); - exitSocket = null; - // If client is still here, it will be disconnected by the client server logic - if (clientSocket) { - try { clientSocket.destroy(); } catch (_) {} - } - setTimeout(connectToExitNode, 2000); - }); - - sock.on('error', (err) => { - console.error('Exit node connection error:', err.message); - }); -} - +// Client PSK callback const clientPskCallback = (socket, identity) => { console.log(`Client identity: ${identity}`); + + if (identity !== options.clientIdentity) { + console.warn(`Client PSK identity mismatch. Expected '${options.clientIdentity}', got '${identity}'.`); + return null; + } + return Buffer.from(pskKey, 'hex'); }; +// Exit PSK callback +const exitPskCallback = (socket, identity) => { + console.log(`Exit identity: ${identity}`); + + if (identity !== options.exitIdentity) { + console.warn(`Exit PSK identity mismatch. Expected '${options.exitIdentity}', got '${identity}'.`); + return null; + } + + return Buffer.from(pskKey, 'hex'); +}; + +// Create server for client connections const clientServer = tls.createServer( { pskCallback: clientPskCallback, @@ -173,16 +146,60 @@ const clientServer = tls.createServer( } ); -clientServer.listen(parseInt(options.tunnelPort, 10), options.host, () => { - console.log(`PSK Proxy Relay-Node listening for clients on ${options.host}:${options.tunnelPort}`); +// Create server for exit connections +const exitServer = tls.createServer( + { + pskCallback: exitPskCallback, + ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256', + }, + (socket) => { + if (exitSocket) { + console.log('Rejecting new exit connection, already have one.'); + socket.destroy(); + return; + } + console.log('Exit node connected'); + exitSocket = socket; + + socket.setNoDelay(true); + socket.setKeepAlive(true, 30000); + + exitReader = createMessageReader((type, connId, data) => { + // Forward messages from exit to client + if (clientSocket && !clientSocket.destroyed) { + writeMessage(clientSocket, type, connId, data); + } + }); + + socket.on('data', (data) => exitReader(data)); + + socket.on('close', () => { + console.log('Exit node disconnected'); + exitSocket = null; + // When exit disconnects, we can optionally close the client connection + // or keep it alive. For simplicity, we keep it. + }); + + socket.on('error', (err) => { + console.error('Exit socket error:', err.message); + }); + } +); + +// Start listening for client connections +clientServer.listen(parseInt(options.clientPort, 10), options.host, () => { + console.log(`PSK Proxy Relay-Node listening for clients on ${options.host}:${options.clientPort}`); }); -// Start connection to the exit node -connectToExitNode(); +// Start listening for exit connections +exitServer.listen(parseInt(options.exitPort, 10), options.host, () => { + console.log(`PSK Proxy Relay-Node listening for exit nodes on ${options.host}:${options.exitPort}`); +}); process.on('SIGINT', () => { console.log('Shutting down...'); try { clientServer.close(); } catch (_) {} + try { exitServer.close(); } catch (_) {} if (clientSocket) try { clientSocket.destroy(); } catch (_) {} if (exitSocket) try { exitSocket.destroy(); } catch (_) {} process.exit(0);