Go语言 ssh


原文链接: Go语言 ssh

Go ssh 交互式执行命令 - 漠然的博客
SSH Client connection in Golang · Software adventures and thoughts

认证方法 rfc4252

3. 使用ssh agent的公钥认证

为解决每次登陆远程机器都需要输入passphrase的问题,ssh-agent被引入了。ssh-agent启动后,可通过ssh-add将私钥加入agent. ssh-add会提示用户输入passphrase以解密私钥,然后将解密后的私钥纳入agent管理。agent可同时管理多个私钥。如果ssh-agent中有多个私钥, 会依次尝试,直到认证通过或遍历所有私钥

passphrase和 passcode都是password的一种,只不过
passphrase是满足了一定规则约束的password, 安全性要高一些;
passcode指由纯数字组成的password
// 1. 根据privte key 或 passwd 生成 ssh.Signer

    `signer, err := ssh.ParsePrivateKey(key)`

// 2. 根据 ssh.Signer 生成 []ssh.AuthMethod

    `AuthMethod := ssh.PublicKeys(signer)`

// 3. 根据 ssh.AuthMethod 生成 ssh.ClientConfig

   ClientConfig := &ssh.ClientConfig{
        User: "user",
        Auth: []ssh.AuthMethod{
            // Use the PublicKeys method for remote authentication.
            ssh.PublicKeys(signer),
        },
        HostKeyCallback: ssh.FixedHostKey(hostKey),
    }
    ClientConfig.SetDefaults()

// 4. 根据 ssh.ClientConfig 创建 ssh.Clent

  `client, err := ssh.Dial("tcp", "host.com:22", ClientConfig)`

// 5. 创建 session

   `session, err := client.NewSession()
    defer session.Close()`

// 6. 运行

    `err := session.Run("/usr/bin/ls")`

v1 和 v2 存在一些区别:What’s the difference between versions 1 and 2 of the SSH protocol?

type AuthMethod interface {
    // contains filtered or unexported methods
}

func KeyboardInteractive(challenge KeyboardInteractiveChallenge) AuthMethod
func Password(secret string) AuthMethod
func PasswordCallback(prompt func() (secret string, err error)) AuthMethod
func PublicKeys(signers ...Signer) AuthMethod
func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMethod
func RetryableAuthMethod(auth AuthMethod, maxTries int) AuthMethod

有以上几种认证方法,其中 RetryableAuthMethod 方法就是将其他的方法封装一下,然后如果失败使用重试机制,这样确保其他原因导致的连接失败,参数 maxTries 表示重试次数,如果 <= 0 ,则会无限期的重试,直到成功。

PKI - Public Key Infrastructure

// 官方示例:https://godoc.org/golang.org/x/crypto/ssh
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 := ioutil.ReadFile("/home/user/.ssh/id_rsa")
if err != nil {
    log.Fatalf("unable to read private key: %v", err)
}

// 1. Create the Signer for this private key.
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
    log.Fatalf("unable to parse private key: %v", err)
}
// 2. 
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()

config := &ssh.ClientConfig{
    User: "root",
    Auth: []ssh.AuthMethod{
        ssh.Password("xxxxx"),
    },
    HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
        return nil
    },
}
client, err := ssh.Dial("tcp", "host:port", config)
if err != nil {
    panic("Failed to dial: " + err.Error())
}

session, err := client.NewSession()
if err != nil {
    panic("Failed to create session: " + err.Error())
}
defer session.Close()

var b bytes.Buffer
session.Stdout = &b
if err := session.Run("/usr/bin/ls"); err != nil {
    panic("Failed to run: " + err.Error())
}

Client

type Client struct {
    Conn
    // contains filtered or unexported fields
}

func Dial(network, addr string, config *ClientConfig) (*Client, error)

建立 Client:

config := &ssh.ClientConfig{
    User: "root",
    Auth: []ssh.AuthMethod{
        ssh.Password("xxxxx"),
    },
    HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
        return nil
    },
}
client, err := ssh.Dial("tcp", "host:port", config)
if err != nil {
    panic("Failed to dial: " + err.Error())
}

我们也可以建立Client,后针对服务端进行一些系统的动作,比如:

端口监听:client.Listen("tcp", "0.0.0.0:8080")
服务器和别的机器又建立联系:func (c *Client) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error)
建立会话:func (c *Client) NewSession() (*Session, error),这样就可以在这个会话上进行一系列的操作了,比如运行命令什么的
…

建立 Client 的时候,我们需要用到一些配置:

type ClientConfig struct {
    Config

    User string
    Auth []AuthMethod
    HostKeyCallback HostKeyCallback
    ClientVersion string
    HostKeyAlgorithms []string
    Timeout time.Duration
}

例如:

config := &ssh.ClientConfig{

User: "root",
Auth: []ssh.AuthMethod{
    ssh.Password("pwd"),
},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
    return nil
},

}

Session

往往我们通过 Session ,和服务器进行一系列的交互操作。

type Session struct {

// Stdin specifies the remote process's standard input.
// If Stdin is nil, the remote process reads from an empty
// bytes.Buffer.
Stdin io.Reader

// Stdout and Stderr specify the remote process's standard
// output and error.
//
// If either is nil, Run connects the corresponding file
// descriptor to an instance of ioutil.Discard. There is a
// fixed amount of buffering that is shared for the two streams.
// If either blocks it may eventually cause the remote
// command to block.
Stdout io.Writer
Stderr io.Writer
// contains filtered or unexported fields

}

交互操作:

config := &ssh.ClientConfig{
    User: "root",
    Auth: []ssh.AuthMethod{
        ssh.Password("xxxx"),
    },
    HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
        return nil
    },
}
client, err := ssh.Dial("tcp", "host:port", config)
if err != nil {
    panic("Failed to dial: " + err.Error())
}

// Each ClientConn can support multiple interactive sessions,
// represented by a Session.
session, err := client.NewSession()
if err != nil {
    panic("Failed to create session: " + err.Error())
}
defer session.Close()

// 需要重定向才能在当前机器上显示对应的结果
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin

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
}

// 设置交互会话窗口属性
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
    session.Close()
    fmt.Printf("request for pseudo terminal failed: %s", err)
}
session.Run("top")
`