commit 1f0cd36375c8dd8b797c78ac5f6120b634a79cd6 Author: dcorral Date: Thu Nov 6 14:43:35 2025 +0100 Initial commit with Dockerfile and Go code diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..a829b60 --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,27 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - master + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: dcorral/ssh-remote:latest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75b8f21 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY main.go ./ +RUN go build -o sshserver main.go + +FROM alpine:latest + +RUN apk add --no-cache ca-certificates + +COPY --from=builder /app/sshserver /usr/local/bin/sshserver + +EXPOSE 22 + +CMD ["/usr/local/bin/sshserver"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ca42381 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + ssh-server: + image: ssh-server + build: . + ports: + - "22:22" + volumes: + - ../tui:/app/tui:ro + cap_add: + - SYS_CHROOT + restart: unless-stopped diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..003328c --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module sshserver + +go 1.21 + +require ( + github.com/creack/pty/v2 v2.0.1 + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f +) + +require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..04732e4 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/creack/pty/v2 v2.0.1 h1:RDY1VY5b+7m2mfPsugucOYPIxMp+xal5ZheSyVzUA+k= +github.com/creack/pty/v2 v2.0.1/go.mod h1:2dSssKp3b86qYEMwA/FPwc3ff+kYpDdQI8osU8J7gxQ= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/main.go b/main.go new file mode 100644 index 0000000..bc17c3d --- /dev/null +++ b/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/binary" + "io" + "log" + "net" + "os" + "os/exec" + "golang.org/x/crypto/ssh" + "github.com/creack/pty/v2" +) + +func main() { + config := &ssh.ServerConfig{ + NoClientAuth: true, + } + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + log.Fatal(err) + } + signer, err := ssh.NewSignerFromKey(key) + if err != nil { + log.Fatal(err) + } + config.AddHostKey(signer) + listener, err := net.Listen("tcp", ":22") + if err != nil { + log.Fatal(err) + } + log.Println("SSH server listening on :22") + for { + conn, err := listener.Accept() + if err != nil { + log.Println("Accept error:", err) + continue + } + go handleConn(conn, config) + } +} + +func handleConn(conn net.Conn, config *ssh.ServerConfig) { + sshConn, chans, reqs, err := ssh.NewServerConn(conn, config) + if err != nil { + log.Println("ServerConn error:", err) + conn.Close() + return + } + log.Println("New connection from", sshConn.RemoteAddr(), "user", sshConn.User()) + go ssh.DiscardRequests(reqs) + for newChannel := range chans { + if newChannel.ChannelType() != "session" { + newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + continue + } + channel, requests, err := newChannel.Accept() + if err != nil { + log.Println("Channel accept error:", err) + continue + } + go handleChannel(channel, requests) + } + sshConn.Wait() +} + +func handleChannel(channel ssh.Channel, requests <-chan *ssh.Request) { + defer channel.Close() + var ptmx *os.File + for req := range requests { + switch req.Type { + case "pty-req": + req.Reply(true, nil) + case "window-change": + if ptmx != nil { + width := binary.BigEndian.Uint32(req.Payload) + height := binary.BigEndian.Uint32(req.Payload[4:]) + pty.Setsize(ptmx, &pty.Winsize{Cols: uint16(width), Rows: uint16(height)}) + } + req.Reply(true, nil) + case "shell": + req.Reply(true, nil) + cmd := exec.Command("/app/tui") + cmd.Env = []string{"PATH=/bin"} + cmd.Dir = "/" + var err error + ptmx, err = pty.Start(cmd) + if err != nil { + log.Println("PTY start error:", err) + return + } + go func() { + defer ptmx.Close() + go io.Copy(channel, ptmx) + go io.Copy(ptmx, channel) + cmd.Wait() + channel.Close() + }() + case "exec": + req.Reply(true, nil) + command := string(req.Payload[4:]) + runCommand(channel, command) + return + default: + req.Reply(false, nil) + } + } +} + +func runCommand(channel ssh.Channel, command string) { + cmd := exec.Command("/bin/bash", "-c", command) + cmd.Env = []string{"PATH=/bin"} + cmd.Dir = "/" + stdin, err := cmd.StdinPipe() + if err != nil { + log.Println("StdinPipe error:", err) + return + } + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Println("StdoutPipe error:", err) + return + } + stderr, err := cmd.StderrPipe() + if err != nil { + log.Println("StderrPipe error:", err) + return + } + if err := cmd.Start(); err != nil { + log.Println("Start error:", err) + return + } + go io.Copy(stdin, channel) + go io.Copy(channel, stdout) + go io.Copy(channel, stderr) + cmd.Wait() +} \ No newline at end of file