mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01:00 
			
		
		
		
	The upstream crypto package now supports sending banners at any time during authentication, so the Tailscale fork of crypto/ssh is no longer necessary. github.com/tailscale/golang-x-crypto is still needed for some custom ACME autocert functionality. tempfork/gliderlabs is still necessary because of a few other customizations, mostly related to TTY handling. Originally implemented in 46fd4e58a27495263336b86ee961ee28d8c332b7, which was reverted in b60f6b849af1fae1cf343be98f7fb1714c9ea165 to keep the change out of v1.80. Updates #8593 Signed-off-by: Percy Wegmann <percy@tailscale.com>
		
			
				
	
	
		
			200 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			200 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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.
 | |
| //
 | |
| // Connections are allowed using any username except for "denyme". Connecting as
 | |
| // "denyme" will result in an authentication failure with error message.
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"crypto/ecdsa"
 | |
| 	"crypto/ed25519"
 | |
| 	"crypto/elliptic"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/rsa"
 | |
| 	"crypto/x509"
 | |
| 	"encoding/pem"
 | |
| 	"errors"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"time"
 | |
| 
 | |
| 	gossh "golang.org/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()
 | |
| 			var spac gossh.ServerPreAuthConn
 | |
| 			return &gossh.ServerConfig{
 | |
| 				PreAuthConnCallback: func(conn gossh.ServerPreAuthConn) {
 | |
| 					spac = conn
 | |
| 				},
 | |
| 				NoClientAuth: true, // required for the NoClientAuthCallback to run
 | |
| 				NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
 | |
| 					spac.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
 | |
| 
 | |
| 					if cm.User() == "denyme" {
 | |
| 						return nil, &gossh.BannerError{
 | |
| 							Err:     errors.New("denying access"),
 | |
| 							Message: "denyme is not allowed to access this machine\n",
 | |
| 						}
 | |
| 					}
 | |
| 
 | |
| 					totalBanners := 2
 | |
| 					if cm.User() == "banners" {
 | |
| 						totalBanners = 5
 | |
| 					}
 | |
| 					for banner := 2; banner <= totalBanners; banner++ {
 | |
| 						time.Sleep(time.Second)
 | |
| 						if banner == totalBanners {
 | |
| 							spac.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
 | |
| 						} else {
 | |
| 							spac.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
 | |
| }
 |