Go实现SSH-Client客户端(2)

Admin 2022-05-07 17:10:55 GoLang

上一篇我们实现了一个SSH-Server的服务,这篇我们实现SSH-Client。SSH的客户端常见有交互式 tty-bash 和命令执行,其中命令执行又分为交互式 command 和一般的 bash 命令,如tail、top等属于交互式命令,而 ls、who 等属于一般的 bash 执行。

1、交互方式

package main

import (
    "log"
    "os"
    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/terminal"
)

func main() {
    config := &ssh.ClientConfig{
        User: "demo",
        Auth: []ssh.AuthMethod{ssh.Password("123456")},
    }

    client, err := ssh.Dial("tcp", "127.0.0.1:19022", config)
    if err != nil {
        log.Fatal(err)
    }

    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    session.Stdout = os.Stdout
    session.Stderr = os.Stderr
    session.Stdin = os.Stdin

    modes := ssh.TerminalModes{
        ssh.ECHO:          1,
        ssh.ECHOCTL:       0,
        ssh.TTY_OP_ISPEED: 14400,
        ssh.TTY_OP_OSPEED: 14400,
    }

    termFD := int(os.Stdin.Fd())
    w, h, _ := terminal.GetSize(termFD)
    termState, _ := terminal.MakeRaw(termFD)
    defer terminal.Restore(termFD, termState)

    err = session.RequestPty("xterm-256color", h, w, modes)
    if err != nil {
        log.Fatal(err)
    }

    session.Shell()
    session.Wait()
}

2、命令方式

  • 非交互命令(执行即返回结果)


package main

import (
    "log"
    "os"
    "golang.org/x/crypto/ssh"
)

func main() {
    config := &ssh.ClientConfig{
        User: "demo",
        Auth: []ssh.AuthMethod{ssh.Password("123456")},
    }

    client, err := ssh.Dial("tcp", "127.0.0.1:19022", config)
    if err != nil {
        log.Fatal(err)
    }

    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    session.Stdout = os.Stdout
    session.Stderr = os.Stderr

    err = session.Run("hostname")
    if err != nil {
        log.Println(err)
    }
}
  • 交互命令(需tty支持,交互式输入输出)

package main

import (
    "log"
    "os"
    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/terminal"
)

func main() {
    config := &ssh.ClientConfig{
        User: "demo",
        Auth: []ssh.AuthMethod{ssh.Password("123456")},
    }

    client, err := ssh.Dial("tcp", "127.0.0.1:19022", config)
    if err != nil {
        log.Fatal(err)
    }

    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    session.Stdout = os.Stdout
    session.Stderr = os.Stderr
    session.Stdin = os.Stdin

    modes := ssh.TerminalModes{
        ssh.ECHO:          1,
        ssh.TTY_OP_ISPEED: 14400,
        ssh.TTY_OP_OSPEED: 14400,
    }

    termFD := int(os.Stdin.Fd())
    w, h, _ := terminal.GetSize(termFD)
    termState, _ := terminal.MakeRaw(termFD)
    defer terminal.Restore(termFD, termState)

    err = session.RequestPty("xterm", h, w, modes)
    if err != nil {
        log.Fatal(err)
    }

    err = session.Run("hostname")
    if err != nil {
        log.Fatal(err)
    }
}

3、sshAgent机制

sshAgent 严格来说是一个客户端认证机制,客户端通过和 SSH_AUTH_SOCK 这个环境变量指向的 Unix Domain Socket(本质上是 SSHD 建立的)通信,以获取其存储的私钥(实际也是保存在内存中,用户不可见),以此作为 ssh 的客户端验证方式:

ssh_agent.gif

package main

import (
    "log"
    "os"
    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/agent"
    "net"
)

func main() {
    sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
    signers := ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
    if err != nil {
        log.Fatal(err)
    }

    config := &ssh.ClientConfig{
        User: "demo",
        Auth: []ssh.AuthMethod{signers},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    client, err := ssh.Dial("tcp", "127.0.0.1:19022", config)
    if err != nil {
        log.Fatal(err)
    }

    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    combo, err := session.CombinedOutput("who;hostname")
    if err != nil {
        log.Fatal(err)
    }
    log.Println("output: ", string(combo))
}


在此之前,需要在机器上开启 ssh-agent 功能及通过 ssh-add 向其中添加私钥:

eval `ssh-agent -s`
ssh-add ~/.ssh/id_rsa
env|grep SSH_AUTH_SOCK

4、hostkey认证

HostKeyCallback 为客户端验证 hostkey 的回调接口,通常我们都设置为 ssh.InsecureIgnoreHostKey(),但是这样是不安全的(SSH中间人攻击, 可参考https://www.ssh.com/attack/man-in-the-middle

更安全的做法,是在这里检查上一次登录中保存的机器指纹数据(或者系统采集的机器指纹数据)与登录时的指纹进行比对,相同才放行,HostKeyCallback 的原型如下:

HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
    // 实现机器指纹验证逻辑
    return nil
},
package main

import (
    kh "golang.org/x/crypto/ssh/knownhosts"
    "golang.org/x/crypto/ssh"
    "log"
    "os"
    "fmt"
)

func main() {
    hostKeyCallback, err := kh.New("/root/.ssh/known_hosts")
    if err != nil {
        log.Fatal("could not create hostkeycallback function:", err)
    }

    config := &ssh.ClientConfig{
        User: "demo",
        Auth: []ssh.AuthMethod{
            ssh.Password("123123"),
        },
        HostKeyCallback: hostKeyCallback,
    }


    client, err := ssh.Dial("tcp", "127.0.0.1:19022", config)
    if err != nil {
        log.Fatalf("unable to connect: %v", err)
    }
    defer client.Close()

    session, err := client.NewSession()
    if err != nil {
        log.Fatal("unable to create SSH session:", err)
    }
    defer session.Close()

    session.Stdout = os.Stdout
    session.Run(command)

    fmt.Println("done")
}

5、keepalive机制

如何实现 SSH 客户端的 keepalive 机制?答案是使用 SendRequest 定时向服务端发送心跳包,Stack Overflow 上给出了解决方案

func SSHDialTimeout(network, addr string, config *ssh.ClientConfig, timeout time.Duration) (*ssh.Client, error) {
    conn, err := net.DialTimeout(network, addr, timeout)
    if err != nil {
        return nil, err
    }

    timeoutConn := &Conn{conn, timeout, timeout}
    c, chans, reqs, err := ssh.NewClientConn(timeoutConn, addr, config)
    if err != nil {
        return nil, err
    }
    client := ssh.NewClient(c, chans, reqs)

    // this sends keepalive packets every 2 seconds
    // there's no useful response from these, so we can just abort if there's an error
    go func() {
        t := time.NewTicker(2 * time.Second)
        defer t.Stop()
        for range t.C {
            _, _, err := client.Conn.SendRequest("keepalive@golang.org", true, nil)
            if err != nil {
                return
            }
        }
    }()
    return client, nil}

不过,在现网中,我们是通过在交互式的 session 发送 SendRequest 来实现 keepalive,如下面的客户端代码:

func Client() {
        //....
        session, _ := client.NewSession()
        defer session.Close()
        session.Stdout = os.Stdout
        session.Stderr = os.Stderr
        session.Stdin = os.Stdin
        modes := ssh.TerminalModes{
                ssh.ECHO:          1,
                ssh.ECHOCTL:       0,
                ssh.TTY_OP_ISPEED: 14400,
                ssh.TTY_OP_OSPEED: 14400,
        }
        termFD := int(os.Stdin.Fd())
        w, h, _ := terminal.GetSize(termFD)
        termState, _ := terminal.MakeRaw(termFD)
        defer terminal.Restore(termFD, termState)
        err = session.RequestPty("xterm-256color", h, w, modes)
        go func() {
                for {
                        time.Sleep(time.Second * time.Duration(30))
                        // 在当前的 session 中发送 keepalive
                        session.SendRequest("keepalive@openssh.com", true, nil)
                }
        }()
        session.Shell()
        session.Wait()
        //...}

有个细节是,SSHD 服务端必须对 keepalive@openssh.com 这个 Request 有 reply 行为,不然的话,客户端会阻塞在 session.SendRequest 方法上;SSHD 服务端需要添加如下代码

go func() {
    for req := range requests {
        switch req.Type {
        //......
        case "keepalive@openssh.com":
            req.Reply(true, nil)
        }
}}()

6、SetEnv问题

SSH 客户端可以使用 Setenv 方法在 ssh 会话中设置环境变量,其原理也是通过 ssh.SendRequest 方法,但是这里需要注意的是:

在 SSH 服务端的配置 /etc/ssh/sshd_config 中需要加上这样的配置,如下:

AcceptEnv ENV_NAME

这样,客户端就可以设置名字为 ENV_NAME 的环境变量了。

func (s *Session) Setenv(name, value string) error {
	msg := setenvRequest{
		Name:  name,
		Value: value,
	}
	ok, err := s.ch.SendRequest("env", true, Marshal(&msg))
	if err == nil && !ok {
		err = errors.New("ssh: setenv failed")
	}
	return err}

参考

https://smallstep.com/blog/ssh-agent-explained/

https://blog.csdn.net/sdcxyz/article/details/41487897

https://github.com/parsiya/SSH-Scanner/blob/master/SSHHarvesterv1.go?ts=4

https://github.com/pandaychen/golang_in_action/blob/master/sshserver/sshserver.go#L167

https://github.com/rinetd/ssh/blob/master/auth.go

相关文章
最新推荐