tailscale/tsnet/example/ssh-game/ssh-game.go
Brad Fitzpatrick 3e34e721e8
Some checks failed
checklocks / checklocks (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
Dockerfile build / deploy (push) Has been cancelled
natlab-basic / EasyEasy (push) Has been cancelled
CI / gomod-cache (push) Has been cancelled
CI / fuzz (push) Has been cancelled
tailscale.com/cmd/vet / vet (push) Has been cancelled
CI / race-root-integration (1/4) (push) Has been cancelled
CI / race-root-integration (2/4) (push) Has been cancelled
CI / race-root-integration (3/4) (push) Has been cancelled
CI / race-root-integration (4/4) (push) Has been cancelled
CI / test (-race, amd64, 1/3) (push) Has been cancelled
CI / test (-race, amd64, 2/3) (push) Has been cancelled
CI / test (-race, amd64, 3/3) (push) Has been cancelled
CI / test (386) (push) Has been cancelled
CI / test (amd64) (push) Has been cancelled
CI / Windows (${{ matrix.name || matrix.shard}}) (win-bench, benchmarks) (push) Has been cancelled
CI / Windows (${{ matrix.name || matrix.shard}}) (win-shard-1-2, 1/2) (push) Has been cancelled
CI / Windows (${{ matrix.name || matrix.shard}}) (win-shard-2-2, 2/2) (push) Has been cancelled
CI / macos (push) Has been cancelled
CI / privileged (push) Has been cancelled
CI / vm (push) Has been cancelled
CI / cross (386, linux) (push) Has been cancelled
CI / cross (amd64, darwin) (push) Has been cancelled
CI / cross (amd64, freebsd) (push) Has been cancelled
CI / cross (amd64, openbsd) (push) Has been cancelled
CI / cross (amd64, windows) (push) Has been cancelled
CI / cross (arm, 5, linux) (push) Has been cancelled
CI / cross (arm, 7, linux) (push) Has been cancelled
CI / cross (arm64, darwin) (push) Has been cancelled
CI / cross (arm64, linux) (push) Has been cancelled
CI / cross (arm64, windows) (push) Has been cancelled
CI / cross (loong64, linux) (push) Has been cancelled
CI / ios (push) Has been cancelled
CI / crossmin (amd64, illumos) (push) Has been cancelled
CI / crossmin (amd64, plan9) (push) Has been cancelled
CI / crossmin (amd64, solaris) (push) Has been cancelled
CI / crossmin (ppc64, aix) (push) Has been cancelled
CI / android (push) Has been cancelled
CI / wasm (push) Has been cancelled
CI / tailscale_go (push) Has been cancelled
CI / depaware (push) Has been cancelled
CI / go_generate (push) Has been cancelled
CI / make_tidy (push) Has been cancelled
CI / licenses (push) Has been cancelled
CI / staticcheck (${{ matrix.name }}) (--with-tags-all=darwin, arm64, darwin, macOS) (push) Has been cancelled
CI / staticcheck (${{ matrix.name }}) (--with-tags-all=linux, amd64, linux, Linux) (push) Has been cancelled
CI / staticcheck (${{ matrix.name }}) (--with-tags-all=windows, amd64, windows, Windows) (push) Has been cancelled
CI / staticcheck (${{ matrix.name }}) (--without-tags-any=windows,darwin,linux --shard=1/4, amd64, linux, Portable (1/4)) (push) Has been cancelled
CI / staticcheck (${{ matrix.name }}) (--without-tags-any=windows,darwin,linux --shard=2/4, amd64, linux, Portable (2/4)) (push) Has been cancelled
CI / staticcheck (${{ matrix.name }}) (--without-tags-any=windows,darwin,linux --shard=3/4, amd64, linux, Portable (3/4)) (push) Has been cancelled
CI / staticcheck (${{ matrix.name }}) (--without-tags-any=windows,darwin,linux --shard=4/4, amd64, linux, Portable (4/4)) (push) Has been cancelled
CI / notify_slack (push) Has been cancelled
CI / merge_blocker (push) Has been cancelled
CI / check_mergeability_strict (push) Has been cancelled
CI / check_mergeability (push) Has been cancelled
tsnet: add opt-in SSH support (Server.ListenSSH)
This adds tsnet.Server.ListenSSH which, if the SSH feature is linked,
returns a net.Listener whose Accept yields *tailssh.Session values (as
net.Conn). This lets tsnet apps accept incoming SSH connections to
implement custom TUI applications.

Basic apps can use net.Conn directly (Read/Write/Close). Rich apps
import ssh/tailssh and type-assert for peer identity, PTY, signals,
etc. If feature/ssh isn't imported, ListenSSH returns an error.

Includes a demo guess-the-number game in tsnet/example/ssh-game.

Updates tailscale/corp#37839

Change-Id: I4e7c3c96afb030cdf4da8f2d8b2253820628129a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-05-30 14:17:50 -07:00

93 lines
1.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9
// The ssh-game server demonstrates how to use tsnet's ListenSSH to build
// a custom SSH application. It runs a simple "guess the number" game.
//
// Usage:
//
// go run ./tsnet/example/ssh-game
//
// Then from another Tailscale node:
//
// ssh -p 2222 <hostname>
package main
import (
"bufio"
"fmt"
"log"
"math/rand/v2"
"net"
"strings"
_ "tailscale.com/feature/ssh"
"tailscale.com/ssh/tailssh"
"tailscale.com/tsnet"
)
func main() {
s := &tsnet.Server{
Hostname: "ssh-game",
}
defer s.Close()
ln, err := s.ListenSSH(":2222")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
log.Println("Listening on :2222")
for {
conn, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
go handleGame(conn)
}
}
func handleGame(c net.Conn) {
sess, ok := c.(*tailssh.Session)
if !ok {
fmt.Fprintf(c, "unexpected connection type\n")
c.Close()
return
}
defer sess.Exit(0)
target := rand.IntN(100) + 1
scanner := bufio.NewScanner(sess)
fmt.Fprintf(sess, "Welcome, %s from %s!\r\n",
sess.UserProfile().LoginName,
sess.Peer().ComputedName())
fmt.Fprintf(sess, "I'm thinking of a number between 1 and 100.\r\n")
fmt.Fprintf(sess, "Can you guess it?\r\n\r\n")
for attempts := 1; ; attempts++ {
fmt.Fprintf(sess, "Your guess: ")
if !scanner.Scan() {
return
}
line := strings.TrimSpace(scanner.Text())
var guess int
if _, err := fmt.Sscanf(line, "%d", &guess); err != nil {
fmt.Fprintf(sess, "Please enter a number.\r\n")
continue
}
switch {
case guess < target:
fmt.Fprintf(sess, "Higher!\r\n")
case guess > target:
fmt.Fprintf(sess, "Lower!\r\n")
default:
fmt.Fprintf(sess, "Correct! You got it in %d attempts.\r\n", attempts)
return
}
}
}