exit node as client
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -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"
|
||||||
|
@@ -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);
|
||||||
|
137
proxy-server.js
137
proxy-server.js
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start connection to the exit node
|
socket.on('data', (data) => exitReader(data));
|
||||||
connectToExitNode();
|
|
||||||
|
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 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', () => {
|
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);
|
||||||
|
Reference in New Issue
Block a user