|
#!/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; |
|
} |
|
} |