Skip to content

Instantly share code, notes, and snippets.

@lideming
Last active October 24, 2023 17:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lideming/51e1b38290bb473fecc5cf6d50040f69 to your computer and use it in GitHub Desktop.
Save lideming/51e1b38290bb473fecc5cf6d50040f69 to your computer and use it in GitHub Desktop.
deno run --allow-net --unstable portforward.ts 443 github.com 443

# Or, install it as CLI and run
cp portforward.ts /usr/bin/portforward
portforward 443 github.com 443

Arguments:

[bind_addr] bind_port target_addr target_port

See also this gist: TCP Socket activation process/docker by Deno

#!/usr/bin/env -S deno run --allow-net --unstable
const UDP_ENABLED = true;
const UDP_SESSION_TIMEOUT = 30_000;
const DEBUG = false;
runFromArgs();
function runFromArgs() {
const args = [...Deno.args];
if (args.length !== 3 && args.length !== 4) {
console.error(
"Usage: portforward [bind_addr] bind_port target_addr target_port",
);
Deno.exit(1);
}
if (args.length === 3) {
args.unshift("0.0.0.0");
}
portForward(
{ hostname: args[0], port: +args[1] },
{ hostname: args[2], port: +args[3] },
UDP_ENABLED,
);
}
function formatAddr(clientAddr: Deno.Addr) {
//@ts-expect-error
return `[${clientAddr.hostname}]:${clientAddr.port}`;
}
type HostnamePort = { hostname: string; port: number };
async function portForward(
listenAddr: HostnamePort,
targetAddr: HostnamePort,
withUdp = false,
) {
const listener = Deno.listen(listenAddr);
console.info("TCP listening", listenAddr);
if (withUdp) udpPortForward(listenAddr, targetAddr);
while (true) {
const conn = await listener.accept();
console.info("new TCP connection from", formatAddr(conn.remoteAddr));
onConnection(conn);
}
async function onConnection(client: Deno.Conn) {
let target;
try {
target = await Deno.connect(targetAddr);
console.info("connected to target");
await Promise.all([
tcpCopy(client, target, "sent"),
tcpCopy(target, client, "received"),
]);
} catch (error) {
console.error("connection error", error);
} finally {
client.close();
target?.close();
console.info("connection closed");
}
}
}
async function tcpCopy(from: Deno.Conn, to: Deno.Conn, direction: string) {
const buf = new Uint8Array(64 * 1024);
while (true) {
const haveRead = await from.read(buf);
if (!haveRead) break;
let haveWritten = 0;
while (haveWritten < haveRead) {
haveWritten += await to.write(buf.subarray(haveWritten, haveRead));
}
DEBUG && console.info("tcp " + direction, haveRead);
}
console.info("close from", formatAddr(from.remoteAddr));
await to.closeWrite();
}
async function udpPortForward(
listenAddr: HostnamePort,
targetAddr: HostnamePort,
) {
const listener = Deno.listenDatagram({ ...listenAddr, transport: "udp" });
const clientMap = new Map<
string, // downstream addr
{ conn: Deno.DatagramConn; lastActive: number }
>();
setInterval(() => {
// remove inactive sessions
for (const [addr, upstream] of clientMap) {
if (Date.now() - upstream.lastActive > UDP_SESSION_TIMEOUT) {
console.info(
"udp session timed out",
`${addr}->${formatAddr(upstream.conn.addr)}`,
);
clientMap.delete(addr);
upstream.lastActive = 0; // to ignore read error
upstream.conn.close();
}
}
}, 5000);
console.info("UDP listening", listenAddr);
while (true) {
const [data, clientAddr] = await listener.receive();
let upstream = clientMap.get(formatAddr(clientAddr as Deno.NetAddr));
if (!upstream) {
upstream = handleNewClient(clientAddr);
}
const haveSent = await upstream.conn.send(data, {
...targetAddr,
transport: "udp",
});
if (haveSent !== data.length) throw new Error("UDP haveSent !== data.length");
DEBUG && console.info("udp sent", data.length);
upstream.lastActive = Date.now();
}
function handleNewClient(clientAddr: Deno.Addr) {
const upstream = {
conn: Deno.listenDatagram({
hostname: "0.0.0.0",
port: 0,
transport: "udp",
}),
lastActive: Date.now(),
};
console.info(
"new udp session",
`${formatAddr(clientAddr)}->${formatAddr(
upstream.conn.addr as Deno.NetAddr,
)}`,
);
clientMap.set(formatAddr(clientAddr), upstream);
// upstream -> client
(async () => {
try {
while (true) {
const [data] = await upstream.conn.receive();
DEBUG && console.info("udp received", data.length);
// Didn't check the source addr, so it's like Full Cone NAT
const haveSent = await listener.send(data, clientAddr);
if (haveSent !== data.length)
throw new Error("UDP haveSent !== data.length");
upstream.lastActive = Date.now();
}
} catch (error) {
if (upstream.lastActive) {
console.error(error);
} // else it's because of timeout
}
})();
return upstream;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment