1. SSH客户端使用

在运维开发中,有时会涉及到在目标机器上执行shell命令,或者需要通过ssh通道传输文件,此时ssh客户端的使用就必不可少。如果涉及频繁连接SSH客户端,建议使用连接池进行管理,降低SSH开销,同时避免过多的ssh连接导致被跳板机拒绝。使用golang实现ssh客户端场景较多,实现ssh服务端场景较少,此处仅实现ssh客户端作为示例。涉及到的包有:

  • golang.org/x/crypto/ssh
  • github.com/bramvdbogaerde/go-scp

    1.1. 使用ssh client

    在实际操作中,分两种情况,一种是直接操作目标主机,一种是通过跳板机操作目标机器,下面代码对两种情况进行了 ssh.Client 实现! ```go // 目标主机信息 type Host struct { IP string SSHPort int Username string Password string SSHKey string JumpServer *JumpServer }

// 跳板机 type JumpServer struct { IP string SSHPort int Username string Password string SSHKey string }

// 生成密钥信息, 分为两种:password 和 ssh key func sshClientConfig(passwd, key string) (auth []ssh.AuthMethod, err error) { if passwd != “” { auth = append(auth, ssh.Password(passwd)) } if key != “” { privateKey, err := ssh.ParsePrivateKey([]byte(key)) if err != nil { return nil, err } auth = append(auth, ssh.PublicKeys(privateKey)) } return }

// 生成ssh 客户端信息 func opensshClient(host Host) (client ssh.Client, err error) { if host.JumpServer == nil { return clientWithoutJumpServer(host) } return clientWithJumpServer(host) }

// 如果没有跳板机,则直接连接目标机器 func clientWithoutJumpServer(host Host) (client ssh.Client, err error) { auth, err := sshClientConfig(host.Password, host.SSHKey) // 生成密钥 if err != nil { return nil, err } // 生成ssh client的配置 config := &ssh.ClientConfig{ User: host.Username, Auth: auth, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, Timeout: time.Second * 10, } // 拨号 client, err = ssh.Dial(“tcp”, fmt.Sprintf(“%s:%d”, host.IP, host.SSHPort), config) if err != nil { return nil, err } return }

// 如果存在跳板机,则进行中转 func clientWithJumpServer(host Host) (client ssh.Client, err error) { // 生成跳板机密钥 jAuth, err := sshClientConfig(host.JumpServer.Password, host.JumpServer.SSHKey) if err != nil { return nil, err } // 生成跳板机的ssh client配置 jConfig := &ssh.ClientConfig{ User: host.JumpServer.Username, Auth: jAuth, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, Timeout: time.Second 10, } // 对跳板机进行拨号 jClient, err := ssh.Dial(“tcp”, fmt.Sprintf(“%s:%d”, host.JumpServer.IP, host.JumpServer.SSHPort), jConfig) if err != nil { return nil, err } // 生成目标机器的密钥 auth, err := sshClientConfig(host.Password, host.SSHKey) if err != nil { return nil, err } // 生成目标机器的ssh client配置 config := &ssh.ClientConfig{ User: host.Username, Auth: auth, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, Timeout: time.Second 10, } // 使用跳板机对目标机器进行拨号 conn, err := jClient.Dial(“tcp”, fmt.Sprintf(“%s:%d”, host.IP, host.SSHPort)) if err != nil { return nil, err } // 生成目标机器的 ssh client clientConn, channels, requests, err := ssh.NewClientConn(conn, fmt.Sprintf(“%s:%d”, host.IP, host.SSHPort), config) if err != nil { return nil, err } return ssh.NewClient(clientConn, channels, requests), nil }

  1. <a name="wQpml"></a>
  2. ### 1.2. 执行命令
  3. ```go
  4. func execCommand(client *ssh.Client, cmd ...string) (stdout, stderr string, err error) {
  5. session, err := client.NewSession() // 开启新的ssh会话
  6. if err != nil {
  7. return "", "", err
  8. }
  9. defer func() { _ = session.Close() }()
  10. // 指定标准输出和标准错误
  11. var stdOut bytes.Buffer
  12. var stdErr bytes.Buffer
  13. session.Stderr = &stdErr
  14. session.Stdout = &stdOut
  15. if err := session.Run(strings.Join(cmd, " && ")); err != nil {
  16. return stdOut.String(), stdErr.String(), err
  17. }
  18. return stdOut.String(), stdErr.String(), nil
  19. }
  1. // 之间连接对于主机,并执行命令
  2. func main() {
  3. node100 := &Host{
  4. IP: "10.4.7.100",
  5. SSHPort: 22,
  6. Username: "root",
  7. SSHKey: func() string {
  8. content, _ := ioutil.ReadFile("/root/.ssh/id_rsa")
  9. return string(content)
  10. }(),
  11. }
  12. client, err := opensshClient(node100)
  13. if err != nil {
  14. logger.Errorf("open ssh connect to %s failed, error:%s", node100.IP, err.Error())
  15. return
  16. }
  17. defer func() { _ = client.Close() }()
  18. stdout, stderr, err := execCommand(client, "echo $HOSTNAME", "df -h")
  19. if err != nil {
  20. logger.Errorf("%s run command echo $HOSTNAME failed, stdout:%s, stderr:%s, err:%s", node100.IP, stdout, stderr, err.Error())
  21. return
  22. }
  23. fmt.Print(stdout)
  24. fmt.Print(stderr)
  25. }
  1. [root@duduniao ssh]# go run command.go
  2. jumpserver-100
  3. Filesystem Size Used Avail Use% Mounted on
  4. udev 1.9G 0 1.9G 0% /dev
  5. tmpfs 393M 1.1M 392M 1% /run
  6. /dev/sda2 20G 7.4G 12G 40% /
  7. tmpfs 2.0G 0 2.0G 0% /dev/shm
  8. tmpfs 5.0M 0 5.0M 0% /run/lock
  9. tmpfs 2.0G 0 2.0G 0% /sys/fs/cgroup
  10. tmpfs 393M 0 393M 0% /run/user/0
  1. // 存在跳板机的情况
  2. func main() {
  3. node101 := &Host{
  4. IP: "10.4.7.101",
  5. SSHPort: 22,
  6. Username: "root",
  7. SSHKey: func() string {
  8. content, _ := ioutil.ReadFile("/root/.ssh/id_rsa")
  9. return string(content)
  10. }(),
  11. JumpServer: &JumpServer{
  12. IP: "10.4.7.100",
  13. SSHPort: 22,
  14. Username: "root",
  15. SSHKey: func() string {
  16. content, _ := ioutil.ReadFile("/root/.ssh/id_rsa")
  17. return string(content)
  18. }(),
  19. },
  20. }
  21. client, err := opensshClient(node101)
  22. if err != nil {
  23. logger.Errorf("open ssh connect to %s failed, error:%s", node101.IP, err.Error())
  24. return
  25. }
  26. defer func() { _ = client.Close() }()
  27. stdout, stderr, err := execCommand(client, "echo $HOSTNAME", "df -h")
  28. if err != nil {
  29. logger.Errorf("%s run command echo $HOSTNAME failed, stdout:%s, stderr:%s, err:%s", node101.IP, stdout, stderr, err.Error())
  30. return
  31. }
  32. fmt.Print(stdout)
  33. fmt.Print(stderr)
  34. }
  1. [root@duduniao ssh]# go run command.go
  2. worker-101
  3. Filesystem Size Used Avail Use% Mounted on
  4. udev 1.9G 0 1.9G 0% /dev
  5. tmpfs 393M 1.2M 392M 1% /run
  6. /dev/sda2 20G 7.4G 12G 40% /
  7. tmpfs 2.0G 0 2.0G 0% /dev/shm
  8. tmpfs 5.0M 0 5.0M 0% /run/lock
  9. tmpfs 2.0G 0 2.0G 0% /sys/fs/cgroup
  10. 10.4.7.100:/data/nfs 20G 7.4G 12G 40% /data/nfs
  11. tmpfs 393M 0 393M 0% /run/user/0

1.3. 转发文件

  1. // 发送文件
  2. func sendFiles(client *ssh.Client, remoteDir string, localFile string) error {
  3. // github.com/bramvdbogaerde/go-scp
  4. // 调用scp模块,创建scp的client
  5. scpClient, err := scp.NewClientBySSH(client)
  6. if err != nil {
  7. return err
  8. }
  9. defer func() { _ = scpClient.Close }()
  10. if err := scpClient.Connect(); err != nil {
  11. return err
  12. }
  13. file, err := os.Open(localFile)
  14. if err != nil {
  15. return err
  16. }
  17. // 这里存在几个问题:
  18. // 1. 文件权限必须要是字符串的数字格式。无法使用 file.stat 中的mode
  19. // 2. 同一个 scpClient 只能发送一次文件,发多个文件会出现: ssh: StdinPipe after process started
  20. // 3. 应该有其它的模块能改进避免上述的俩个文件
  21. if err := scpClient.CopyFile(file, path.Join(remoteDir, path.Base(localFile)), "0755"); err != nil {
  22. return err
  23. }
  24. _ = file.Close()
  25. return nil
  26. }
  1. func main() {
  2. node101 := &Host{
  3. IP: "10.4.7.101",
  4. SSHPort: 22,
  5. Username: "root",
  6. SSHKey: func() string {
  7. content, _ := ioutil.ReadFile("/root/.ssh/id_rsa")
  8. return string(content)
  9. }(),
  10. JumpServer: &JumpServer{
  11. IP: "10.4.7.100",
  12. SSHPort: 22,
  13. Username: "root",
  14. SSHKey: func() string {
  15. content, _ := ioutil.ReadFile("/root/.ssh/id_rsa")
  16. return string(content)
  17. }(),
  18. },
  19. }
  20. client, err := opensshClient(node101)
  21. if err != nil {
  22. logger.Errorf("open ssh connect to %s failed, error:%s", node101.IP, err.Error())
  23. return
  24. }
  25. defer func() { _ = client.Close() }()
  26. err = sendFiles(client, "/tmp", "/root/bin/scan_host.sh")
  27. if err != nil {
  28. logger.Errorf("%s send file failed ,err:%s", node101.IP, err.Error())
  29. }
  30. }

2. SSH作为代理转发

ssh 服务可以作为隧道进行请求的转发,比如目标机器上存在一个nginx服务器,对外暴露80端口,但是当前服务器与目标机器网络不通,此时可以通过中间的跳板机上ssh通道进行HTTP的请求转发!

  1. // 这里面涉及的channel让gc进行回收,手动关闭容易出现panic
  2. // 端口转发
  3. func forward(client *ssh.Client, protocol, localAddr, remoteAddr string, stop chan bool, errMsg chan error) {
  4. // 打开本地端口
  5. listener, err := net.Listen(protocol, localAddr)
  6. if err != nil {
  7. errMsg <- err
  8. return
  9. }
  10. defer func() { _ = listener.Close() }()
  11. // 定义异常退出机制,因为设置了 stop chan,为了避免在stop chan阻塞,引入err chan,两者满足其一就能退出
  12. var errChan = make(chan error)
  13. // 循环接收本地端口的请求
  14. go func() {
  15. for {
  16. localConn, err := listener.Accept()
  17. if localConn == nil {
  18. errMsg <- err
  19. return
  20. }
  21. if err != nil {
  22. _ = localConn.Close()
  23. errMsg <- err
  24. return
  25. }
  26. go establishLocal(client, protocol, remoteAddr, localConn, errChan)
  27. }
  28. }()
  29. select {
  30. case <-stop:
  31. errMsg <- nil
  32. case err := <-errChan:
  33. errMsg <- err
  34. }
  35. }
  36. // 处理本地端口的请求
  37. func establishLocal(client *ssh.Client, protocol, remoteAddr string, local net.Conn, errChan chan error) {
  38. // 打开远程的端口, 每次接收一个新的TCP连接,都得开一次远程转发
  39. remote, err := client.Dial(protocol, remoteAddr)
  40. if err != nil {
  41. errChan <- err
  42. return
  43. }
  44. defer func() { _ = remote.Close() }()
  45. errCh := make(chan error, 1)
  46. go exchangeData(local, remote, errCh)
  47. go exchangeData(remote, local, errCh)
  48. <-errCh
  49. <-errCh
  50. }
  51. type closeWriter interface {
  52. CloseWrite() error
  53. }
  54. // 数据交换
  55. func exchangeData(r io.Reader, w io.Writer, errCh chan error) {
  56. _, err := io.Copy(w, r)
  57. if tcpConn, ok := w.(closeWriter); ok {
  58. _ = tcpConn.CloseWrite() // 必须要关闭,否则内存泄露
  59. }
  60. errCh <- err
  61. }
  1. func main() {
  2. node101 := &Host{
  3. IP: "10.4.7.101",
  4. SSHPort: 22,
  5. Username: "root",
  6. SSHKey: func() string {
  7. content, _ := ioutil.ReadFile("/root/.ssh/id_rsa")
  8. return string(content)
  9. }(),
  10. JumpServer: &JumpServer{
  11. IP: "10.4.7.100",
  12. SSHPort: 22,
  13. Username: "root",
  14. SSHKey: func() string {
  15. content, _ := ioutil.ReadFile("/root/.ssh/id_rsa")
  16. return string(content)
  17. }(),
  18. },
  19. }
  20. client, err := opensshClient(node101)
  21. if err != nil {
  22. logger.Errorf("open ssh connect to %s failed, error:%s", node101.IP, err.Error())
  23. return
  24. }
  25. defer func() { _ = client.Close() }()
  26. stop := make(chan bool, 1)
  27. errMsg := make(chan error, 1)
  28. go forward(client, "tcp", "127.0.0.1:10080", "172.17.0.2:80", stop, errMsg)
  29. // 测试网络隧道是否就绪, 因为使用goroutine打开隧道,本地通道可能还能没有就绪
  30. for i := 0; i < 10; i++ {
  31. _, err = net.DialTimeout("tcp", "127.0.0.1:10080", time.Millisecond*100)
  32. if err == nil {
  33. break
  34. }
  35. }
  36. if err != nil {
  37. stop <- true
  38. return
  39. }
  40. // 测试
  41. for i := 0; i < 100000; i++ {
  42. httpClient := http.Client{Timeout: time.Second}
  43. resp, err := httpClient.Get("http://127.0.0.1:10080/info")
  44. if err != nil {
  45. logger.Errorf("send request failed, err:%s", err.Error())
  46. break
  47. }
  48. content, _ := ioutil.ReadAll(resp.Body)
  49. _ = resp.Body.Close()
  50. fmt.Print(string(content))
  51. time.Sleep(time.Millisecond)
  52. }
  53. stop <- true
  54. }