Skip to content

Instantly share code, notes, and snippets.

@codref
Last active October 23, 2023 13:32
Show Gist options
  • Star 75 You must be signed in to star a gist
  • Fork 27 You must be signed in to fork a gist
  • Save codref/473351a24a3ef90162cf10857fac0ff3 to your computer and use it in GitHub Desktop.
Save codref/473351a24a3ef90162cf10857fac0ff3 to your computer and use it in GitHub Desktop.
Go SSH reverse tunnel implementation (SSH -R)
/*
Go-Language implementation of an SSH Reverse Tunnel, the equivalent of below SSH command:
ssh -R 8080:127.0.0.1:8080 operatore@146.148.22.123
which opens a tunnel between the two endpoints and permit to exchange information on this direction:
server:8080 -----> client:8080
once authenticated a process on the SSH server can interact with the service answering to port 8080 of the client
without any NAT rule via firewall
Copyright 2017, Davide Dal Farra
MIT License, http://www.opensource.org/licenses/mit-license.php
*/
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"net"
"golang.org/x/crypto/ssh"
)
type Endpoint struct {
Host string
Port int
}
func (endpoint *Endpoint) String() string {
return fmt.Sprintf("%s:%d", endpoint.Host, endpoint.Port)
}
// From https://sosedoff.com/2015/05/25/ssh-port-forwarding-with-go.html
// Handle local client connections and tunnel data to the remote server
// Will use io.Copy - http://golang.org/pkg/io/#Copy
func handleClient(client net.Conn, remote net.Conn) {
defer client.Close()
chDone := make(chan bool)
// Start remote -> local data transfer
go func() {
_, err := io.Copy(client, remote)
if err != nil {
log.Println(fmt.Sprintf("error while copy remote->local: %s", err))
}
chDone <- true
}()
// Start local -> remote data transfer
go func() {
_, err := io.Copy(remote, client)
if err != nil {
log.Println(fmt.Sprintf("error while copy local->remote: %s", err))
}
chDone <- true
}()
<-chDone
}
func publicKeyFile(file string) ssh.AuthMethod {
buffer, err := ioutil.ReadFile(file)
if err != nil {
log.Fatalln(fmt.Sprintf("Cannot read SSH public key file %s", file))
return nil
}
key, err := ssh.ParsePrivateKey(buffer)
if err != nil {
log.Fatalln(fmt.Sprintf("Cannot parse SSH public key file %s", file))
return nil
}
return ssh.PublicKeys(key)
}
// local service to be forwarded
var localEndpoint = Endpoint{
Host: "localhost",
Port: 8080,
}
// remote SSH server
var serverEndpoint = Endpoint{
Host: "146.148.22.123",
Port: 22,
}
// remote forwarding port (on remote SSH server network)
var remoteEndpoint = Endpoint{
Host: "localhost",
Port: 8080,
}
func main() {
// refer to https://godoc.org/golang.org/x/crypto/ssh for other authentication types
sshConfig := &ssh.ClientConfig{
// SSH connection username
User: "operatore",
Auth: []ssh.AuthMethod{
// put here your private key path
publicKeyFile("/home/operatore/.ssh/id_rsa"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// Connect to SSH remote server using serverEndpoint
serverConn, err := ssh.Dial("tcp", serverEndpoint.String(), sshConfig)
if err != nil {
log.Fatalln(fmt.Printf("Dial INTO remote server error: %s", err))
}
// Listen on remote server port
listener, err := serverConn.Listen("tcp", remoteEndpoint.String())
if err != nil {
log.Fatalln(fmt.Printf("Listen open port ON remote server error: %s", err))
}
defer listener.Close()
// handle incoming connections on reverse forwarded tunnel
for {
// Open a (local) connection to localEndpoint whose content will be forwarded so serverEndpoint
local, err := net.Dial("tcp", localEndpoint.String())
if err != nil {
log.Fatalln(fmt.Printf("Dial INTO local service error: %s", err))
}
client, err := listener.Accept()
if err != nil {
log.Fatalln(err)
}
handleClient(client, local)
}
}
@arojoal
Copy link

arojoal commented Mar 31, 2018

Hi,
I'm trying to use this example but, when I run it I get an error "Cannot parse SSH public key file /Users/rojalval/.ssh/id_rsa. Error: ssh: cannot decode encrypted private keys"

I've checked that the process is reading the correct id_rsa file so, what can be happening? could you help me?

@mennanov
Copy link

mennanov commented Apr 20, 2018

Hi,

any ideas on how to implement a reverse tunnel that will enable a direct connection to the remote host? In other words an equivalent of this SSH command:

ssh -R 146.148.22.123:8080:127.0.0.1:8080 operatore@146.148.22.123

upd: Found a solution:

var remoteEndpoint = Endpoint{
	Host: "146.148.22.123",
	Port: 8080,
}

@joonas-fi
Copy link

joonas-fi commented Sep 6, 2018

Thanks for the awesome gist! I improved it quite a bit (parallel connections, logging, retries on errors, start on system startup via systemd, configuration from JSON file, prebuilt binaries): https://github.com/function61/holepunch-client

@SakiiR
Copy link

SakiiR commented Jan 15, 2020

It seems that your code is great for a standalone reverse ssh tool :)

However, when starting/stopping asynchronously, it seems to keep remote listenner up :(

@develamit
Copy link

This is an awesome implementation. Works great.
-amit

@develamit
Copy link

Thanks for the awesome gist! I improved it quite a bit (parallel connections, logging, retries on errors, start on system startup via systemd, configuration from JSON file, prebuilt binaries): https://github.com/function61/holepunch-client

Hi joonas-fi,
I am new to this and I am trying to build and use your code by following the README. But I am falling short. Could you please update the README on how I can build holepunch client and execute it? Also, if the remote server has the sshd running on a port other than 22, then how can I introduce custom port in the json file?
So far I have done this:

  1. Edited a file holepunch.json (similar to holepunch.example.json) and kept it in cmd/holepunch directory.
  2. Invoked the command: go build main.go

But I am unable to make this work. Getting errors.
Thanks,
-amit

@joonas-fi
Copy link

Sorry, I didn't get a notification here. But we worked out the issue: function61/holepunch-client#10 (in case anyone else has trouble)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment