Search code examples
proxydeno

Create TCP proxy with Deno without using node features


There is a common example out there for creating a TCP proxy server in nodejs (I can't find my original source but its essentially a small adaptation of the echo server)

nodejs boils down to:

var net = require('net');
net.createServer(function(from) {
    var to = net.createConnection({
        host: '<toHost>',
        port: <toPort>
    });
    from.on('error', (err) => {
        to.destroy(err);
    });
    to.on('error', (err) => {
        from.destroy(err);
    });
    from.pipe(to);
    to.pipe(from);
}).listen('<someport>', undefined);

Works great. if you change it to import net from "node:net"; it works with the compatibility layer in Deno.

But I was trying to get it to work with Deno's native API. I went to the echo server example https://docs.deno.com/runtime/tutorials/tcp_echo and tried to do the same, perform a connection and pipe them together:

const listener = Deno.listen({
    port: <fromPort>,
    host: undefined
});
try{
for await (const conn1 of listener) {
    try{
        const conn2 = await Deno.connect({
            host: '<toHost>',
            port: <toPort>   
        });

        if (conn2) {
            conn1.readable.pipeTo(conn2.writable);
            conn2.readable.pipeTo(conn1.writable);
        }
    } catch (e) { console.log(e); }

}} catch (e) { console.log(e); }

And it works! ...for about a minute. Then it exits with this error:

error: Uncaught (in promise) Interrupted: operation canceled
    at async Object.pull (ext:deno_web/06_streams.js:1001:27)

I wrapped all the awaits with try-catches but it doesn't catch the issue. behaves identically with and without them. I don't see from the docs or the code how to handle this. If a client is canceling I wouldn't expect the server to go down.


Solution

  • Looks like the issue is that NodeJS connection.pipe is not a promise but in Deno connection.pipeTo is a promise so they need to be awaited to be caught with try-catch (though you want them to run in parallel so a promise all works)

    The Interrupted error still happens when a connection gets cancelled but the server doesn't die.

    const listener = Deno.listen({
        port: <fromPort>,
        host: undefined
    });
    try{
    for await (const conn1 of listener) {
        try{
            const conn2 = await Deno.connect({
                host: '<toHost>',
                port: <toPort>   
            });
    
            if (conn2) {
             await Promise.all([
                conn1.readable.pipeTo(conn2.writable),
                conn2.readable.pipeTo(conn1.writable)
                ]);
            }
        } catch (e) { console.log(e); }
    
    }} catch (e) { console.log(e); }