use socks5 instead of HTTP Proxy to allow more protocols
This commit is contained in:
285
proxy-server.js
285
proxy-server.js
@@ -3,23 +3,33 @@
|
||||
/**
|
||||
* PSK Proxy Out-Node (Server)
|
||||
*
|
||||
* Listens for a single TLS-PSK tunnel connection from the proxy client.
|
||||
* Listens for a TLS-PSK tunnel connection from the proxy 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.
|
||||
*
|
||||
* Protocol (over a single TLS socket):
|
||||
* Header: [1 byte type][4 bytes connection id][4 bytes data length][data...]
|
||||
*
|
||||
* Message Types:
|
||||
* DATA (2): Carry stream data
|
||||
* CLOSE (3): Close a stream
|
||||
* OPEN (4): Open stream to host:port, payload = [2B hostLen][host][2B port]
|
||||
* OPEN_RESULT (5): Result for OPEN, payload = [1B status] (1 = success, 0 = failure)
|
||||
* DATA (2): Carry TCP stream data
|
||||
* CLOSE (3): Close a TCP stream
|
||||
* OPEN (4): Open TCP stream to host:port, payload = [2B hostLen][host][2B port]
|
||||
* OPEN_RESULT (5): Result for OPEN, payload = [1B status] (1 = success, 0 = failure)
|
||||
*
|
||||
* UDP_OPEN (6): Create a UDP association (bidirectional relay); payload = empty
|
||||
* UDP_OPEN_RESULT (7): Result for UDP_OPEN, payload = [1B status] (1 = success, 0 = failure)
|
||||
* UDP_SEND (8): Send one UDP datagram to host:port
|
||||
* payload = [2B hostLen][host][2B port][2B dataLen][data...]
|
||||
* UDP_RECV (9): UDP datagram received from remote; same payload format as UDP_SEND
|
||||
* UDP_CLOSE (10): Close UDP association; payload = empty
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const fs = require('fs');
|
||||
const dgram = require('dgram');
|
||||
const { program } = require('commander');
|
||||
|
||||
program
|
||||
@@ -47,6 +57,12 @@ const MSG_TYPES = {
|
||||
CLOSE: 3,
|
||||
OPEN: 4,
|
||||
OPEN_RESULT: 5,
|
||||
|
||||
UDP_OPEN: 6,
|
||||
UDP_OPEN_RESULT: 7,
|
||||
UDP_SEND: 8,
|
||||
UDP_RECV: 9,
|
||||
UDP_CLOSE: 10,
|
||||
};
|
||||
|
||||
function writeMessage(socket, type, connectionId, data = Buffer.alloc(0)) {
|
||||
@@ -109,6 +125,32 @@ function buildOpenResultPayload(success) {
|
||||
return b;
|
||||
}
|
||||
|
||||
function parseUdpPayload(buf) {
|
||||
// [2B hostLen][host][2B port][2B dataLen][data...]
|
||||
if (buf.length < 2) return null;
|
||||
let off = 0;
|
||||
const hostLen = buf.readUInt16BE(off); off += 2;
|
||||
if (buf.length < 2 + hostLen + 2 + 2) return null;
|
||||
const host = buf.subarray(off, off + hostLen).toString('utf8'); off += hostLen;
|
||||
const port = buf.readUInt16BE(off); off += 2;
|
||||
const dataLen = buf.readUInt16BE(off); off += 2;
|
||||
if (buf.length < 2 + hostLen + 2 + 2 + dataLen) return null;
|
||||
const data = buf.subarray(off, off + dataLen);
|
||||
return { host, port, data };
|
||||
}
|
||||
|
||||
function buildUdpPayload(host, port, data) {
|
||||
const hostBuf = Buffer.from(host, 'utf8');
|
||||
const buf = Buffer.allocUnsafe(2 + hostBuf.length + 2 + 2 + data.length);
|
||||
let off = 0;
|
||||
buf.writeUInt16BE(hostBuf.length, off); off += 2;
|
||||
hostBuf.copy(buf, off); off += hostBuf.length;
|
||||
buf.writeUInt16BE(port, off); off += 2;
|
||||
buf.writeUInt16BE(data.length, off); off += 2;
|
||||
data.copy(buf, off);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function pskCallback(socket, identity) {
|
||||
console.log(`Tunnel client identity: ${identity}`);
|
||||
return Buffer.from(pskKey, 'hex');
|
||||
@@ -117,109 +159,181 @@ function pskCallback(socket, identity) {
|
||||
let tunnelSocket = null;
|
||||
const upstreamConns = new Map(); // connectionId -> net.Socket
|
||||
|
||||
// UDP association mapping: connectionId -> { udp4?: dgram.Socket, udp6?: dgram.Socket }
|
||||
const udpAssoc = new Map();
|
||||
|
||||
function closeAllUpstreams() {
|
||||
for (const [id, s] of upstreamConns) {
|
||||
for (const [, s] of upstreamConns) {
|
||||
try { s.destroy(); } catch (_) {}
|
||||
}
|
||||
upstreamConns.clear();
|
||||
}
|
||||
|
||||
const server = tls.createServer({
|
||||
pskCallback,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
}, (socket) => {
|
||||
console.log('Proxy tunnel client connected');
|
||||
tunnelSocket = socket;
|
||||
function closeAllUdp() {
|
||||
for (const [, obj] of udpAssoc) {
|
||||
try { obj.udp4 && obj.udp4.close(); } catch (_) {}
|
||||
try { obj.udp6 && obj.udp6.close(); } catch (_) {}
|
||||
}
|
||||
udpAssoc.clear();
|
||||
}
|
||||
|
||||
socket.setNoDelay(true);
|
||||
socket.setKeepAlive(true, 30000);
|
||||
function ensureUdpSocketsForAssoc(connectionId) {
|
||||
if (udpAssoc.has(connectionId)) return udpAssoc.get(connectionId);
|
||||
const onMessage = (msg, rinfo) => {
|
||||
// Forward incoming datagrams to client
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_RECV, connectionId, buildUdpPayload(rinfo.address, rinfo.port, msg));
|
||||
};
|
||||
// Create both IPv4 and IPv6 sockets to support all targets
|
||||
const u4 = dgram.createSocket('udp4');
|
||||
const u6 = dgram.createSocket('udp6');
|
||||
|
||||
const reader = createMessageReader();
|
||||
u4.on('message', onMessage);
|
||||
u6.on('message', onMessage);
|
||||
|
||||
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(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
return;
|
||||
}
|
||||
u4.on('error', (err) => { /* log but keep running */ console.warn(`UDP4 error (conn ${connectionId}): ${err.message}`); });
|
||||
u6.on('error', (err) => { /* log but keep running */ console.warn(`UDP6 error (conn ${connectionId}): ${err.message}`); });
|
||||
|
||||
const { host, port } = spec;
|
||||
// Create outbound TCP connection
|
||||
const upstream = net.createConnection({ host, port });
|
||||
upstream.setNoDelay(true);
|
||||
upstream.setKeepAlive(true, 30000);
|
||||
// Bind to ephemeral ports to receive replies
|
||||
try { u4.bind(0); } catch (_) {}
|
||||
try { u6.bind(0); } catch (_) {}
|
||||
|
||||
let connected = false;
|
||||
const connectTimer = setTimeout(() => {
|
||||
if (!connected) {
|
||||
upstream.destroy(new Error('Connect timeout'));
|
||||
const entry = { udp4: u4, udp6: u6 };
|
||||
udpAssoc.set(connectionId, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
const server = tls.createServer(
|
||||
{
|
||||
pskCallback,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
},
|
||||
(socket) => {
|
||||
console.log('Proxy tunnel client connected');
|
||||
tunnelSocket = socket;
|
||||
|
||||
socket.setNoDelay(true);
|
||||
socket.setKeepAlive(true, 30000);
|
||||
|
||||
const reader = createMessageReader();
|
||||
|
||||
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(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
return;
|
||||
}
|
||||
}, OUT_CONNECT_TIMEOUT);
|
||||
|
||||
upstream.once('connect', () => {
|
||||
connected = true;
|
||||
clearTimeout(connectTimer);
|
||||
upstreamConns.set(connectionId, upstream);
|
||||
// Notify open success
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
});
|
||||
const { host, port } = spec;
|
||||
// Create outbound TCP connection
|
||||
const upstream = net.createConnection({ host, port });
|
||||
upstream.setNoDelay(true);
|
||||
upstream.setKeepAlive(true, 30000);
|
||||
|
||||
upstream.on('data', (chunk) => {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, connectionId, chunk);
|
||||
});
|
||||
let connected = false;
|
||||
const connectTimer = setTimeout(() => {
|
||||
if (!connected) {
|
||||
upstream.destroy(new Error('Connect timeout'));
|
||||
}
|
||||
}, OUT_CONNECT_TIMEOUT);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(connectTimer);
|
||||
if (upstreamConns.get(connectionId) === upstream) {
|
||||
upstream.once('connect', () => {
|
||||
connected = true;
|
||||
clearTimeout(connectTimer);
|
||||
upstreamConns.set(connectionId, upstream);
|
||||
// Notify open success
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
});
|
||||
|
||||
upstream.on('data', (chunk) => {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, connectionId, chunk);
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(connectTimer);
|
||||
if (upstreamConns.get(connectionId) === upstream) {
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, connectionId);
|
||||
};
|
||||
|
||||
upstream.on('error', (err) => {
|
||||
if (!connected) {
|
||||
clearTimeout(connectTimer);
|
||||
writeMessage(tunnelSocket, 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();
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, connectionId);
|
||||
};
|
||||
|
||||
upstream.on('error', (err) => {
|
||||
if (!connected) {
|
||||
clearTimeout(connectTimer);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
} else {
|
||||
// Upstream error after established
|
||||
cleanup();
|
||||
// UDP handling
|
||||
} else if (type === MSG_TYPES.UDP_OPEN) {
|
||||
try {
|
||||
ensureUdpSocketsForAssoc(connectionId);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
} catch (e) {
|
||||
console.warn(`Failed to create UDP association for ${connectionId}: ${e.message}`);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
}
|
||||
});
|
||||
|
||||
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.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
|
||||
}
|
||||
} else if (type === MSG_TYPES.CLOSE) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream) {
|
||||
upstream.destroy();
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
} else {
|
||||
// ignore unknown types
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('Proxy tunnel disconnected');
|
||||
tunnelSocket = null;
|
||||
closeAllUpstreams();
|
||||
});
|
||||
socket.on('close', () => {
|
||||
console.log('Proxy tunnel disconnected');
|
||||
tunnelSocket = null;
|
||||
closeAllUpstreams();
|
||||
closeAllUdp();
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Tunnel socket error:', err.message);
|
||||
});
|
||||
});
|
||||
socket.on('error', (err) => {
|
||||
console.error('Tunnel socket error:', err.message);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
server.listen(parseInt(options.tunnelPort, 10), options.host, () => {
|
||||
console.log(`PSK Proxy Out-Node listening on ${options.host}:${options.tunnelPort}`);
|
||||
@@ -229,5 +343,6 @@ process.on('SIGINT', () => {
|
||||
console.log('Shutting down...');
|
||||
try { server.close(); } catch (_) {}
|
||||
closeAllUpstreams();
|
||||
closeAllUdp();
|
||||
process.exit(0);
|
||||
});
|
||||
|
Reference in New Issue
Block a user