mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
11 KiB
Go
401 lines
11 KiB
Go
// Copyright 2011 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ssh_test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
func ExampleNewServerConn() {
|
|
// Public key authentication is done by comparing
|
|
// the public key of a received connection
|
|
// with the entries in the authorized_keys file.
|
|
authorizedKeysBytes, err := os.ReadFile("authorized_keys")
|
|
if err != nil {
|
|
log.Fatalf("Failed to load authorized_keys, err: %v", err)
|
|
}
|
|
|
|
authorizedKeysMap := map[string]bool{}
|
|
for len(authorizedKeysBytes) > 0 {
|
|
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
authorizedKeysMap[string(pubKey.Marshal())] = true
|
|
authorizedKeysBytes = rest
|
|
}
|
|
|
|
// An SSH server is represented by a ServerConfig, which holds
|
|
// certificate details and handles authentication of ServerConns.
|
|
config := &ssh.ServerConfig{
|
|
// Remove to disable password auth.
|
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
// Should use constant-time compare (or better, salt+hash) in
|
|
// a production setting.
|
|
if c.User() == "testuser" && string(pass) == "tiger" {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("password rejected for %q", c.User())
|
|
},
|
|
|
|
// Remove to disable public key auth.
|
|
PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
|
if authorizedKeysMap[string(pubKey.Marshal())] {
|
|
return &ssh.Permissions{
|
|
// Record the public key used for authentication.
|
|
Extensions: map[string]string{
|
|
"pubkey-fp": ssh.FingerprintSHA256(pubKey),
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown public key for %q", c.User())
|
|
},
|
|
}
|
|
|
|
privateBytes, err := os.ReadFile("id_rsa")
|
|
if err != nil {
|
|
log.Fatal("Failed to load private key: ", err)
|
|
}
|
|
|
|
private, err := ssh.ParsePrivateKey(privateBytes)
|
|
if err != nil {
|
|
log.Fatal("Failed to parse private key: ", err)
|
|
}
|
|
config.AddHostKey(private)
|
|
|
|
// Once a ServerConfig has been configured, connections can be
|
|
// accepted.
|
|
listener, err := net.Listen("tcp", "0.0.0.0:2022")
|
|
if err != nil {
|
|
log.Fatal("failed to listen for connection: ", err)
|
|
}
|
|
nConn, err := listener.Accept()
|
|
if err != nil {
|
|
log.Fatal("failed to accept incoming connection: ", err)
|
|
}
|
|
|
|
// Before use, a handshake must be performed on the incoming
|
|
// net.Conn.
|
|
conn, chans, reqs, err := ssh.NewServerConn(nConn, config)
|
|
if err != nil {
|
|
log.Fatal("failed to handshake: ", err)
|
|
}
|
|
log.Printf("logged in with key %s", conn.Permissions.Extensions["pubkey-fp"])
|
|
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
// The incoming Request channel must be serviced.
|
|
wg.Add(1)
|
|
go func() {
|
|
ssh.DiscardRequests(reqs)
|
|
wg.Done()
|
|
}()
|
|
|
|
// Service the incoming Channel channel.
|
|
for newChannel := range chans {
|
|
// Channels have a type, depending on the application level
|
|
// protocol intended. In the case of a shell, the type is
|
|
// "session" and ServerShell may be used to present a simple
|
|
// terminal interface.
|
|
if newChannel.ChannelType() != "session" {
|
|
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
|
|
continue
|
|
}
|
|
channel, requests, err := newChannel.Accept()
|
|
if err != nil {
|
|
log.Fatalf("Could not accept channel: %v", err)
|
|
}
|
|
|
|
// Sessions have out-of-band requests such as "shell",
|
|
// "pty-req" and "env". Here we handle only the
|
|
// "shell" request.
|
|
wg.Add(1)
|
|
go func(in <-chan *ssh.Request) {
|
|
for req := range in {
|
|
req.Reply(req.Type == "shell", nil)
|
|
}
|
|
wg.Done()
|
|
}(requests)
|
|
|
|
term := terminal.NewTerminal(channel, "> ")
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer func() {
|
|
channel.Close()
|
|
wg.Done()
|
|
}()
|
|
for {
|
|
line, err := term.ReadLine()
|
|
if err != nil {
|
|
break
|
|
}
|
|
fmt.Println(line)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func ExampleServerConfig_AddHostKey() {
|
|
// Minimal ServerConfig supporting only password authentication.
|
|
config := &ssh.ServerConfig{
|
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
// Should use constant-time compare (or better, salt+hash) in
|
|
// a production setting.
|
|
if c.User() == "testuser" && string(pass) == "tiger" {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("password rejected for %q", c.User())
|
|
},
|
|
}
|
|
|
|
privateBytes, err := os.ReadFile("id_rsa")
|
|
if err != nil {
|
|
log.Fatal("Failed to load private key: ", err)
|
|
}
|
|
|
|
private, err := ssh.ParsePrivateKey(privateBytes)
|
|
if err != nil {
|
|
log.Fatal("Failed to parse private key: ", err)
|
|
}
|
|
// Restrict host key algorithms to disable ssh-rsa.
|
|
signer, err := ssh.NewSignerWithAlgorithms(private.(ssh.AlgorithmSigner), []string{ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512})
|
|
if err != nil {
|
|
log.Fatal("Failed to create private key with restricted algorithms: ", err)
|
|
}
|
|
config.AddHostKey(signer)
|
|
}
|
|
|
|
func ExampleClientConfig_HostKeyCallback() {
|
|
// Every client must provide a host key check. Here is a
|
|
// simple-minded parse of OpenSSH's known_hosts file
|
|
host := "hostname"
|
|
file, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
var hostKey ssh.PublicKey
|
|
for scanner.Scan() {
|
|
fields := strings.Split(scanner.Text(), " ")
|
|
if len(fields) != 3 {
|
|
continue
|
|
}
|
|
if strings.Contains(fields[0], host) {
|
|
var err error
|
|
hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
|
|
if err != nil {
|
|
log.Fatalf("error parsing %q: %v", fields[2], err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if hostKey == nil {
|
|
log.Fatalf("no hostkey for %s", host)
|
|
}
|
|
|
|
config := ssh.ClientConfig{
|
|
User: os.Getenv("USER"),
|
|
HostKeyCallback: ssh.FixedHostKey(hostKey),
|
|
}
|
|
|
|
_, err = ssh.Dial("tcp", host+":22", &config)
|
|
log.Println(err)
|
|
}
|
|
|
|
func ExampleDial() {
|
|
var hostKey ssh.PublicKey
|
|
// An SSH client is represented with a ClientConn.
|
|
//
|
|
// To authenticate with the remote server you must pass at least one
|
|
// implementation of AuthMethod via the Auth field in ClientConfig,
|
|
// and provide a HostKeyCallback.
|
|
config := &ssh.ClientConfig{
|
|
User: "username",
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password("yourpassword"),
|
|
},
|
|
HostKeyCallback: ssh.FixedHostKey(hostKey),
|
|
}
|
|
client, err := ssh.Dial("tcp", "yourserver.com:22", config)
|
|
if err != nil {
|
|
log.Fatal("Failed to dial: ", err)
|
|
}
|
|
defer client.Close()
|
|
|
|
// Each ClientConn can support multiple interactive sessions,
|
|
// represented by a Session.
|
|
session, err := client.NewSession()
|
|
if err != nil {
|
|
log.Fatal("Failed to create session: ", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
// Once a Session is created, you can execute a single command on
|
|
// the remote side using the Run method.
|
|
var b bytes.Buffer
|
|
session.Stdout = &b
|
|
if err := session.Run("/usr/bin/whoami"); err != nil {
|
|
log.Fatal("Failed to run: " + err.Error())
|
|
}
|
|
fmt.Println(b.String())
|
|
}
|
|
|
|
func ExamplePublicKeys() {
|
|
var hostKey ssh.PublicKey
|
|
// A public key may be used to authenticate against the remote
|
|
// server by using an unencrypted PEM-encoded private key file.
|
|
//
|
|
// If you have an encrypted private key, the crypto/x509 package
|
|
// can be used to decrypt it.
|
|
key, err := os.ReadFile("/home/user/.ssh/id_rsa")
|
|
if err != nil {
|
|
log.Fatalf("unable to read private key: %v", err)
|
|
}
|
|
|
|
// Create the Signer for this private key.
|
|
signer, err := ssh.ParsePrivateKey(key)
|
|
if err != nil {
|
|
log.Fatalf("unable to parse private key: %v", err)
|
|
}
|
|
|
|
config := &ssh.ClientConfig{
|
|
User: "user",
|
|
Auth: []ssh.AuthMethod{
|
|
// Use the PublicKeys method for remote authentication.
|
|
ssh.PublicKeys(signer),
|
|
},
|
|
HostKeyCallback: ssh.FixedHostKey(hostKey),
|
|
}
|
|
|
|
// Connect to the remote server and perform the SSH handshake.
|
|
client, err := ssh.Dial("tcp", "host.com:22", config)
|
|
if err != nil {
|
|
log.Fatalf("unable to connect: %v", err)
|
|
}
|
|
defer client.Close()
|
|
}
|
|
|
|
func ExampleClient_Listen() {
|
|
var hostKey ssh.PublicKey
|
|
config := &ssh.ClientConfig{
|
|
User: "username",
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password("password"),
|
|
},
|
|
HostKeyCallback: ssh.FixedHostKey(hostKey),
|
|
}
|
|
// Dial your ssh server.
|
|
conn, err := ssh.Dial("tcp", "localhost:22", config)
|
|
if err != nil {
|
|
log.Fatal("unable to connect: ", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Request the remote side to open port 8080 on all interfaces.
|
|
l, err := conn.Listen("tcp", "0.0.0.0:8080")
|
|
if err != nil {
|
|
log.Fatal("unable to register tcp forward: ", err)
|
|
}
|
|
defer l.Close()
|
|
|
|
// Serve HTTP with your SSH server acting as a reverse proxy.
|
|
http.Serve(l, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
fmt.Fprintf(resp, "Hello world!\n")
|
|
}))
|
|
}
|
|
|
|
func ExampleSession_RequestPty() {
|
|
var hostKey ssh.PublicKey
|
|
// Create client config
|
|
config := &ssh.ClientConfig{
|
|
User: "username",
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password("password"),
|
|
},
|
|
HostKeyCallback: ssh.FixedHostKey(hostKey),
|
|
}
|
|
// Connect to ssh server
|
|
conn, err := ssh.Dial("tcp", "localhost:22", config)
|
|
if err != nil {
|
|
log.Fatal("unable to connect: ", err)
|
|
}
|
|
defer conn.Close()
|
|
// Create a session
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
log.Fatal("unable to create session: ", err)
|
|
}
|
|
defer session.Close()
|
|
// Set up terminal modes
|
|
modes := ssh.TerminalModes{
|
|
ssh.ECHO: 0, // disable echoing
|
|
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
|
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
|
}
|
|
// Request pseudo terminal
|
|
if err := session.RequestPty("xterm", 40, 80, modes); err != nil {
|
|
log.Fatal("request for pseudo terminal failed: ", err)
|
|
}
|
|
// Start remote shell
|
|
if err := session.Shell(); err != nil {
|
|
log.Fatal("failed to start shell: ", err)
|
|
}
|
|
}
|
|
|
|
func ExampleCertificate_SignCert() {
|
|
// Sign a certificate with a specific algorithm.
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 3072)
|
|
if err != nil {
|
|
log.Fatal("unable to generate RSA key: ", err)
|
|
}
|
|
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
log.Fatal("unable to get RSA public key: ", err)
|
|
}
|
|
caKey, err := rsa.GenerateKey(rand.Reader, 3072)
|
|
if err != nil {
|
|
log.Fatal("unable to generate CA key: ", err)
|
|
}
|
|
signer, err := ssh.NewSignerFromKey(caKey)
|
|
if err != nil {
|
|
log.Fatal("unable to generate signer from key: ", err)
|
|
}
|
|
mas, err := ssh.NewSignerWithAlgorithms(signer.(ssh.AlgorithmSigner), []string{ssh.KeyAlgoRSASHA256})
|
|
if err != nil {
|
|
log.Fatal("unable to create signer with algoritms: ", err)
|
|
}
|
|
certificate := ssh.Certificate{
|
|
Key: publicKey,
|
|
CertType: ssh.UserCert,
|
|
}
|
|
if err := certificate.SignCert(rand.Reader, mas); err != nil {
|
|
log.Fatal("unable to sign certificate: ", err)
|
|
}
|
|
// Save the public key to a file and check that rsa-sha-256 is used for
|
|
// signing:
|
|
// ssh-keygen -L -f <path to the file>
|
|
fmt.Println(string(ssh.MarshalAuthorizedKey(&certificate)))
|
|
}
|