SSH-bound Go CLI Daemon – SSH Connects Successfully, but No Input Accepted

2 weeks ago 10
ARTICLE AD BOX

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

Read Entire Article