文章目录
- ssh原理
- 使用golang远程下发命令
- 使用golang远程传输文件
ssh原理
说到ssh原理个人觉得解释最全的一张图是这张华为画的
Connection establishment
这一步就是建立tcp连接
version negotiation
这一步是ssh客户端(连接者)和被ssh服务端(连接者)进行协议的交换,比如ssh客户端支持那些加密协议,协商出最后使用的协议,还有就是协商ssh版本
Key Exchange
在说Key Exchange之前,我们要知道主机有2类的ssh-key分别是hostkey和user ssh key,每类key都分公钥和私钥,公钥可以加密,私钥可以解密
-
hostkey:
-
user ssh key:
首先hostkey,当ssh server安装的时候这个hostkey就会默认生成,在第一次连接对端的时候会显示的提醒我们叫我们确认时候继续连接如下
The authenticity of host '192.168.152.132 (192.168.152.132)' can't be established.
ECDSA key fingerprint is SHA256:MzAmI+qRcIEb0AS+6XMcAH5gtxnB779KpHRa1vOvAMs.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
这是啥意思呢我们第一次连接ssh服务端,服务段会发送自己的hostkey的hash后的值给我们,我们如果选择yes,这段hash后的ssh服务端的hostkey被存入本地的.ssh/know_hosts
中这是为了防止中间人攻击,有了这个hostkey之后,会使用diffie-hellman,这个算法用于为2端同时生成session key,因为diffie-hellman算法的特新session key必定相等,后续session建立后都会用这个session key进行加密传输
Key Authentication
随后是user ssh key,这个东西是用户用ssh-keygen
生成的(也是ssh -i指定的identity_file]),当用户使用key认证而非密码认证的时候,这个就非常重要,ssh客户端将自己的public user ssh key发送给服务端(因为是session,用session可以进行加密),然后每当ssh客户端登录到服务端都会自己生成一个随机数用user 自己的userprivate key进行加密通过session传递给客户端,服务端再用接收客户端的公钥进行解密再发送回客户端,客户端进行check,是自己刚刚生成的key就发送验证成功的消息给服务端,最后验证通过,每当客户端向服务端进行数据传输都会使用之前的session key进行加密,服务段接收后用相同的session key进行解密
使用golang远程下发命令
package mainimport ("fmt""log""os""golang.org/x/crypto/ssh""golang.org/x/crypto/ssh/knownhosts"//"golang.org/x/cryto/ssh"
)func main() {//用于对方返回的keyhostkey, err := knownhosts.New("/root/.ssh/known_hosts")if err != nil {log.Fatal("get knowhosts file error: %s", err.Error())}identify_file := "/root/.ssh/ansible"addr := "192.168.152.132:22"key, err := os.ReadFile(identify_file)if err != nil {log.Fatal("error,Can not Read file " + identify_file + " : " + err.Error())}//将私钥用PEM方式加密返回成签名signer, err := ssh.ParsePrivateKey(key)if err != nil {log.Fatal("unbale to parseprivatekey: ", err)}//设置连接peer时候的configconfig := &ssh.ClientConfig{User: "root",Auth: []ssh.AuthMethod{ssh.PublicKeys(signer),},HostKeyCallback: hostkey,}//连接(key exchange,这里exhcange的是host key,用于认证session,而user key也就是自己手动生成的key用于验证用户)client, err := ssh.Dial("tcp", addr, config)if err != nil {log.Fatal("unable to connect: " + err.Error())}defer client.Close()session, err := client.NewSession()if err != nil {log.Fatal("new session error: %s", err.Error())}result, _ := session.Output("ip a")if err != nil {fmt.Fprintf(os.Stderr, "faile to run command, err:%s", err.Error())}fmt.Println(string(result))}
代码非常简单,注意我们如果是第一次连接那么上述代码会执行失败,因为第一次连接,ssh server要返回自己的hostkey给客户端(我们执行代码的机器),客户端要在自己的know_host里面check,但是第一次连接know_host没有数据就会失败,所以可以将代码改成不安全的模式,也就是不check know_host,方法也很简单,就是将连接时候的config的HostKeyCallback
对应的值改为ssh.InsecureIgnoreHostKey()
如下
package mainimport ("fmt""log""os""golang.org/x/crypto/ssh"//"golang.org/x/crypto/ssh/knownhosts"//"golang.org/x/cryto/ssh"
)func main() {//用于对方返回的key//hostkey, err := knownhosts.New("/root/.ssh/known_hosts")//if err != nil {// log.Fatal("get knowhosts file error: %s", err.Error())//}identify_file := "/root/.ssh/ansible"addr := "192.168.152.132:22"key, err := os.ReadFile(identify_file)if err != nil {log.Fatal("error,Can not Read file " + identify_file + " : " + err.Error())}//将私钥用PEM方式加密返回成签名signer, err := ssh.ParsePrivateKey(key)if err != nil {log.Fatal("unbale to parseprivatekey: ", err)}//设置连接peer时候的configconfig := &ssh.ClientConfig{User: "root",Auth: []ssh.AuthMethod{ssh.PublicKeys(signer),},HostKeyCallback: ssh.InsecureIgnoreHostKey(),}//连接(key exchange,这里exhcange的是host key,用于认证session,而user key也就是自己手动生成的key用于验证用户)client, err := ssh.Dial("tcp", addr, config)if err != nil {log.Fatal("unable to connect: " + err.Error())}defer client.Close()session, err := client.NewSession()if err != nil {log.Fatal("new session error: %s", err.Error())}result, _ := session.Output("ip a")if err != nil {fmt.Fprintf(os.Stderr, "faile to run command, err:%s", err.Error())}fmt.Println(string(result))}
最后打印出结果
root@master:~/demo/ssh# ./ssh
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet 127.0.0.1/8 scope host lovalid_lft forever preferred_lft foreverinet6 ::1/128 scope hostvalid_lft forever preferred_lft forever
2: ens32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:0c:29:7e:76:1b brd ff:ff:ff:ff:ff:ffaltname enp2s0inet 192.168.152.132/24 brd 192.168.152.255 scope global noprefixroute ens32valid_lft forever preferred_lft foreverinet6 fe80::20c:29ff:fe7e:761b/64 scope linkvalid_lft forever preferred_lft forever
使用golang远程传输文件
在代码之前,我们要了解2个小的知识点
scp -t
的用途- 文件形式
首先scp -t这个选项不管是man还是其他的公开官方资料中都很难找到其身影,我们先运行看是在干嘛
root@slaver1:/test# scp -t .
输入后就卡着不动,因为这个时候scp的程序在等待远端的scp程序通过tcp的22号端口向他发送文件…
所以scp -t是通过端口接收文件用的
在说一个文件的格式,一个文件开头第一行标记了文件的信息,比如权限,比如大小,比如文件名,后面的才是内容,最后以\x000
结尾,所以我们传递的时候最好使用管道,先传递文件原信息,再传递文件的内容,最后传递\x000
表示结尾,直接看代码
package main
import ("bytes""fmt""io""log""os""sync""golang.org/x/crypto/ssh"
)func main(){identify_file := "/root/.ssh/ansible"addr := "192.168.152.132:22"key, _ := os.ReadFile(identify_file)signer, _ := ssh.ParsePrivateKey(key) //用协商出来的动态session key进行加密config := &ssh.ClientConfig{User: "root",Auth: []ssh.AuthMethod{ssh.PublicKeys(signer),},HostKeyCallback: ssh.InsecureIgnoreHostKey(),}client, _ := ssh.Dial("tcp", addr, config)defer client.Close()session, _ := client.NewSession()defer session.Close()file, _ := os.Open("apply.sh")defer file.Close()stat, _ := file.Stat()wg := sync.WaitGroup{}wg.Add(1)go func() {hostIn, _ := session.StdinPipe()defer hostIn.Close()fmt.Fprintf(hostIn, "C0655 %d %s\n", stat.Size(), "apply.sh")//第一行输入文件元数据io.Copy(hostIn, file) //copy 文件内容fmt.Fprint(hostIn, "\x000") //表示结束wg.Done()}()session.Run("/usr/bin/scp -t /test")wg.Wait()}