From 755e3c77078d08fe741d9c1272afcf7c5f8f97f5 Mon Sep 17 00:00:00 2001 From: "Jonathan Leibiusky @xetorthio" Date: Mon, 19 Jun 2017 11:55:04 -0300 Subject: [PATCH] Add ssh proxy --- Dockerfile | 4 + api.go | 2 + docker-compose.yml | 8 +- handlers/sshproxy.go | 206 +++++++++++++++++++++++++++++++++++++++++++ pwd/instance.go | 14 +++ pwd/pwd.go | 1 + 6 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 handlers/sshproxy.go diff --git a/Dockerfile b/Dockerfile index 04975bc..7e75229 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,14 +10,18 @@ WORKDIR /go/src/github.com/play-with-docker/play-with-docker RUN go get -v -d ./... +RUN ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null + RUN CGO_ENABLED=0 go build -a -installsuffix nocgo -o /go/bin/play-with-docker . + FROM alpine RUN apk --update add ca-certificates RUN mkdir -p /app/pwd COPY --from=0 /go/bin/play-with-docker /app/play-with-docker +COPY --from=0 /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key COPY ./www /app/www WORKDIR /app diff --git a/api.go b/api.go index 995034b..313e2bf 100644 --- a/api.go +++ b/api.go @@ -111,6 +111,8 @@ func main() { log.Fatal(httpServer.ListenAndServe()) }() + go handlers.ListenSSHProxy("0.0.0.0:1022") + // Now listen for TLS connections that need to be proxied handlers.StartTLSProxy(config.SSLPortNumber) } diff --git a/docker-compose.yml b/docker-compose.yml index e779202..8bd8bf3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: # use the latest golang image image: golang # go to the right place and starts the app - command: /bin/sh -c 'cd /go/src/github.com/play-with-docker/play-with-docker; go run api.go -save /pwd/sessions1 -name pwd1 -cname host1' + command: /bin/sh -c 'ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null; cd /go/src/github.com/play-with-docker/play-with-docker; go run api.go -save /pwd/sessions1 -name pwd1 -cname host1' volumes: # since this app creates networks and launches containers, we need to talk to docker daemon - /var/run/docker.sock:/var/run/docker.sock @@ -24,13 +24,15 @@ services: - sessions:/pwd environment: GOOGLE_RECAPTCHA_DISABLED: "true" + ports: + - "1022:1022" pwd2: # pwd daemon container always needs to be named this way container_name: pwd2 # use the latest golang image image: golang # go to the right place and starts the app - command: /bin/sh -c 'cd /go/src/github.com/play-with-docker/play-with-docker; go run api.go -save /pwd/sessions2 -name pwd2 -cname host2' + command: /bin/sh -c 'ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null; cd /go/src/github.com/play-with-docker/play-with-docker; go run api.go -save /pwd/sessions2 -name pwd2 -cname host2' volumes: # since this app creates networks and launches containers, we need to talk to docker daemon - /var/run/docker.sock:/var/run/docker.sock @@ -39,6 +41,8 @@ services: - sessions:/pwd environment: GOOGLE_RECAPTCHA_DISABLED: "true" + ports: + - "1023:1022" prometheus: container_name: prometheus image: prom/prometheus diff --git a/handlers/sshproxy.go b/handlers/sshproxy.go new file mode 100644 index 0000000..68d9d8f --- /dev/null +++ b/handlers/sshproxy.go @@ -0,0 +1,206 @@ +package handlers + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net" + "strings" + "sync" + + "golang.org/x/crypto/ssh" +) + +var sshConfig = &ssh.ServerConfig{ + PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + user := c.User() + chunks := strings.Split(user, "-") + ip := strings.Join(chunks[:4], ".") + sessionPrefix := chunks[4] + + log.Println(ip, sessionPrefix) + + return nil, nil + }, +} + +func ListenSSHProxy(laddr string) { + privateBytes, err := ioutil.ReadFile("/etc/ssh/ssh_host_rsa_key") + if err != nil { + log.Fatal("Failed to load private key: ", err) + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + log.Fatal("Failed to parse private key: ", err) + } + + sshConfig.AddHostKey(private) + + listener, err := net.Listen("tcp", laddr) + if err != nil { + log.Fatal("failed to listen for connection: ", err) + } + for { + nConn, err := listener.Accept() + if err != nil { + log.Fatal("failed to accept incoming connection: ", err) + } + + go handle(nConn) + } +} + +func handle(c net.Conn) { + sshCon, chans, reqs, err := ssh.NewServerConn(c, sshConfig) + if err != nil { + c.Close() + return + } + + user := sshCon.User() + chunks := strings.Split(user, "-") + ip := strings.Join(chunks[:4], ".") + sessionPrefix := chunks[4] + + i := core.InstanceFindByIPAndSession(sessionPrefix, ip) + if i == nil { + log.Printf("Couldn't find instance with ip [%s] in session [%s]\n", ip, sessionPrefix) + c.Close() + return + } + + // The incoming Request channel must be serviced. + go ssh.DiscardRequests(reqs) + + newChannel := <-chans + if newChannel == nil { + sshCon.Close() + return + } + + if newChannel.ChannelType() != "session" { + newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + return + } + + channel, requests, err := newChannel.Accept() + if err != nil { + log.Fatalf("Could not accept channel: %v", err) + } + + stderr := channel.Stderr() + + fmt.Fprintf(stderr, "Connecting to %s\r\n", ip) + + clientConfig := &ssh.ClientConfig{ + User: "root", + Auth: []ssh.AuthMethod{ + ssh.Password("root"), + }, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil + }, + } + + client, err := ssh.Dial("tcp", fmt.Sprintf("%s:22", ip), clientConfig) + if err != nil { + fmt.Fprintf(stderr, "Connect failed: %v\r\n", err) + channel.Close() + return + } + + go func() { + for newChannel = range chans { + if newChannel == nil { + return + } + + channel2, reqs2, err := client.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData()) + if err != nil { + x, ok := err.(*ssh.OpenChannelError) + if ok { + newChannel.Reject(x.Reason, x.Message) + } else { + newChannel.Reject(ssh.Prohibited, "remote server denied channel request") + } + continue + } + + channel, reqs, err := newChannel.Accept() + if err != nil { + channel2.Close() + continue + } + go proxy(reqs, reqs2, channel, channel2) + } + }() + + // Forward the session channel + channel2, reqs2, err := client.OpenChannel("session", []byte{}) + if err != nil { + fmt.Fprintf(stderr, "Remote session setup failed: %v\r\n", err) + channel.Close() + return + } + + maskedReqs := make(chan *ssh.Request, 1) + go func() { + for req := range requests { + if req.Type == "auth-agent-req@openssh.com" { + continue + } + maskedReqs <- req + } + }() + proxy(maskedReqs, reqs2, channel, channel2) +} + +func proxy(reqs1, reqs2 <-chan *ssh.Request, channel1, channel2 ssh.Channel) { + var closer sync.Once + closeFunc := func() { + channel1.Close() + channel2.Close() + } + + defer closer.Do(closeFunc) + + closerChan := make(chan bool, 1) + + go func() { + io.Copy(channel1, channel2) + closerChan <- true + }() + + go func() { + io.Copy(channel2, channel1) + closerChan <- true + }() + + for { + select { + case req := <-reqs1: + if req == nil { + return + } + b, err := channel2.SendRequest(req.Type, req.WantReply, req.Payload) + if err != nil { + return + } + req.Reply(b, nil) + + case req := <-reqs2: + if req == nil { + return + } + b, err := channel1.SendRequest(req.Type, req.WantReply, req.Payload) + if err != nil { + return + } + req.Reply(b, nil) + case <-closerChan: + return + } + } +} diff --git a/pwd/instance.go b/pwd/instance.go index 0278b66..072098b 100644 --- a/pwd/instance.go +++ b/pwd/instance.go @@ -150,6 +150,20 @@ func (p *pwd) InstanceFindByIP(ip string) *Instance { return nil } +func (p *pwd) InstanceFindByIPAndSession(sessionPrefix, ip string) *Instance { + defer observeAction("InstanceFindByIPAndSession", time.Now()) + for id, s := range sessions { + if strings.HasPrefix(id, sessionPrefix) { + for _, i := range s.Instances { + if i.IP == ip { + return i + } + } + } + } + return nil +} + func (p *pwd) InstanceFindByAlias(sessionPrefix, alias string) *Instance { defer observeAction("InstanceFindByAlias", time.Now()) for id, s := range sessions { diff --git a/pwd/pwd.go b/pwd/pwd.go index 124bd06..183e9f3 100644 --- a/pwd/pwd.go +++ b/pwd/pwd.go @@ -68,6 +68,7 @@ type PWDApi interface { InstanceGet(session *Session, name string) *Instance InstanceFindByIP(ip string) *Instance InstanceFindByAlias(sessionPrefix, alias string) *Instance + InstanceFindByIPAndSession(sessionPrefix, ip string) *Instance InstanceDelete(session *Session, instance *Instance) error InstanceWriteToTerminal(instance *Instance, data string) InstanceAllowedImages() []string