From 385e05075b8f3075a168b4ef1c359567c0d91924 Mon Sep 17 00:00:00 2001 From: Marcos Lilljedahl Date: Sun, 18 Jun 2017 12:13:40 -0300 Subject: [PATCH 1/4] Avoid dialing if the instance IP doesn't exit --- handlers/reverseproxy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/handlers/reverseproxy.go b/handlers/reverseproxy.go index 98ad132..ed5a72e 100644 --- a/handlers/reverseproxy.go +++ b/handlers/reverseproxy.go @@ -60,6 +60,14 @@ func (p *tcpProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { logFunc = p.ErrorLog.Printf } + vars := mux.Vars(r) + instanceIP := vars["node"] + + if i := core.InstanceFindByIP(strings.Replace(instanceIP, "-", ".", -1)); i == nil { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + outreq := new(http.Request) // shallow copying *outreq = *r From 849dfffac09dbc5f4227d4053a33cd3e2dd27449 Mon Sep 17 00:00:00 2001 From: Marcos Lilljedahl Date: Mon, 19 Jun 2017 11:03:08 -0300 Subject: [PATCH 2/4] Add ssh to dind instance --- Dockerfile.dind | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile.dind b/Dockerfile.dind index bba9a2d..3972e13 100644 --- a/Dockerfile.dind +++ b/Dockerfile.dind @@ -1,7 +1,7 @@ ARG VERSION=docker:17.05.0-ce-dind FROM ${VERSION} -RUN apk add --no-cache git tmux py2-pip apache2-utils vim build-base gettext-dev curl bash-completion bash util-linux jq +RUN apk add --no-cache git tmux py2-pip apache2-utils vim build-base gettext-dev curl bash-completion bash util-linux jq openssh # Compile and install httping @@ -20,7 +20,7 @@ RUN curl -L https://github.com/docker/machine/releases/download/${MACHINE_VERSIO -o /usr/bin/docker-machine && chmod +x /usr/bin/docker-machine # Add bash completion -RUN mkdir /etc/bash_completion.d && curl https://raw.githubusercontent.com/docker/docker/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker +RUN mkdir /etc/bash_completion.d && curl https://raw.githubusercontent.com/docker/cli/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker # Replace modprobe with a no-op to get rid of spurious warnings # (note: we can't just symlink to /bin/true because it might be busybox) @@ -51,6 +51,8 @@ CMD cat /etc/hosts >/etc/hosts.bak && \ sed -i "s/\DOCKER_TLSCERT/$DOCKER_TLSCERT/" /etc/docker/daemon.json && \ sed -i "s/\DOCKER_TLSKEY/$DOCKER_TLSKEY/" /etc/docker/daemon.json && \ umount /var/lib/docker && mount -t securityfs none /sys/kernel/security && \ + echo "root:root" | chpasswd &> /dev/null && ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null && \ + /usr/sbin/sshd -o PermitRootLogin=yes 2>/dev/null && \ dockerd &>/docker.log & \ while true ; do script -q -c "/bin/bash -l" /dev/null ; done # ... and then put a shell in the foreground, restarting it if it exits From 9de63d5c17043227b286f50e4ba8899cce1b8f48 Mon Sep 17 00:00:00 2001 From: Marcos Lilljedahl Date: Mon, 19 Jun 2017 11:48:28 -0300 Subject: [PATCH 3/4] Set bash as default shell and don't print MOTD twice --- Dockerfile.dind | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile.dind b/Dockerfile.dind index 3972e13..d768cb4 100644 --- a/Dockerfile.dind +++ b/Dockerfile.dind @@ -19,9 +19,11 @@ RUN pip install docker-compose==${COMPOSE_VERSION} RUN curl -L https://github.com/docker/machine/releases/download/${MACHINE_VERSION}/docker-machine-Linux-x86_64 \ -o /usr/bin/docker-machine && chmod +x /usr/bin/docker-machine -# Add bash completion -RUN mkdir /etc/bash_completion.d && curl https://raw.githubusercontent.com/docker/cli/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker - +# Add bash completion and set bash as default shell +RUN mkdir /etc/bash_completion.d \ + && curl https://raw.githubusercontent.com/docker/cli/master/contrib/completion/bash/docker -o /etc/bash_completion.d/docker \ + && sed -i "s/ash/bash/" /etc/passwd + # Replace modprobe with a no-op to get rid of spurious warnings # (note: we can't just symlink to /bin/true because it might be busybox) RUN rm /sbin/modprobe && echo '#!/bin/true' >/sbin/modprobe && chmod +x /sbin/modprobe @@ -52,7 +54,7 @@ CMD cat /etc/hosts >/etc/hosts.bak && \ sed -i "s/\DOCKER_TLSKEY/$DOCKER_TLSKEY/" /etc/docker/daemon.json && \ umount /var/lib/docker && mount -t securityfs none /sys/kernel/security && \ echo "root:root" | chpasswd &> /dev/null && ssh-keygen -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key >/dev/null && \ - /usr/sbin/sshd -o PermitRootLogin=yes 2>/dev/null && \ + /usr/sbin/sshd -o PermitRootLogin=yes -o PrintMotd=no 2>/dev/null && \ dockerd &>/docker.log & \ while true ; do script -q -c "/bin/bash -l" /dev/null ; done # ... and then put a shell in the foreground, restarting it if it exits From 755e3c77078d08fe741d9c1272afcf7c5f8f97f5 Mon Sep 17 00:00:00 2001 From: "Jonathan Leibiusky @xetorthio" Date: Mon, 19 Jun 2017 11:55:04 -0300 Subject: [PATCH 4/4] 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