exit node as client

This commit is contained in:
2025-09-28 09:35:23 -04:00
parent 3539b21f49
commit 2c47716c4e
3 changed files with 226 additions and 207 deletions

7
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{ {
"name": "psk-proxy-tunnel", "name": "psk-proxy-tunnel",
"version": "1.0.0", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "psk-proxy-tunnel", "name": "psk-proxy-tunnel",
"version": "1.0.0", "version": "1.1.0",
"dependencies": { "dependencies": {
"commander": "^14.0.0" "commander": "^14.0.0"
}, },
"bin": { "bin": {
"psk-proxy-client": "proxy-client.js", "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": { "devDependencies": {
"pkg": "^5.8.1" "pkg": "^5.8.1"

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env node #!/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, * Receives OPEN(host,port) to create outbound TCP connections to remote servers,
* then forwards DATA/CLOSE frames bidirectionally. * 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'); const net = require('net');
@@ -17,11 +17,12 @@ const dgram = require('dgram');
const { program } = require('commander'); const { program } = require('commander');
program program
.requiredOption('-P, --relay-port <port>', 'Port for proxy relay TLS-PSK tunnel connections') .requiredOption('-H, --relay-host <host>', 'Relay server host to connect to')
.requiredOption('-H, --host <host>', 'Host to bind to (e.g., 0.0.0.0)') .requiredOption('-P, --relay-port <port>', 'Relay server port for exit connections')
.requiredOption('--psk-file <path>', 'Path to PSK key file') .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('--connect-timeout <ms>', 'Timeout for outbound TCP connect (ms)', '10000')
.option('--reconnect-delay <ms>', 'Delay before reconnecting to relay (ms)', '2000')
.parse(); .parse();
const options = program.opts(); const options = program.opts();
@@ -35,6 +36,7 @@ try {
} }
const OUT_CONNECT_TIMEOUT = parseInt(options.connectTimeout, 10) || 10000; const OUT_CONNECT_TIMEOUT = parseInt(options.connectTimeout, 10) || 10000;
const RECONNECT_DELAY = parseInt(options.reconnectDelay, 10) || 2000;
// Message Types // Message Types
const MSG_TYPES = { const MSG_TYPES = {
@@ -136,20 +138,7 @@ function buildUdpPayload(host, port, data) {
return buf; return buf;
} }
function pskCallback(socket, identity) { // Global state
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');
}
let relaySocket = null; let relaySocket = null;
const upstreamConns = new Map(); // connectionId -> net.Socket const upstreamConns = new Map(); // connectionId -> net.Socket
@@ -277,21 +266,34 @@ function ensureUdpSocketsForAssoc(connectionId) {
return entry; return entry;
} }
const server = tls.createServer( function connectToRelay() {
console.log(`Connecting to relay server ${options.relayHost}:${options.relayPort} via TLS-PSK...`);
const pskCb = () => ({
identity: options.identity,
psk: Buffer.from(pskKey, 'hex'),
});
const sock = tls.connect(
{ {
pskCallback, host: options.relayHost,
port: parseInt(options.relayPort, 10),
pskCallback: pskCb,
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256', ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
checkServerIdentity: () => undefined,
}, },
(socket) => { () => {
console.log('Proxy relay connected'); console.log('Connected to relay server');
relaySocket = socket; }
);
socket.setNoDelay(true); sock.setNoDelay(true);
socket.setKeepAlive(true, 30000); sock.setKeepAlive(true, 30000);
relaySocket = sock;
const reader = createMessageReader(); const reader = createMessageReader();
socket.on('data', (data) => { sock.on('data', (data) => {
reader(data, (type, connectionId, payload) => { reader(data, (type, connectionId, payload) => {
if (type === MSG_TYPES.OPEN) { if (type === MSG_TYPES.OPEN) {
const spec = parseOpenPayload(payload); const spec = parseOpenPayload(payload);
@@ -400,26 +402,25 @@ const server = tls.createServer(
}); });
}); });
socket.on('close', () => { sock.on('close', () => {
console.log('Proxy relay disconnected'); console.log(`Disconnected from relay server. Retrying in ${RECONNECT_DELAY}ms...`);
relaySocket = null; relaySocket = null;
closeAllUpstreams(); closeAllUpstreams();
closeAllUdp(); closeAllUdp();
setTimeout(connectToRelay, RECONNECT_DELAY);
}); });
socket.on('error', (err) => { sock.on('error', (err) => {
console.error('Relay socket error:', err.message); console.error('Relay connection error:', err.message);
}); });
} }
);
server.listen(parseInt(options.relayPort, 10), options.host, () => { // Start connection to the relay server
console.log(`PSK Proxy Exit-Node listening on ${options.host}:${options.relayPort}`); connectToRelay();
});
process.on('SIGINT', () => { process.on('SIGINT', () => {
console.log('Shutting down...'); console.log('Shutting down...');
try { server.close(); } catch (_) {} if (relaySocket) try { relaySocket.destroy(); } catch (_) {}
closeAllUpstreams(); closeAllUpstreams();
closeAllUdp(); closeAllUdp();
process.exit(0); process.exit(0);

View File

@@ -3,9 +3,11 @@
/** /**
* PSK Proxy Relay-Node (Server) * PSK Proxy Relay-Node (Server)
* *
* Listens for a TLS-PSK tunnel connection from the proxy client. * Exposes two ports:
* Establishes a single TLS-PSK tunnel connection to an exit node. * 1. Client port: Listens for TLS-PSK tunnel connections from proxy clients
* Relays frames between the client and the exit node. * 2. Exit port: Listens for TLS-PSK tunnel connections from exit nodes
*
* Relays frames between connected clients and exits.
*/ */
const tls = require('tls'); const tls = require('tls');
@@ -13,12 +15,12 @@ const fs = require('fs');
const { program } = require('commander'); const { program } = require('commander');
program program
.requiredOption('-P, --tunnel-port <port>', 'Port for proxy client TLS-PSK tunnel connections') .requiredOption('-C, --client-port <port>', 'Port for proxy client TLS-PSK tunnel connections')
.requiredOption('-E, --exit-port <port>', 'Port for exit node TLS-PSK tunnel connections')
.requiredOption('-H, --host <host>', 'Host to bind to (e.g., 0.0.0.0)') .requiredOption('-H, --host <host>', 'Host to bind to (e.g., 0.0.0.0)')
.requiredOption('--psk-file <path>', 'Path to PSK key file for client and exit connections') .requiredOption('--psk-file <path>', 'Path to PSK key file for client and exit connections')
.requiredOption('--exit-host <host>', 'Exit node host') .requiredOption('--client-identity <identity>', 'Expected PSK identity for client connections')
.requiredOption('--exit-port <port>', 'Exit node port') .requiredOption('--exit-identity <identity>', 'Expected PSK identity for exit connections')
.requiredOption('--exit-identity <identity>', 'PSK identity for the exit node')
.parse(); .parse();
const options = program.opts(); const options = program.opts();
@@ -80,60 +82,31 @@ function writeMessage(socket, type, connectionId, data = Buffer.alloc(0)) {
socket.write(buf); socket.write(buf);
} }
function connectToExitNode() { // Client PSK callback
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);
});
}
const clientPskCallback = (socket, identity) => { const clientPskCallback = (socket, identity) => {
console.log(`Client identity: ${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'); 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( const clientServer = tls.createServer(
{ {
pskCallback: clientPskCallback, pskCallback: clientPskCallback,
@@ -173,16 +146,60 @@ const clientServer = tls.createServer(
} }
); );
clientServer.listen(parseInt(options.tunnelPort, 10), options.host, () => { // Create server for exit connections
console.log(`PSK Proxy Relay-Node listening for clients on ${options.host}:${options.tunnelPort}`); 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 // Start listening for exit connections
connectToExitNode(); 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', () => { process.on('SIGINT', () => {
console.log('Shutting down...'); console.log('Shutting down...');
try { clientServer.close(); } catch (_) {} try { clientServer.close(); } catch (_) {}
try { exitServer.close(); } catch (_) {}
if (clientSocket) try { clientSocket.destroy(); } catch (_) {} if (clientSocket) try { clientSocket.destroy(); } catch (_) {}
if (exitSocket) try { exitSocket.destroy(); } catch (_) {} if (exitSocket) try { exitSocket.destroy(); } catch (_) {}
process.exit(0); process.exit(0);