Building your own Ngrok in 130 lines

$ ssh -R 8080:localhost:3000 your-ssh-server.com
$ localtunnel 3000
func join(a io.ReadWriteCloser, b io.ReadWriteCloser) {
go io.Copy(b, a)
io.Copy(a, b)
a.Close()
b.Close()
}
func newSubdomain() string {
b := make([]byte, 10)
if _, err := rand.Read(b); err != nil {
panic(err)
}
letters := []rune("abcdefghijklmnopqrstuvwxyz1234567890")
r := make([]rune, 10)
for i := range r {
r[i] = letters[int(b[i])*len(letters)/256]
}
return string(r) + "."
}
func fatal(err error) {
if err != nil {
log.Fatal(err)
}
}
var port = flag.String("p", "9999", "server port to use")
var host = flag.String("h", "vcap.me", "server hostname to use")
var addr = flag.String("b", "127.0.0.1", "ip to bind [server only]")
flag.Parse()
// client usage: groktunnel [-h=<server hostname>] <local port>
if flag.Arg(0) != "" {
conn, err := net.Dial("tcp", net.JoinHostPort(*host, *port))
fatal(err)
client := httputil.NewClientConn(conn, bufio.NewReader(conn))
req, err := http.NewRequest("GET", "/", nil)
req.Host = net.JoinHostPort(*host, *port)
fatal(err)
client.Write(req)
resp, _ := client.Read(req)
fmt.Printf("port %s http available at:\n", flag.Arg(0))
fmt.Printf("http://%s\n", resp.Header.Get("X-Public-Host"))
c, _ := client.Hijack()
sess := session.New(c)
defer sess.Close()
for {
ch, err := sess.Accept()
fatal(err)
conn, err := net.Dial("tcp", "127.0.0.1:"+flag.Arg(0))
fatal(err)
go join(conn, ch)
}
}
// server usage: groktunnel [-h=<hostname>] [-b=<bind ip>]
l, err := net.Listen("tcp", net.JoinHostPort(*addr, *port))
fatal(err)
defer l.Close()
vmux, err := vhost.NewHTTPMuxer(l, 1*time.Second)
fatal(err)

go serve(vmux, *host, *port)

log.Printf("groktunnel server [%s] ready!\n", *host)
for {
conn, err := vmux.NextError()
fmt.Println(err)
if conn != nil {
conn.Close()
}
}
func serve(vmux *vhost.HTTPMuxer, host, port string) {
ml, err := vmux.Listen(net.JoinHostPort(host, port))
fatal(err)
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
publicHost := strings.TrimSuffix(net.JoinHostPort(newSubdomain()+host, port), ":80")
pl, err := vmux.Listen(publicHost)
fatal(err)
w.Header().Add("X-Public-Host", publicHost)
w.Header().Add("Connection", "close")
w.WriteHeader(http.StatusOK)
conn, _, _ := w.(http.Hijacker).Hijack()
sess := session.New(conn)
defer sess.Close()
log.Printf("%s: start session", publicHost)
go func() {
for {
conn, err := pl.Accept()
if err != nil {
log.Println(err)
return
}
ch, err := sess.Open(context.Background())
if err != nil {
log.Println(err)
return
}
go join(ch, conn)
}
}()
sess.Wait()
log.Printf("%s: end session", publicHost)
})}
srv.Serve(ml)
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store