From 8a187159b23632a0bc4a18458db2b6ac276059b5 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 9 Oct 2022 17:54:23 -0700 Subject: [PATCH] cmd/ssh-auth-none-demo: add demo SSH server that acts like Tailscale SSH For SSH client authors to fix their clients without setting up Tailscale stuff. Change-Id: I8c7049398512de6cb91c13716d4dcebed4d47b9c Signed-off-by: Brad Fitzpatrick --- cmd/ssh-auth-none-demo/ssh-auth-none-demo.go | 171 +++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 cmd/ssh-auth-none-demo/ssh-auth-none-demo.go diff --git a/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go b/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go new file mode 100644 index 000000000..4f975baed --- /dev/null +++ b/cmd/ssh-auth-none-demo/ssh-auth-none-demo.go @@ -0,0 +1,171 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// ssh-auth-none-demo is a demo SSH server that's meant to run on the +// public internet 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" + "io/ioutil" + "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 { + return &gossh.ServerConfig{ + ImplicitAuthMethod: "tailscale", + NoClientAuth: true, // required for the NoClientAuthCallback to run + NoClientAuthCallback: func(gossh.ConnMetadata) (*gossh.Permissions, error) { + 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 := ioutil.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 +}