SSH tunneling in Golang

In my previous post, I illustrated the basic usage of ssh package. In this article I will demonstrate how we should use it to implement SSH tunneling . We will forward connection to localhost:9000 through example.com:22 to localhost:8080 .

The tunneling protocol allows a network user to access or provide a network service that the underlying network does not support or provide directly.

We have four actors in this scenario:

client - the client that needs resource from remote server

- the client that needs resource from local server - a server accessible by the client

- a server accessible by the client intermediate server - a server accessible by the local server and remote/target server

- a server accessible by the local server and server remote/target server - a server running behind the intermediate server network

Each of this server endpoints can be represented by the following struct:

type Endpoint struct { // Server host address Host string // Server port Port int } func (endpoint *Endpoint) String() string { return fmt.Sprintf("%s:%d", endpoint.Host, endpoint.Port) }

Lets instanciate the each endpoint for these servers:

localEndpoint := &Endpoint{ Host: "localhost", Port: 9000, } serverEndpoint := &Endpoint{ Host: "example.com", Port: 22, } remoteEndpoint := &Endpoint{ Host: "localhost", Port: 8080, }

The client is connecting to local endpoint . Then the server endpoint mediates between local endpoint and remote endpoint .

The algorithms is encapsulated in SSHTunnel struct:

type SSHTunnel struct { Local *Endpoint Server *Endpoint Remote *Endpoint Config *ssh.ClientConfig }

We should establish our own local server by using net package and net.Listen function. For any client accepted by the listener, we are forwarding its request to the intermediate server via forward go routine function:

func (tunnel *SSHTunnel) Start() error { listener, err := net.Listen("tcp", tunnel.Local.String()) if err != nil { return err } defer listener.Close() for { conn, err := listener.Accept() if err != nil { return err } go tunnel.forward(conn) } }

Port forwarding is processed by establishing an SSH connection to the intermediate server. When we are connected to the intermediate server, we are able to acces the target server. The data tansfer between the client and the remote server is processed by io.Copy function:

func (tunnel *SSHTunnel) forward(localConn net.Conn) { serverConn, err := ssh.Dial("tcp", tunnel.Server.String(), tunnel.Config) if err != nil { fmt.Printf("Server dial error: %s

", err) return } remoteConn, err := serverConn.Dial("tcp", tunnel.Remote.String()) if err != nil { fmt.Printf("Remote dial error: %s

", err) return } copyConn:=func(writer, reader net.Conn) { _, err:= io.Copy(writer, reader) if err != nil { fmt.Printf("io.Copy error: %s", err) } } go copyConn(localConn, remoteConn) go copyConn(remoteConn, localConn) }

Usage

You can start the tunneling server in the following way:

tunnel := &SSHTunnel{ Config: sshConfig, Local: localEndpoint, Server: serverEndpoint, Remote: remoteEndpoint, } tunnel.Start()

Note tunnel.Start function is blocking. if you want to enable tunneling for your client application, you should start the function as a go routine.

You can simple establish an connection to your local server localhost:9000 in the following manner:

conn, err := net.Dial("tcp", "localhost:9000") if err != nil { // handle error } reader := bufio.NewReader(conn) // ...

You can download the example source code from here.

Do you have the next big idea? Consult with the experts. Hire Phogo Labs to help you build brilliant software. Get your free consultation

Please enable JavaScript to view the comments powered by Disqus.