1

I'm building a custom CLI daemon in Go for Ubuntu. When I run the CLI locally (without SSH), everything works correctly — for example:

admin@admin> whoami

responds as expected.

However, after binding the CLI loop to an SSH session and running it as a server, I can successfully SSH into it and see the banner and prompt, but no keyboard input is accepted. The session displays output, but I cannot type anything — not even a single character.

So the issue is: 🟢 Local CLI → input works normally 🟢 SSH connection → connects and shows prompt 🔴 SSH session → no input is received inside my Go program

Below is my code, for the daemon:

package main

import (
    "bufio"
    "database/sql"
    "fmt"
    "log"
    "os"
    "os/exec"
    "strconv"
    "strings"
    "time"

    "github.com/gliderlabs/ssh"
    _ "github.com/mattn/go-sqlite3"
    "golang.org/x/crypto/bcrypt"
)

const dbPath = "/opt/ngfw/auth/db/admins.db"

// ---------------- AUTH --------------------

func authUser(username, password string) bool {
    db, err := sql.Open("sqlite3", dbPath)
    if err != nil {
        log.Println("DB error:", err)
        return false
    }
    defer db.Close()

    var hash string
    err = db.QueryRow("SELECT password FROM users WHERE username=?", username).Scan(&hash)
    if err != nil {
        return false
    }

    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}

// ---------------- SYSTEM INFO --------------------

func showSystem(s ssh.Session) {
    hostname, _ := os.Hostname()

    uptimeBytes, _ := os.ReadFile("/proc/uptime")
    uptimeParts := strings.Fields(string(uptimeBytes))
    uptimeSec, _ := strconv.ParseFloat(uptimeParts[0], 64)
    uptime := time.Duration(uptimeSec) * time.Second

    kernelBytes, _ := exec.Command("uname", "-r").Output()
    kernel := strings.TrimSpace(string(kernelBytes))

    fmt.Fprintf(s, "\nSystem Information\n")
    fmt.Fprintf(s, "-------------------\n")
    fmt.Fprintf(s, "Hostname:       %s\n", hostname)
    fmt.Fprintf(s, "Version:        0.1-dev\n")
    fmt.Fprintf(s, "Kernel:         %s\n", kernel)
    fmt.Fprintf(s, "Uptime:         %s\n\n", uptime.Truncate(time.Second))
}

// ---------------- CLI LOOP --------------------

func cliSession(s ssh.Session) {

    hostname, _ := os.Hostname()
    user := s.User()

    pty, _, ok := s.Pty()
    fmt.Fprintf(s, "\n[DEBUG] PTY OK=%v TERM=%s\n", ok, pty.Term)

    reader := bufio.NewReader(s)
    fmt.Fprintf(s, "DEBUG READ TEST...\n")
    b, err := reader.Peek(1)
    fmt.Fprintf(s, "PEEK=%v ERR=%v\n", b, err)

    fmt.Fprintf(s, "\n🔥 Welcome to CLI 🔥\nLogged in as: %s\n\n", user)

    for {
        fmt.Fprintf(s, "%s@%s> ", user, hostname)

        line, err := reader.ReadString('\n')
        if err != nil {
            fmt.Fprintln(s, "\n🛑 Lost input stream — disconnecting")
            return
        }

        cmd := strings.TrimSpace(line)

        switch cmd {
        case "whoami":
            fmt.Fprintln(s, user)

        case "show ?":
            fmt.Fprintln(s, "\nAvailable SHOW commands:")
            fmt.Fprintln(s, "  system")
            fmt.Fprintln(s, "  interfaces (coming soon)")
            fmt.Fprintln(s, "  users (coming soon)")

        case "show system":
            showSystem(s)

        case "exit":
            fmt.Fprintln(s, "🫡 Logging out…")
            return

        default:
            if cmd != "" {
                fmt.Fprintf(s, "%s: command not found\n", cmd)
            }
        }
    }
}

// ---------------- SSH SERVER --------------------

func main() {
    fmt.Println("🔥 SSH CLI STARTED on port 2222")

    ssh.Handle(cliSession)

    log.Fatal(ssh.ListenAndServe(
        ":2222",
        nil,
        ssh.PasswordAuth(func(ctx ssh.Context, pass string) bool {
            return authUser(ctx.User(), pass)
        }),
    ))
}

Help is appreciated

1
  • Does your server print [DEBUG] PTY OK=true TERM=…? Commented Nov 17 at 8:22

1 Answer 1

0

The problem with your server is in waiting for the '\n' character in:

line, err := reader.ReadString('\n')

'\n' translates to 0x0A = LF. It's equivalent of pressing Ctrl-J.
Which is not what terminal sends via SSH when you press Enter.

You can check that input actually is received, but your server hangs on blocking reading waiting for never incoming byte.
Change this to:

b, err := reader.Peek(1)
log.Printf("read character %0x\n", b)

And in your server logs, you will see how enter is encoded by ssh client/terminal:

2025/11/21 17:35:56 read character 0d

As you see, enter is interpreted as 0x0D - CR (How is a return / enter keypress encoded in a string sent to a SSH shell?)

Change it to the:

line, err := reader.ReadString('\r')
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.