@xutl/forward
Version:
Port Forwarding
153 lines (152 loc) • 5.25 kB
JavaScript
import * as Util from 'node:util';
import * as Net from 'node:net';
import * as Events from 'node:events';
import * as DNS from 'node:dns';
import * as OS from 'node:os';
export class PortForward extends Events.EventEmitter {
#server;
#sockets = new Set();
constructor(listen, destination) {
super();
this.#server = Net.createServer((socket) => {
const remote = { address: socket.remoteAddress, port: socket.remotePort };
this.emit('connection', remote);
socket.on('close', () => this.emit('disconnection', remote));
const other = Net.createConnection(destination.port, destination.address);
this.#forward(socket, other);
});
this.#server
.on('error', (err) => {
this.#server.close();
this.emit('error', err);
})
.on('close', () => {
this.emit('close');
})
.on('listening', () => {
this.emit('listening');
})
.listen(listen.port, listen.address);
}
#forward(one, two) {
this.#sockets.add(one);
this.#sockets.add(two);
one.pipe(two);
two.pipe(one);
one
.on('error', (e) => this.emit('error', e))
.on('close', () => {
this.#sockets.delete(one);
this.emit('close');
});
two
.on('error', (e) => this.emit('error', e))
.on('close', () => {
this.#sockets.delete(two);
this.emit('close');
});
}
close() {
this.#server.close();
for (const socket of this.#sockets.values()) {
try {
socket.end();
}
catch { }
}
}
}
export default PortForward;
async function main() {
const { values: args, positionals: portsargs } = Util.parseArgs({
options: {
host: {
type: 'string',
short: 'a',
},
},
allowPositionals: true,
});
const ports = portsargs
.flatMap((item) => {
const [source, target = source] = `${item}`.split(':').map((p) => +p.trim());
if (!source || !target)
return undefined;
return { source, target };
})
.filter((x) => !!x);
if (!ports.length) {
console.error(`Usage: npx run @xutl/forward [--host=<address>] <source>:<target> [<source>:<target>...]`);
process.exit(1);
}
const inits = [];
if (args.host) {
const host = (await DNS.promises.lookup(args.host)).address;
const local = Object.values(OS.networkInterfaces())
.flat()
.find((iface) => !iface?.internal && iface?.address === host);
const sourceHost = local ? args.host : 'localhost';
const targetHost = local ? 'localhost' : args.host;
for (const address of await DNS.promises.lookup(sourceHost, { all: true })) {
for (const { source, target } of ports) {
const listen = { address: address.address, port: source };
const destination = { address: targetHost, port: target };
inits.push([listen, destination]);
}
}
}
else {
for (const addr of Object.values(OS.networkInterfaces()).flat()) {
if (!addr || addr.internal)
continue;
for (const { source, target } of ports) {
const listen = { address: addr.address, port: source };
const destination = { address: 'localhost', port: target };
inits.push([listen, destination]);
}
}
}
const forwards = new Map();
for (const [listen, target] of inits)
setupForward(forwards, listen, target);
const stop = () => {
process.removeListener('SIGINT', stop);
const fwds = forwards.values();
forwards.clear();
for (const fwd of fwds)
fwd.close();
Promise.resolve().then(() => process.exit());
};
process.on('SIGINT', stop);
}
function setupForward(forwards, listen, destination) {
let respawn = true;
const id = `${listen.port}:${destination.port}`;
const fwd = new PortForward(listen, destination);
fwd
.on('error', (err) => {
if (['EADDRNOTAVAIL', 'EADDRINUSE'].includes(err.code)) {
respawn = false;
return;
}
console.error(`${id}`, err);
})
.on('close', () => {
if (!forwards.size)
return;
forwards.delete(id);
if (respawn)
setupForward(forwards, listen, destination);
})
.on('listening', () => {
console.log(`forwarding ${listen.address}:${listen.port} to ${destination.address}:${destination.port}`);
})
.on('connection', (client) => {
console.log(`connected ${client.address}:${client.port} via ${listen.address}:${listen.port} to ${destination.address}:${destination.port}`);
});
forwards.set(id, fwd);
}
//@ts-expect-error
if (import.meta.main || (process.argv[1] === import.meta.filename))
main();