Skip to content

Instantly share code, notes, and snippets.

@tsingakbar
Last active September 8, 2023 16:05
Show Gist options
  • Save tsingakbar/325b64e55d5d5fa3f1e6237a27aa195a to your computer and use it in GitHub Desktop.
Save tsingakbar/325b64e55d5d5fa3f1e6237a27aa195a to your computer and use it in GitHub Desktop.
golang implementation of `ssh -R`, which will be convenient for environments lacking ssh installation
package main
import (
"io"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/BurntSushi/toml"
"golang.org/x/crypto/ssh"
)
type Config struct {
SshdServer string
User string
Password string
SshdSideListenOn string
SshSideForwardTo string
}
// runTunnel runs a tunnel between two connections; as soon as one connection
// reaches EOF or reports an error, both connections are closed and this
// function returns.
func runTunnel(local, remote net.Conn) {
defer local.Close()
defer remote.Close()
done := make(chan struct{}, 2)
go func() {
io.Copy(local, remote)
done <- struct{}{}
}()
go func() {
io.Copy(remote, local)
done <- struct{}{}
}()
<-done
}
func main() {
if len(os.Args) != 2 {
log.Fatalln("Usage: <exe> /path/to/config.toml")
}
var conf Config
if _, err := toml.DecodeFile(os.Args[1], &conf); err != nil {
log.Fatalln(err)
}
sshConf := &ssh.ClientConfig{
User: conf.User,
Auth: []ssh.AuthMethod{
ssh.Password(conf.Password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshClient, err := ssh.Dial("tcp", conf.SshdServer, sshConf)
if err != nil {
log.Fatalf("Failed to dial sshd: %s\n", err)
}
defer sshClient.Close()
log.Printf("sshd %s connected, ready for forwarding...\n", conf.SshdServer)
sshdSideListener, err := sshClient.Listen("tcp", conf.SshdSideListenOn)
if err != nil {
log.Fatalf("failed to listen on sshd side endpoint: %s\n", err)
}
defer sshdSideListener.Close()
sshdSideIncomeConnChan := make(chan net.Conn)
go func() {
for {
sshdSideIncomeConn, err := sshdSideListener.Accept()
if err != nil {
log.Printf("sshdSideListener.Accept() failed: %s\n", err)
close(sshdSideIncomeConnChan)
return
}
sshdSideIncomeConnChan <- sshdSideIncomeConn
}
}()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT)
keepAliveTimer := time.NewTicker(time.Minute)
defer keepAliveTimer.Stop()
for {
select {
case sshdSideIncomeConn, ok := <-sshdSideIncomeConnChan:
if !ok {
return
}
go func() {
sshSideForwardConn, err := net.Dial("tcp", conf.SshSideForwardTo)
if err != nil {
log.Printf("sshd side connection %s accepted, but failed to forward it to ssh side endpoint %s: %s",
sshdSideIncomeConn.RemoteAddr().String(), conf.SshSideForwardTo, err)
return
}
log.Printf("established %s <- %s <- ssh <- %s <- %s\n",
sshSideForwardConn.RemoteAddr().String(),
sshSideForwardConn.LocalAddr().String(),
sshdSideIncomeConn.LocalAddr().String(),
sshdSideIncomeConn.RemoteAddr().String(),
)
runTunnel(sshSideForwardConn, sshdSideIncomeConn)
}()
case <-keepAliveTimer.C:
_, _, err = sshClient.SendRequest("[email protected]", true, nil)
log.Println("ssh connection keepalive: sshClient.SendRequest")
if err != nil {
log.Fatal("Failed to send keep alive to sshd: ", err)
}
case <-signalChan:
log.Println("shutting down...")
keepAliveTimer.Stop()
sshClient.Close()
}
}
}
@tsingakbar
Copy link
Author

tsingakbar commented Sep 2, 2023

personally I use this together with a local ruby -r webrick/httpproxy -e "WEBrick::HTTPProxyServer.new(:Port => 3128).start" to make sshd side has an http proxy to access internet.
Note: webrick's http proxy is so naive that it forwards response to http client only after it's fully downloaded. for large response(like file downloading), a http proxy support real time streaming will be a better choice.

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