Add -ssh: an opt-in built-in SSH server (public keys only, sessions only)
This commit is contained in:
1 parent
2dea372f9a
commit
fc3d98ac83
10 files changed
+654
No files matched your search
@@ -0,0 +1,87 @@
|
||||
//go:build !windows
|
||||
|
||||
package sshd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
ptySupported = true
|
||||
runSupported = true
|
||||
)
|
||||
|
||||
// run starts the user's shell (or, for exec, runs cmd through it) and returns its exit code.
|
||||
// With a pty request it gets a terminal, otherwise plain pipes.
|
||||
func run(ch ssh.Channel, cmd string, p *ptyReq, win <-chan [2]uint32) int {
|
||||
shell := os.Getenv("SHELL")
|
||||
if shell == "" {
|
||||
shell = "/bin/sh"
|
||||
}
|
||||
var c *exec.Cmd
|
||||
if cmd == "" {
|
||||
c = exec.Command(shell, "-l")
|
||||
} else {
|
||||
c = exec.Command(shell, "-c", cmd)
|
||||
}
|
||||
c.Env = os.Environ()
|
||||
if p != nil {
|
||||
term := p.term
|
||||
if term == "" {
|
||||
term = "xterm"
|
||||
}
|
||||
c.Env = append(c.Env, "TERM="+term)
|
||||
return runPty(ch, c, p, win)
|
||||
}
|
||||
in, err := c.StdinPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintf(ch.Stderr(), "bamboo: %v\r\n", err)
|
||||
return 255
|
||||
}
|
||||
go func() { io.Copy(in, ch); in.Close() }()
|
||||
c.Stdout = ch
|
||||
c.Stderr = ch.Stderr()
|
||||
return exitCode(c.Run(), c)
|
||||
}
|
||||
|
||||
func runPty(ch ssh.Channel, c *exec.Cmd, p *ptyReq, win <-chan [2]uint32) int {
|
||||
f, err := pty.StartWithSize(c, &pty.Winsize{Cols: uint16(p.cols), Rows: uint16(p.rows)})
|
||||
if err != nil {
|
||||
fmt.Fprintf(ch.Stderr(), "bamboo: %v\r\n", err)
|
||||
return 255
|
||||
}
|
||||
defer f.Close()
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case w := <-win:
|
||||
pty.Setsize(f, &pty.Winsize{Cols: uint16(w[0]), Rows: uint16(w[1])})
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() { io.Copy(f, ch) }()
|
||||
io.Copy(ch, f) // ends with an error when the shell exits and the terminal closes
|
||||
return exitCode(c.Wait(), c)
|
||||
}
|
||||
|
||||
func exitCode(err error, c *exec.Cmd) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && ee.ExitCode() >= 0 {
|
||||
return ee.ExitCode()
|
||||
}
|
||||
return 255
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build !windows
|
||||
|
||||
package sshd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func session(t *testing.T) (*ssh.Session, func()) {
|
||||
t.Helper()
|
||||
good := newSigner(t)
|
||||
addr, _, _ := start(t, good)
|
||||
c, err := dial(addr, ssh.PublicKeys(good))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := c.NewSession()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s, func() { s.Close(); c.Close() }
|
||||
}
|
||||
|
||||
func TestExec(t *testing.T) {
|
||||
s, done := session(t)
|
||||
defer done()
|
||||
out, err := s.Output("echo hello; echo oops >&2")
|
||||
if err != nil || string(out) != "hello\n" {
|
||||
t.Fatalf("got %q, %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecExitStatus(t *testing.T) {
|
||||
s, done := session(t)
|
||||
defer done()
|
||||
err := s.Run("exit 3")
|
||||
var ee *ssh.ExitError
|
||||
if !errors.As(err, &ee) || ee.ExitStatus() != 3 {
|
||||
t.Fatalf("want exit status 3, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecReadsStdin(t *testing.T) {
|
||||
s, done := session(t)
|
||||
defer done()
|
||||
s.Stdin = strings.NewReader("from the client\n")
|
||||
out, err := s.Output("cat")
|
||||
if err != nil || string(out) != "from the client\n" {
|
||||
t.Fatalf("got %q, %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPtyExec(t *testing.T) {
|
||||
s, done := session(t)
|
||||
defer done()
|
||||
if err := s.RequestPty("xterm", 24, 80, ssh.TerminalModes{ssh.ECHO: 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := s.CombinedOutput(`echo term=$TERM; test -t 0 && echo tty`)
|
||||
if err != nil || !strings.Contains(string(out), "term=xterm") || !strings.Contains(string(out), "tty") {
|
||||
t.Fatalf("got %q, %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShell(t *testing.T) {
|
||||
s, done := session(t)
|
||||
defer done()
|
||||
s.Stdin = strings.NewReader("echo from-shell\nexit 4\n")
|
||||
var out strings.Builder
|
||||
s.Stdout = &out
|
||||
err := s.Shell()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = s.Wait()
|
||||
var ee *ssh.ExitError
|
||||
if !errors.As(err, &ee) || ee.ExitStatus() != 4 || !strings.Contains(out.String(), "from-shell") {
|
||||
t.Fatalf("got %q, %v", out.String(), err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package sshd
|
||||
|
||||
import "golang.org/x/crypto/ssh"
|
||||
|
||||
// Shells need a pseudo-terminal, which Windows has no simple equivalent of here, so the server
|
||||
// signs people in but refuses to start a session. main reports this before it starts the server.
|
||||
const (
|
||||
ptySupported = false
|
||||
runSupported = false
|
||||
)
|
||||
|
||||
func run(ssh.Channel, string, *ptyReq, <-chan [2]uint32) int { return 1 }
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
// Package sshd is a small SSH server for Bamboo, so a machine behind NAT can be reached with an
|
||||
// ordinary ssh client through a tunnelled port, without installing or configuring sshd. It is
|
||||
// opt-in and deliberately narrow: public keys only (listed in an authorized_keys file), session
|
||||
// channels only (a shell or one command, no port forwarding, no agent forwarding, no SFTP), and
|
||||
// it listens on the loopback address unless told otherwise. Sessions run as the user Bamboo runs as.
|
||||
package sshd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultListen is loopback only: a Hammerhead port that targets this site reaches it
|
||||
// through Bamboo, and nothing else on the network can.
|
||||
DefaultListen = "127.0.0.1:2222"
|
||||
|
||||
maxConns = 16
|
||||
handshakeTimeout = 20 * time.Second
|
||||
)
|
||||
|
||||
// Supported says whether this platform can run shells (it cannot on Windows).
|
||||
const Supported = runSupported
|
||||
|
||||
// Config says where the server listens and which keys may sign in.
|
||||
type Config struct {
|
||||
Listen string // host:port, default DefaultListen
|
||||
AuthorizedKeys string // path of an authorized_keys file; read on every sign-in, so edits apply at once
|
||||
HostKey string // path of the server's ed25519 key; created on first start
|
||||
Logf func(format string, args ...any)
|
||||
}
|
||||
|
||||
// Server accepts SSH connections.
|
||||
type Server struct {
|
||||
cfg Config
|
||||
conf *ssh.ServerConfig
|
||||
host ssh.Signer
|
||||
sem chan struct{}
|
||||
}
|
||||
|
||||
// New loads (or creates) the host key and checks that at least one authorized key exists, so
|
||||
// the server never starts in a state where nobody, or everybody, could sign in.
|
||||
func New(cfg Config) (*Server, error) {
|
||||
if cfg.Listen == "" {
|
||||
cfg.Listen = DefaultListen
|
||||
}
|
||||
if cfg.Logf == nil {
|
||||
cfg.Logf = func(string, ...any) {}
|
||||
}
|
||||
if cfg.AuthorizedKeys == "" {
|
||||
return nil, errors.New("no authorized_keys file configured")
|
||||
}
|
||||
keys, err := loadAuthorizedKeys(cfg.AuthorizedKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, fmt.Errorf("%s has no public keys: add the keys that may sign in, one per line", cfg.AuthorizedKeys)
|
||||
}
|
||||
signer, err := loadOrCreateHostKey(cfg.HostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Server{cfg: cfg, host: signer, sem: make(chan struct{}, maxConns)}
|
||||
s.conf = &ssh.ServerConfig{
|
||||
MaxAuthTries: 3,
|
||||
// Only public keys. There is no password or keyboard-interactive callback at all.
|
||||
PublicKeyCallback: func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
keys, err := loadAuthorizedKeys(s.cfg.AuthorizedKeys)
|
||||
if err != nil {
|
||||
s.cfg.Logf("ssh: %v", err)
|
||||
return nil, errors.New("no access")
|
||||
}
|
||||
for _, k := range keys {
|
||||
if bytes.Equal(k.Marshal(), key.Marshal()) {
|
||||
return &ssh.Permissions{Extensions: map[string]string{"fp": ssh.FingerprintSHA256(key)}}, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("key not authorized")
|
||||
},
|
||||
}
|
||||
s.conf.AddHostKey(signer)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Fingerprint is the server's host key fingerprint, for the log so it can be checked on first use.
|
||||
func (s *Server) Fingerprint() string { return ssh.FingerprintSHA256(s.host.PublicKey()) }
|
||||
|
||||
// Run listens until ctx is cancelled.
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
ln, err := net.Listen("tcp", s.cfg.Listen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(ctx, ln)
|
||||
}
|
||||
|
||||
// Serve accepts connections on ln until ctx is cancelled.
|
||||
func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
|
||||
go func() { <-ctx.Done(); ln.Close() }()
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case s.sem <- struct{}{}:
|
||||
default:
|
||||
c.Close() // too many sessions already
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-s.sem }()
|
||||
s.handle(ctx, c)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handle(ctx context.Context, nc net.Conn) {
|
||||
defer nc.Close()
|
||||
nc.SetDeadline(time.Now().Add(handshakeTimeout))
|
||||
conn, chans, reqs, err := ssh.NewServerConn(nc, s.conf)
|
||||
if err != nil {
|
||||
s.cfg.Logf("ssh: sign-in from %s failed: %v", nc.RemoteAddr(), err)
|
||||
return
|
||||
}
|
||||
nc.SetDeadline(time.Time{})
|
||||
defer conn.Close()
|
||||
s.cfg.Logf("ssh: %s signed in as %s (%s)", conn.User(), nc.RemoteAddr(), conn.Permissions.Extensions["fp"])
|
||||
go func() { <-ctx.Done(); conn.Close() }()
|
||||
go ssh.DiscardRequests(reqs)
|
||||
var wg sync.WaitGroup
|
||||
for ch := range chans {
|
||||
if ch.ChannelType() != "session" {
|
||||
ch.Reject(ssh.UnknownChannelType, "only sessions are allowed")
|
||||
continue
|
||||
}
|
||||
c, creqs, err := ch.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.session(c, creqs)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type ptyReq struct {
|
||||
term string
|
||||
cols, rows uint32
|
||||
}
|
||||
|
||||
// session answers a session channel's requests: pty-req, window-change, env (ignored), shell and exec.
|
||||
func (s *Server) session(ch ssh.Channel, reqs <-chan *ssh.Request) {
|
||||
defer ch.Close()
|
||||
var pty *ptyReq
|
||||
var win chan [2]uint32
|
||||
for req := range reqs {
|
||||
switch req.Type {
|
||||
case "pty-req":
|
||||
var p struct {
|
||||
Term string
|
||||
Cols, Rows, Wpx, Hpx uint32
|
||||
Modes string
|
||||
}
|
||||
if ssh.Unmarshal(req.Payload, &p) != nil || !ptySupported {
|
||||
req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
pty = &ptyReq{term: p.Term, cols: p.Cols, rows: p.Rows}
|
||||
win = make(chan [2]uint32, 4)
|
||||
req.Reply(true, nil)
|
||||
case "window-change":
|
||||
var w struct{ Cols, Rows, Wpx, Hpx uint32 }
|
||||
if ssh.Unmarshal(req.Payload, &w) == nil && win != nil {
|
||||
select {
|
||||
case win <- [2]uint32{w.Cols, w.Rows}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
req.Reply(true, nil)
|
||||
case "env":
|
||||
req.Reply(true, nil) // accepted and ignored: the client cannot change the environment
|
||||
case "shell", "exec":
|
||||
var cmd string
|
||||
if req.Type == "exec" {
|
||||
var e struct{ Command string }
|
||||
if ssh.Unmarshal(req.Payload, &e) != nil {
|
||||
req.Reply(false, nil)
|
||||
return
|
||||
}
|
||||
cmd = e.Command
|
||||
}
|
||||
if !runSupported {
|
||||
req.Reply(false, nil)
|
||||
return
|
||||
}
|
||||
req.Reply(true, nil)
|
||||
code := run(ch, cmd, pty, win)
|
||||
ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Code uint32 }{uint32(code)}))
|
||||
return
|
||||
default:
|
||||
req.Reply(false, nil) // subsystem (sftp), agent and X11 forwarding, signals: not offered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadAuthorizedKeys reads the public keys from an authorized_keys file. Options before a key
|
||||
// (command=, from=, ...) are not supported, so a line that has them is skipped rather than being
|
||||
// honoured too loosely.
|
||||
func loadAuthorizedKeys(path string) ([]ssh.PublicKey, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authorized keys: %w", err)
|
||||
}
|
||||
var keys []ssh.PublicKey
|
||||
for len(b) > 0 {
|
||||
key, _, options, rest, err := ssh.ParseAuthorizedKey(b)
|
||||
if err != nil {
|
||||
break // comments and blank lines are skipped by the parser; anything else ends the list
|
||||
}
|
||||
b = rest
|
||||
if len(options) > 0 {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// loadOrCreateHostKey keeps the server's identity across restarts, so clients are not warned
|
||||
// that it changed.
|
||||
func loadOrCreateHostKey(path string) (ssh.Signer, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("no host key path configured")
|
||||
}
|
||||
if b, err := os.ReadFile(path); err == nil {
|
||||
return ssh.ParsePrivateKey(b)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(path, pemBytes, 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ssh.ParsePrivateKey(pemBytes)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package sshd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func newSigner(t *testing.T) ssh.Signer {
|
||||
t.Helper()
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := ssh.NewSignerFromKey(priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func authLine(s ssh.Signer) string { return string(ssh.MarshalAuthorizedKey(s.PublicKey())) }
|
||||
|
||||
// start runs a server that accepts the given signers and returns its address.
|
||||
func start(t *testing.T, allowed ...ssh.Signer) (addr, keysFile string, srv *Server) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
keysFile = filepath.Join(dir, "authorized_keys")
|
||||
var lines string
|
||||
for _, s := range allowed {
|
||||
lines += authLine(s)
|
||||
}
|
||||
if err := os.WriteFile(keysFile, []byte("# who may sign in\n"+lines), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv, err := New(Config{AuthorizedKeys: keysFile, HostKey: filepath.Join(dir, "host_key"), Logf: t.Logf})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() { srv.Serve(ctx, ln); close(done) }()
|
||||
t.Cleanup(func() { cancel(); <-done })
|
||||
return ln.Addr().String(), keysFile, srv
|
||||
}
|
||||
|
||||
func dial(addr string, auth ...ssh.AuthMethod) (*ssh.Client, error) {
|
||||
return ssh.Dial("tcp", addr, &ssh.ClientConfig{User: "tester", Auth: auth, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second})
|
||||
}
|
||||
|
||||
func TestNewNeedsKeys(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := Config{AuthorizedKeys: filepath.Join(dir, "authorized_keys"), HostKey: filepath.Join(dir, "host_key")}
|
||||
if _, err := New(cfg); err == nil {
|
||||
t.Fatal("a missing authorized_keys file should be an error")
|
||||
}
|
||||
os.WriteFile(cfg.AuthorizedKeys, []byte("# nothing yet\n\n"), 0o600)
|
||||
if _, err := New(cfg); err == nil {
|
||||
t.Fatal("an authorized_keys file without keys should be an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsLinesAreSkipped(t *testing.T) {
|
||||
a, b := newSigner(t), newSigner(t)
|
||||
path := filepath.Join(t.TempDir(), "authorized_keys")
|
||||
os.WriteFile(path, []byte(`command="/bin/false" `+authLine(a)+authLine(b)), 0o600)
|
||||
keys, err := loadAuthorizedKeys(path)
|
||||
if err != nil || len(keys) != 1 || ssh.FingerprintSHA256(keys[0]) != ssh.FingerprintSHA256(b.PublicKey()) {
|
||||
t.Fatalf("only the key without options should load, got %d keys, err %v", len(keys), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostKeyIsStable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
keys := filepath.Join(dir, "authorized_keys")
|
||||
os.WriteFile(keys, []byte(authLine(newSigner(t))), 0o600)
|
||||
cfg := Config{AuthorizedKeys: keys, HostKey: filepath.Join(dir, "sub", "host_key")}
|
||||
a, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.Fingerprint() != b.Fingerprint() {
|
||||
t.Fatal("the host key changed between starts")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyAuth(t *testing.T) {
|
||||
good, bad := newSigner(t), newSigner(t)
|
||||
addr, _, _ := start(t, good)
|
||||
|
||||
c, err := dial(addr, ssh.PublicKeys(good))
|
||||
if err != nil {
|
||||
t.Fatalf("authorized key refused: %v", err)
|
||||
}
|
||||
c.Close()
|
||||
if _, err := dial(addr, ssh.PublicKeys(bad)); err == nil {
|
||||
t.Fatal("unknown key was accepted")
|
||||
}
|
||||
if _, err := dial(addr, ssh.Password("hunter2")); err == nil {
|
||||
t.Fatal("a password was accepted")
|
||||
}
|
||||
if _, err := dial(addr, ssh.KeyboardInteractive(func(string, string, []string, []bool) ([]string, error) { return nil, nil })); err == nil {
|
||||
t.Fatal("keyboard-interactive was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeysCanChangeWhileRunning(t *testing.T) {
|
||||
first, second := newSigner(t), newSigner(t)
|
||||
addr, keysFile, _ := start(t, first)
|
||||
if _, err := dial(addr, ssh.PublicKeys(second)); err == nil {
|
||||
t.Fatal("second key accepted before it was added")
|
||||
}
|
||||
f, _ := os.OpenFile(keysFile, os.O_APPEND|os.O_WRONLY, 0o600)
|
||||
f.WriteString(authLine(second))
|
||||
f.Close()
|
||||
c, err := dial(addr, ssh.PublicKeys(second))
|
||||
if err != nil {
|
||||
t.Fatalf("key added to the file was not picked up: %v", err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func TestNoForwarding(t *testing.T) {
|
||||
good := newSigner(t)
|
||||
addr, _, _ := start(t, good)
|
||||
c, err := dial(addr, ssh.PublicKeys(good))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
if conn, err := c.Dial("tcp", "127.0.0.1:1"); err == nil {
|
||||
conn.Close()
|
||||
t.Fatal("port forwarding was allowed")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user