Port Forwarding with Go

Local Port Forwarding as well as Network Tunnelling are useful tools for debugging, navigating infrastructure and much, much more. They’re frequently used by sysadmins and ops-engineers, but also provide some nice use-cases for developers.

In this post, we will take a look at how simple it is to do local port forwarding with just the Go standard library and how powerful this concept can be as a building block for more interesting use-cases.

The basic idea in the following example is to send traffic on some local port, which is forwarded to another machine, which then sends it to some service.

This could be useful, for example, if this service is only accessible from the forwarding machine, but not locally.

Of course, there are two components to this. A local part and a remote part. However, these two parts do the same thing, they accept some TCP traffic on a port and forward it to a specified target.

As an example, we could try to forward HTTP traffic from our local browser to a remote nginx instance via our tool. We can even simulate this locally, without a remote server.

First, we start an nginx instance on port 8181 (e.g.: with Docker) like this:

docker run -d -p 8181:80 nginx

And then we start two versions of our program:

go run main.go --target 127.0.0.1:8181 --port 1337

This is the remote server, which accepts traffic on port 1337 and forwards it to the nginx instance.

go run main.go --target 127.0.0.1:1337 --port 1338

This is the local service, which accepts traffic on 1338, e.g. from a browser, and forwards it to our remote service.

With a setup like this, it should be possible for traffic on port 1338 to go through both applications to nginx on port 8181 and back again seamlessly.

And, of course, the same thing also works over the network. For example, one could connect to a remote server via SSH on the remote service and provide this service to the local service on some given TCP port.

The same concept works for any TCP-based protocol and we have all the building blocks we need to build ourselves a tunnelling protocol. Pretty Neat!

Alright, now that we have a rough idea of what we’re trying to build here, let’s look at a simple implementation:

Example

First off, we use the flag package to parse the incoming target and port parameters.

var (
    target string
    port   int
)

func init() {
    flag.StringVar(&target, "target", "", "target (<host>:<port>)")
    flag.IntVar(&port, "port", 1337, "port")
}

Then, we start a server on the given port, so clients can connect to our service (1337 and 1338 in the above example).

    incoming, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
    if err != nil {
        log.Fatalf("could not start server on %d: %v", port, err)
    }
    fmt.Printf("server running on %d\n", port)

We listen for incoming connections and accept the first one, creating our client. This version only works for one connected client, but it wouldn’t be hard to change this to a multi-client solution by creating a goroutine for each connected client.

    client, err := incoming.Accept()
    if err != nil {
        log.Fatal("could not accept client connection", err)
    }
    defer client.Close()
    fmt.Printf("client '%v' connected!\n", client.RemoteAddr())

Finally, we connect to the specified target server (nginx and the remote service in the example).

    target, err := net.Dial("tcp", target)
    if err != nil {
        log.Fatal("could not connect to target", err)
    }
    defer target.Close()
    fmt.Printf("connection to server %v established!\n", target.RemoteAddr())

Alright, we have a server which accepts incoming connections and is connected to the target server. Now we need a way to move the traffic between client and target.

Luckily, both client and target are of type net.Conn and satisfy both the io.Reader and io.Writer interfaces. So, passing the incoming traffic to the outgoing connection and vice versa is trivial using io.Copy.

    go func() { io.Copy(target, client) }()
    go func() { io.Copy(client, target) }()

If we would start the program like this however, it would instantly run through and quit after the first connection. For this purpose, we introduce a stop channel, which blocks at the end of the program and reacts to an os.Signal.

This way the application will continue running until e.g.: CTRL+C is pressed.

func main() {
    ...flag code...

    signals := make(chan os.Signal, 1)
    stop := make(chan bool)
    signal.Notify(signals, os.Interrupt)
    go func() {
        for _ = range signals {
            fmt.Println("\nReceived an interrupt, stopping...")
                stop <- true
        }
    }()

    ...forwarding code...

    <-stop
}

And that’s it.

If we run this application as specified in the beginning of the post, with one local and one remote part and navigate to http://localhost:1338 we can see that the traffic is passed through successfully.

Although this is just a fairly useless HTTP proxy at this state, there are lots of things one could accomplish by extending this simple idea.

For example, you could monitor, multiplex, filter, manipulate or encrypt traffic… endless possibilities! :)

Conclusion

This was another nice little example made possible by the power of Go’s Reader and Writer interfaces.

Also, this example only uses a bare minimum of functionality, all from the standard library, without being overly verbose, which speaks for Go’s networking primitives.

Have fun forwarding! :)

Resources


I work as a freelance software engineer and trainer and you can hire me. If you like my work, or my writing and if I sound like someone you'd like to work with, let's talk! :)