For SSH client authors to fix their clients without setting up Tailscale stuff. Change-Id: I8c7049398512de6cb91c13716d4dcebed4d47b9c Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
b2994568fe
commit
8a187159b2
@ -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 |
||||
} |
||||
Loading…
Reference in new issue