Search code examples
gonetwork-programmingtcpproxyhttp-proxy

Make reverse TCP connection accept any amount of connections (like a normal TCP server)


I'm trying to create a reverse proxy to a CONNECT-based HTTP proxy. The user who wants to use the proxy just treats machine A as an HTTP proxy. It works the following way:

  1. machine B opens a TCP socket to machine A.
  2. On machine A, a TCP socket is exposed on a port and all the incoming data is tunneled to machine B (io.Copy).
  3. On machine B, all the data is tunneled to the local HTTP server and the socket to machine A.

Essentially this is a reverse-proxy behind an HTTP proxy. The reason it's this complex is because the HTTP proxy is behind NAT (on machine B) and therefore not accessible directly. The use case is being able to host an HTTP proxy behind a NAT.

Machine A tunnel (Go):

package main

import (
    "log"
    "net"
)

func Conn(c *net.TCPConn) string {
    return c.RemoteAddr().String() + " (" + c.LocalAddr().String() + ")"
}

func ProxifyConns(recipientConn, donorConn *net.TCPConn) {
    log.Println("Proxying", ConnrecipientConn), "and", Conn(donorConn))
    go func() {
        _, err := io.Copy(recipientConn, donorConn)
        if err != nil {
            utils.StacktraceError(err)
        }
        recipientConn.Close()
    }()
    go func() {
        _, err := io.Copy(donorConn, recipientConn)
        if err != nil {
            utils.StacktraceError(err)
        }
        recipientConn.Close()
    }()
}


func main() {
    // Open the donor listener
    donorsAddr, err := net.ResolveTCPAddr("tcp4", ":11000")
    if err != nil {
        utils.StacktraceErrorAndExit(err)
    }
    listenerDonors, err := net.ListenTCP("tcp", donorsAddr)
    if err != nil {
        utils.StacktraceErrorAndExit(err)
    }
    defer listenerDonors.Close()
    log.Println("Listening for donors on", listenerDonors.Addr())

    // Open the recipient listener
    recipientsAddr, err := net.ResolveTCPAddr("tcp4", ":10000")
    if err != nil {
        utils.StacktraceErrorAndExit(err)
    }
    listenerRecipients, err := net.ListenTCP("tcp", recipientsAddr)
    if err != nil {
        utils.StacktraceErrorAndExit(err)
    }
    defer listenerRecipients.Close()
    log.Println("Listening for recipients on", listenerRecipients.Addr())

    //  Handle donor connections
    donorConns := make(chan *net.TCPConn)
    go func() {
        for {
            donorConn, err := listenerDonors.AcceptTCP()
            donorConn.SetKeepAlive(true)
            if err != nil {
                utils.StacktraceErrorAndExit(err)
                return
            }
            log.Println("New donor connection from", Conn(donorConn))
            donorConns <- donorConn
        }
    }()

    // Handle recipient connections
    for {
        recipientConn, err := listenerRecipients.AcceptTCP()
        recipientConn.SetKeepAlive(true)
        if err != nil {
            utils.StacktraceErrorAndExit(err)
            return
        }
        log.Println("New recipient connection from", Conn(recipientConn))
        donorConn := <-donorConns
        proxy.ProxifyConns(recipientConn, donorConn)
    }
}

Machine B tunnel (Node.js):

import net, { AddressInfo } from 'net';
import http from 'http';
import golgi from 'golgi';

export const startHttpProxy = () => {
  const server = http.createServer();
  let proxyServer: http.Server = golgi(server);
  // Listening to 0 assigns a random OS-assigned port
  proxyServer = proxyServer.listen(0);
  return proxyServer;
};

export const startDonorSocket = () => {
  const proxyServer = startHttpProxy();
  const proxyServerSocket = new net.Socket();
  proxyServerSocket.connect(
    (proxyServer.address() as AddressInfo).port,
    '127.0.0.1'
  );

  const donorSocket = new net.Socket();
  donorSocket.setKeepAlive(true);
  donorSocket.connect(11000, '2.226.102.14', () => {
    proxyServerSocket.pipe(donorSocket);
    donorSocket.pipe(proxyServerSocket);
  });
};

Unfortunately this works when tunneling to one TCP address but not when tunneling to more. If I open many Machine B tunnels (Node.js code), it works. What I mean is that a donor connection (Node.js) is "consumed" ever time it is taken by a recipient (HTTP proxy user) because a persistent TCP tunnel is made on it.

I wonder is there is a way to make this work for any amount of TCP connections, not just one. My only idea right now is to create more TCP donor connections every time a connection is consumed but I wonder if there is a simpler solution.


Solution

  • When you do

    go func() {
        for {
            donorConn, err := listenerDonors.AcceptTCP()
            donorConn.SetKeepAlive(true)
            if err != nil {
                utils.StacktraceErrorAndExit(err)
                return
            }
            log.Println("New donor connection from", Conn(donorConn))
            donorConns <- donorConn
        }
    }()
    

    You start processing the first TCP connection. This code blocks on donorConns <- donorConn. Until this send to channel finishes the loop won't go into the second iteration (and the next TCP connection won't be accepted).

    You do a very similar second loop

    // Handle recipient connections
    for {
        recipientConn, err := listenerRecipients.AcceptTCP()
        recipientConn.SetKeepAlive(true)
        if err != nil {
            utils.StacktraceErrorAndExit(err)
            return
        }
        log.Println("New recipient connection from", Conn(recipientConn))
        donorConn := <-donorConns
        proxy.ProxifyConns(recipientConn, donorConn)
    }
    

    which requires donorConn := <-donorConns to complete (from the first loop) and requires proxy.ProxifyConns(recipientConn, donorConn) to complete.

    I'm not sure how you intend the whole thing to work, but, most likely, you need a very minor change:
    go proxy.ProxifyConns(recipientConn, donorConn)