tailscale/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go

188 lines
4.7 KiB
Go
Raw Permalink Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// ssh-auth-none-demo is a demo SSH server that's meant to run on the
// public internet (at 188.166.70.128 port 2222) and
// highlight the unique parts of the Tailscale SSH server so SSH
// client authors can hit it easily and fix their SSH clients without
// needing to set up Tailscale and Tailscale SSH.
package main
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/tempfork/gliderlabs/ssh"
)
// keyTypes are the SSH key types that we either try to read from the
// system's OpenSSH keys.
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
var (
addr = flag.String("addr", ":2222", "address to listen on")
)
func main() {
flag.Parse()
cacheDir, err := os.UserCacheDir()
if err != nil {
log.Fatal(err)
}
dir := filepath.Join(cacheDir, "ssh-auth-none-demo")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Fatal(err)
}
keys, err := getHostKeys(dir)
if err != nil {
log.Fatal(err)
}
if len(keys) == 0 {
log.Fatal("no host keys")
}
srv := &ssh.Server{
Addr: *addr,
Version: "Tailscale",
Handler: handleSessionPostSSHAuth,
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
start := time.Now()
return &gossh.ServerConfig{
NextAuthMethodCallback: func(conn gossh.ConnMetadata, prevErrors []error) []string {
return []string{"tailscale"}
},
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
cm.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
totalBanners := 2
if cm.User() == "banners" {
totalBanners = 5
}
for banner := 2; banner <= totalBanners; banner++ {
time.Sleep(time.Second)
if banner == totalBanners {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
} else {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
}
}
return nil, nil
},
BannerCallback: func(cm gossh.ConnMetadata) string {
log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
},
}
},
}
for _, signer := range keys {
srv.AddHostKey(signer)
}
log.Printf("Running on %s ...", srv.Addr)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
log.Printf("done")
}
func handleSessionPostSSHAuth(s ssh.Session) {
log.Printf("Started session from user %q", s.User())
fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
// Abort the session on Control-C or Control-D.
go func() {
buf := make([]byte, 1024)
for {
n, err := s.Read(buf)
for _, b := range buf[:n] {
if b <= 4 { // abort on Control-C (3) or Control-D (4)
io.WriteString(s, "bye\n")
s.Exit(1)
}
}
if err != nil {
return
}
}
}()
for i := 10; i > 0; i-- {
fmt.Fprintf(s, "%v ...\n", i)
time.Sleep(time.Second)
}
s.Exit(0)
}
func getHostKeys(dir string) (ret []ssh.Signer, err error) {
for _, typ := range keyTypes {
hostKey, err := hostKeyFileOrCreate(dir, typ)
if err != nil {
return nil, err
}
signer, err := gossh.ParsePrivateKey(hostKey)
if err != nil {
return nil, err
}
ret = append(ret, signer)
}
return ret, nil
}
func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := os.ReadFile(path)
if err == nil {
return v, nil
}
if !os.IsNotExist(err) {
return nil, err
}
var priv any
switch typ {
default:
return nil, fmt.Errorf("unsupported key type %q", typ)
case "ed25519":
_, priv, err = ed25519.GenerateKey(rand.Reader)
case "ecdsa":
// curve is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
curve := elliptic.P256()
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
case "rsa":
// keySize is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
const keySize = 2048
priv, err = rsa.GenerateKey(rand.Reader, keySize)
}
if err != nil {
return nil, err
}
mk, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
err = os.WriteFile(path, pemGen, 0700)
return pemGen, err
}