SSH協(xié)議詳解 2021-08-24

SSH 協(xié)議架構(gòu)

ssh協(xié)議 =
The SSH Transport Layer Protocol [SSH-TRANS] + The User Authentication Protocol [SSH-USERAUTH] + The Connection Protocol [SSH-CONNECT]
rfc4251: The Secure Shell (SSH) Protocol Architecture

SSH protocol stack

ssh protocol stack:

SSH-CONNECT
SSH-USERAUTH
SSH-TRANS
TCP

SSH協(xié)議棧說(shuō)明,經(jīng)過(guò)服務(wù)器認(rèn)證和用戶認(rèn)證后,才會(huì)最后建立SSH-CONNECTION

  • SSH傳輸層協(xié)議SSH Transport Layer Protocol:它負(fù)責(zé)認(rèn)證服務(wù)器,加密數(shù)據(jù),確保數(shù)據(jù)完整性, 雖然它運(yùn)行在TCP之上,但其實(shí)它可以運(yùn)行在任意可靠的數(shù)據(jù)流之上;
  • SSH用戶認(rèn)證協(xié)議SSH User Authentication Protocol:它負(fù)責(zé)認(rèn)證使用者是否是ssh服務(wù)器的用戶, Public Key Authentication登陸ssh就將在這一層實(shí)現(xiàn);
  • SSH連接協(xié)議SSH Connection Protocol:它把多路(Multiplex)加密的通道轉(zhuǎn)換成邏輯上的Channel

SSH-CONNECT protocol

rfc4254: SSH-CONNECT protocol
The Connection Protocol [SSH-CONNECT] multiplexes the encrypted tunnel into several logical channels.

[channel] is the key word of SSH-CONNECT protocol

channel

channels can be used for a wide range of purposes, such as providing interactive login sessions, remote execution of commands, forwarded TCP/IP connections, and forwarded X11 connections.

All terminal sessions, forwarded connections, etc., are channels.
Either side may open a channel.
Multiple channels are multiplexed into a single connection. 所有channel底層都復(fù)用同一條鏈接隧道

channel type

Each channel has a type.
Usually, we will use “session” channels, but there are also “x11” channels, “forwarded-tcpip” channels, and “direct-tcpip” channels, etc.

Channel type                  Reference
         ------------                  ---------
         session                       [SSH-CONNECT, Section 6.1]
         x11                           [SSH-CONNECT, Section 6.3.2]
         forwarded-tcpip               [SSH-CONNECT, Section 7.2]
         direct-tcpip                  [SSH-CONNECT, Section 7.2]

direct-tcpip is used for “client-to-server forwarded connections” and forwarded-tcpip is used for “server-to-client forwarded connections”

Interactive Sessions

A session is a remote execution of a program, in which server and client communicate by “session” channels.
The program may be a shell, an application, a system command, or some built-in subsystem.
It may or may not have a tty, and may or may not involve X11 forwarding.
Multiple sessions can be active simultaneously.

Opening a Session

A session is started by sending the following message.

      byte      SSH_MSG_CHANNEL_OPEN
      string    "session"
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size

(Client implementations SHOULD reject any session channel open requests to make it more difficult for a corrupt server to attack the client. Client應(yīng)該拒絕任何session channel open requests,以防server可能的攻擊)

as a client implementation example, golang.org/x/crypto/ssh use *ssh.Client.NewSession to open a "session" channel:

func (c *Client) NewSession() (*Session, error) {
    // "session" 指定了 channel 的類型
    ch, in, err := c.OpenChannel("session", nil)
    if err != nil {
        return nil, err
    }
    return newSession(ch, in)
}

Channel-Specific Requests

基于某個(gè)特定的channel,通信雙方通過(guò)發(fā)送和接收Channel-Specific Requests完成信息交互

Many 'channel type' values have extensions that are specific to that particular 'channel type'. An example is requesting a pty (pseudo terminal) for an interactive session.不同類型的channel支持不同的request type

All channel-specific requests use the following format.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    request type in US-ASCII characters only (such as "pty-req" for "session" type channel)
      boolean   want reply
      ....      type-specific data follows

例如,"session" channel 支持以下的request type

Request type                  Reference
         ------------                  ---------
         pty-req                       [SSH-CONNECT, Section 6.2]
         x11-req                       [SSH-CONNECT, Section 6.3.1]
         env                           [SSH-CONNECT, Section 6.4]
         shell                         [SSH-CONNECT, Section 6.5]
         exec                          [SSH-CONNECT, Section 6.5]
         subsystem                     [SSH-CONNECT, Section 6.5]
         window-change                 [SSH-CONNECT, Section 6.7]
         xon-xoff                      [SSH-CONNECT, Section 6.8]
         signal                        [SSH-CONNECT, Section 6.9]
         exit-status                   [SSH-CONNECT, Section 6.10]
         exit-signal                   [SSH-CONNECT, Section 6.10]
Starting a Shell or a Command

Once the session has been set up, a program is started at the remote
end. The program can be a shell, an application program, or a
subsystem with a host-independent name. Only one of these requests
can succeed per channel(三選一)
.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "shell"
      boolean   want reply

This message will request that the user's default shell (typically
defined in /etc/passwd in UNIX systems) be started at the other end.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "exec"
      boolean   want reply
      string    command

This message will request that the server start the execution of the
given command. The 'command' string may contain a path. Normal
precautions MUST be taken to prevent the execution of unauthorized
commands.

      byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "subsystem"
      boolean   want reply
      string    subsystem name

This last form executes a predefined subsystem. It is expected that
these will include a general file transfer mechanism, and possibly
other features.

global request

There are several kinds of requests that affect the state of the remote end globally, independent of any channels. A global request example is a request to start TCP/IP forwarding for a specific port.

global request name                  Reference
         ------------                  ---------
         tcpip-forward                 [SSH-CONNECT, Section 7.1]
         cancel-tcpip-forward          [SSH-CONNECT, Section 7.1]

Note that both the client and server MAY send global requests at any time, and the receiver MUST respond appropriately. All such requests use the following format.

      byte      SSH_MSG_GLOBAL_REQUEST
      string    request name in US-ASCII only
      boolean   want reply
      ....      request-specific data follows

The recipient will respond to this message with
SSH_MSG_REQUEST_SUCCESS or SSH_MSG_REQUEST_FAILURE if 'want reply' is
TRUE.

      byte      SSH_MSG_REQUEST_SUCCESS
      ....     response specific data

Usually, the 'response specific data' is non-existent.

Why am I not disconnected when I restart the sshd server on the server I'm connected to

See below we have sshd on process 14688 started by systemd (process 1). We also have one session, 14701 who was started by sshd:

[root@rh7test ~]# ps -aef | grep ssh
root    14688     1  0 04:46 ?        00:00:00 /usr/sbin/sshd -D
root     14701 14688  0 04:46 ?        00:00:00 sshd: root@pts/2

Then we retsart sshd and it gets a new PID (14805). Notice the old ssh process is still running on 14701 but it was disconnected from the (terminated) sshd and now the owner is process 1(這里是關(guān)鍵).

[root@rh7test ~]# systemctl restart sshd.service
[root@rh7test ~]# ps -aef | grep ssh
root     14701     1  0 04:46 ?        00:00:00 sshd: root@pts/2
root     14805     1  0 04:48 ?        00:00:00 /usr/sbin/sshd -D

Then we connect a new session and can see that the new one is owned by the restarted sshd (14805).

[root@rh7test ~]# ps -aef | grep ssh
root     14701     1  0 04:46 ?        00:00:00 sshd: root@pts/2
root     14805     1  0 04:48 ?        00:00:00 /usr/sbin/sshd -D
root     14811 14805  0 04:48 ?        00:00:00 sshd: root@pts/0

sshd listens for connections from clients. It forks a new daemon for each incoming connection.
The forked daemons handle key exchange, encryption, authentication, command execution, and data exchange.
This child process will not die if SSHD is restarted, but will be owned by process 1.
So your sshd listens for connections but then forks a new process when a user connects to the server.
When you restart sshd you are only restarting the process that listens for new connections. All existing connections stay intact.

https://unix.stackexchange.com/questions/615403/why-am-i-not-disconnected-when-i-restart-the-sshd-server-on-the-server-im-conne

而當(dāng)network restart的時(shí)候,ssh當(dāng)前的連接也不會(huì)斷開。這是因?yàn)?,network restart是非常快速的,restart從開始到結(jié)束這個(gè)時(shí)間段內(nèi),socket在server內(nèi)存中對(duì)應(yīng)的結(jié)構(gòu)體仍然valid,也就是說(shuō)ssh連接依賴的底層tcp連接未斷開,tcp連接根本感知不到network restart。那么,上層的ssh連接仍然保持是非常自然的。注意區(qū)分network restart vs sshd restart時(shí),ssh連接不斷的原因

SSH authentication with SSH keys

Abstract:
The idea is to have a cryptographic key pair - public key and private key - and configure the public key on a server to authorize access and grant anyone who has a copy of the private key access to the server.
Host keys authenticate hosts. Authorized keys and identity keys authenticate users.

使用ssh key實(shí)現(xiàn)遠(yuǎn)程免密登錄涉及以下步驟:

  • 公私鑰生成與公鑰部署ssh client 生成公私鑰,生成公私鑰使用ssh-keygen,例如ssh-keygen -t rsa -C "costumed_comment(such as your email address)"
    然后把公鑰的內(nèi)容追加到 ssh server指定用戶的~/.ssh/authorized_keys。
    The public key can then be installed as an authorized key on a server using the ssh-copy-id
    如將公鑰內(nèi)容追加到root用戶的/root/.ssh/authorized_keys,表示該公鑰已被root用戶認(rèn)證可信
  • ssh client配置~/.ssh/config文件(可選,推薦)。配置~/.ssh/config可以針對(duì)不同的remote目標(biāo)機(jī)器設(shè)置不同的登錄配置,如配置hostname(真實(shí)地址)、IP、端口、指定登錄方式(密碼\ssh key\etc)和私鑰文件等
    eg.配置如下config
# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host devmachine
    HostName 9.9.9.9
    Port 99999
    User root
    PreferredAuthentications publickey  
    IdentityFile /Users/dev/.ssh/id_rsa_devmachine  # 指定私鑰文件

配置完成后,ssh root@devmachine甚至直接ssh devmachine就能夠登錄上9.9.9.9。因?yàn)檫@個(gè)命令依據(jù)devmachine這個(gè)Host讀取config內(nèi)的管理配置,無(wú)需再聲明IP、端口、私鑰和登錄用戶等參數(shù)
ssh的config文件可以存在多個(gè),vscode安裝了remote-ssh插件后可以加載ssh的config文件,實(shí)現(xiàn)ssh到開發(fā)機(jī)做遠(yuǎn)程開發(fā)

如果不配置~/.ssh/config,也可以通過(guò)指定ssh參數(shù)的形式實(shí)現(xiàn)登錄

指定IP、端口和私鑰可以登錄:
ssh -i /Users/dev/.ssh/id_rsa_devmachine root@9.9.9.9 -p 99999
通過(guò)輸入密碼登錄:
ssh root@9.134.165.116 -p 36000

一點(diǎn)踩坑記錄:
部分軟件(如weterm、某些版本的vscode)的ssh功能在使用本地私鑰時(shí)可能會(huì)報(bào)
Error: Error while signing data with privateKey: error:06000066:public key routines:OPENSSL_internal:DECODE_ERROR
解決方式-網(wǎng)上的辦法是轉(zhuǎn)格式
To fix this error, we can convert the private key file from OpenSSH private key format to PEM format.
ssh-keygen -p -m PEM -f target_id_rsa

配置github等代碼托管平臺(tái)使用ssh key免密登錄,具體可以參考https://docs.github.com/en/authentication/connecting-to-github-with-ssh


ssh協(xié)議認(rèn)證過(guò)程

參考:https://xdev.in/posts/understanding-of-ssh/
簡(jiǎn)單來(lái)說(shuō)概括為以下兩步:

  • ssh client驗(yàn)證 ssh server身份。 ssh client通過(guò)ssh命令發(fā)起連接請(qǐng)求, ssh server收到后返回自己的公鑰指紋(RSA key fingerprint)。如果ssh clientssh server是首次建立連接,ssh client會(huì)收到提示如:
    The authenticity of host 'ssh-server.example.com (12.18.429.21)' can't be established.
    RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.
    Are you sure you want to continue connecting (yes/no)?
    
    ssh client選擇是否信任 ssh server,如果信任則會(huì)將 ssh server的公鑰rsa-key寫入本地的~/.ssh/known_hosts。后續(xù)ssh client再與 ssh server進(jìn)行連接,則會(huì)對(duì)比連接時(shí) ssh server發(fā)來(lái)的公鑰與known_hosts中的是否一致。此時(shí),服務(wù)端擁有客戶端的公鑰和本機(jī)的私鑰,客戶端擁有服務(wù)端的公鑰和本機(jī)的私鑰
  • ssh server驗(yàn)證 ssh client身份ssh server利用ssh client連接時(shí)指定的公鑰加密一個(gè)隨機(jī)數(shù)并協(xié)商會(huì)話密鑰sessionkey,如果ssh client用私鑰順利解出隨機(jī)數(shù)并發(fā)回ssh server,那么ssh server驗(yàn)證 ssh client身份完畢
  • 雙向認(rèn)證完畢,避免中間人(man-in-the-middle attack)攻擊。雙方使用sessionkey對(duì)data進(jìn)行對(duì)稱加密后傳輸

如果配置好公私鑰后,用戶仍然需要使用賬號(hào)密碼登錄,檢查

    1. 檢查ssh server端的/etc/ssh/sshd_config是否允許公私鑰登錄

    RSAAuthentication yes
    PubkeyAuthentication yes
    AuthorizedKeysFile .ssh/authorized_keys

然后重啟sshd服務(wù)
service sshd restart

    1. 檢查ssh server端的用戶家目錄下~.ssh是否可讀寫

    ls -l ~/.ssh/
    total 20
    -rw-rw-r-- 1 xiu xiu 576 Oct 18 19:25 authorized_keys
    -rw------- 1 xiu xiu 90 Oct 18 18:07 config
    -rw------- 1 xiu xiu 1675 Oct 18 18:07 id_rsa
    -rw-r--r-- 1 xiu xiu 404 Oct 18 18:07 id_rsa.pub
    -rw-r--r-- 1 xiu xiu 409 Oct 18 19:15 known_hosts

    1. 開啟sshd調(diào)試模式進(jìn)行進(jìn)一步調(diào)試可以參考:
      https://blog.51cto.com/zouqingyun/1874410
  • 關(guān)于ssh client的配置信息

    The ssh program on a host receives its configuration from either the command line or from configuration files ~/.ssh/config and /etc/ssh/ssh_config.

    Command-line options take precedence over configuration files. The user-specific configuration file ~/.ssh/config is used next. Finally, the global /etc/ssh/ssh_config file is used. The first obtained value for each configuration parameter will be used.

  • 忽略known_hosts file的公鑰驗(yàn)證

You can use the following command to ignore known hosts when using ssh command line:

ssh -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" user@host

usage in scp is the same

scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" somefile.txt sarav@mwinventory.in :/var/tmp/

This command will skip host key checking by sending the key to a null known_hosts file.

The ssh known_hosts file is a file that stores the public key of all of the servers that you have connected using ssh.
利用option UserKnownHostsFile指定本次ssh連接要使用的 known_hosts file. 遠(yuǎn)端機(jī)器校驗(yàn)通過(guò)/指定無(wú)需校驗(yàn)遠(yuǎn)端 后,ssh會(huì)將遠(yuǎn)端機(jī)器的 public key 存入 known_hosts file. 如果 known_hosts file中 已有該遠(yuǎn)端機(jī)器的 public key,那什么也不做;如果發(fā)現(xiàn)該遠(yuǎn)端機(jī)器的 public key 和 known_hosts file 已有的不同,追加更新而不是覆蓋更新known_hosts file

-o "StrictHostKeyChecking=no"表示不作遠(yuǎn)端機(jī)器的身份校驗(yàn),"UserKnownHostsFile=/dev/null"表示將遠(yuǎn)端機(jī)器的public key寫入本地/dev/null,也就是丟棄

  • SSH tunneling(also called SSH port forwarding,中文【端口轉(zhuǎn)發(fā)】)

    包含3種端口轉(zhuǎn)發(fā),分別是Local Forwarding、Remote Forwarding和Dynamic Forwarding

    • Local Forwarding(本地端口轉(zhuǎn)發(fā)到遠(yuǎn)端端口)

      In OpenSSH, local port forwarding is configured using the -L option:

      ssh -L local-port:target-host:target-port intermediate-host
      

      Eg. 設(shè)定localhost與gateway.example.com連通;而intra.example.com部署在gateway.example.com后的內(nèi)網(wǎng),與localhost不能互通

      ssh -L 80:intra.example.com:80 gateway.example.com
      

      This example opens a connection to the gateway.example.com jump server, and forwards any connection to port 80 on the local machine to the configured port 80 on intra.example.com.

      上面的命令讓本地的ssh client監(jiān)聽本地80端口,任何發(fā)往本地80端口的請(qǐng)求都會(huì)redirect到ssh client,然后發(fā)往ssh server即gateway.example.com,隨后server將直接deliver數(shù)據(jù)到intra.example.com的80端口,完成響應(yīng)后resp流量按原路返回

      最終的效果是,請(qǐng)求http://localhost:80,實(shí)際上是請(qǐng)求了被gateway.example.com隔離的內(nèi)網(wǎng)機(jī)器intra.example.com上部署的http://intra.example.com:80服務(wù)。這樣一來(lái),原本intra.example.com不能直接被localhost訪問(wèn),但localhost通過(guò)ssh端口轉(zhuǎn)發(fā),把自己的80端口請(qǐng)求通過(guò)ssh連接轉(zhuǎn)發(fā)到gateway.example.com:22然后再轉(zhuǎn)發(fā)給intra.example.com,實(shí)現(xiàn)了外網(wǎng)對(duì)內(nèi)網(wǎng)資源的訪問(wèn)

    tunnel http traffic via ssh local forward

    9.134.165.110:8080起了一個(gè)http服務(wù),該http服務(wù)可以通過(guò)intermedia_server訪問(wèn),而無(wú)法通過(guò)localhost訪問(wèn)
    使用ssh -N -L 8888:9.134.165.110:8080 intermedia_server -p 22 -l root做本地端口轉(zhuǎn)發(fā)
    訪問(wèn)本地的8888端口就等于訪問(wèn)9.134.165.110的8080端口,http://localhost:8888 works

    tunnel https traffic via ssh local forward

    myip.ipip.net:443是我們的目標(biāo)訪問(wèn)端口
    curl https://myip.ipip.net/會(huì)返回ip出口詳情,如當(dāng)前 IP:14.xx.xx.xx 來(lái)自于:中國(guó) 廣東 廣州 電信

    仿照tunnel http traffic,我們嘗試ssh -N -L 8888:myip.ipip.net:443 intermedia_server -p 22 -l root,發(fā)現(xiàn)https://localhost:8888并不正常工作

    HTTPS connection can be redirected via SSH port forwarding - however the SSL/TLS certificate validation will fail in such cases as the host name does not match:
    we are connecting to https://localhost:8888 but the server certificate contains the name myip.ipip.net
    我們使用https協(xié)議訪問(wèn)localhost:8888,目標(biāo)訪問(wèn)站點(diǎn)是localhost。流量被forward到綁定了myip.ipip.net的server。在SSL/TLS握手階段,目標(biāo)訪問(wèn)站點(diǎn)localhost與server namemyip.ipip.net并不匹配,于是SSL/TLS握手失敗

    解決方案:
    server namemyip.ipip.net我們改不了,但我們可以改localhost name,把localhost name改成server name,SSL/TLS握手就能夠正常: vim /etc/hosts -> 127.0.0.1 myip.ipip.net
    這時(shí),訪問(wèn)https://myip.ipip.net:8888就是訪問(wèn)本地的8888端口,然后流量順利通過(guò)intermedia_server被forward到真正的myip.ipip.net:443

    注意:https://myip.ipip.net:8888返回的IP信息,實(shí)際上是intermedia_server請(qǐng)求myip.ipip.net:443的返回,因?yàn)閕ntermedia_server才是真正的請(qǐng)求方

    refer to https://superuser.com/questions/347415/is-it-possible-to-tunnel-https-traffic-via-ssh-tunnel-with-standard-ssh-programs

  • Remote Forwarding(遠(yuǎn)端端口轉(zhuǎn)發(fā)到本地端口)

    In OpenSSH, remote SSH port forwardings are specified using the -R option. For example:

    ssh -R remote-port:target-host:target-port -N remotehost
    

    Eg.

    ssh -R 8080:localhost:80 public.example.com
    

    This allows anyone on the remote server to connect to TCP port 8080 on the remote server. The connection will then be tunneled back to the client host, and the client then makes a TCP connection to port 80 on localhost. Any other host name or IP address could be used instead of localhost to specify the host to connect to.

    與local forwarding相反,建立本地計(jì)算機(jī)到遠(yuǎn)程計(jì)算機(jī)的 SSH 隧道以后,本地轉(zhuǎn)發(fā)是通過(guò)本地計(jì)算機(jī)訪問(wèn)遠(yuǎn)程計(jì)算機(jī),而遠(yuǎn)程轉(zhuǎn)發(fā)則是通過(guò)遠(yuǎn)程計(jì)算機(jī)訪問(wèn)本地計(jì)算機(jī)

    上面的命令建立了遠(yuǎn)端public.example.com到本地的ssh隧道,訪問(wèn)遠(yuǎn)程主機(jī)public.example.com:8080的請(qǐng)求,會(huì)被public.example.com經(jīng)過(guò)ssh端口轉(zhuǎn)發(fā)到本機(jī),然后再轉(zhuǎn)發(fā)到localhost:80

  • Dynamic Forwarding(動(dòng)態(tài)端口轉(zhuǎn)發(fā))

    動(dòng)態(tài)轉(zhuǎn)發(fā)把本地端口綁定到 SSH 服務(wù)器就到此為止。至于 SSH 服務(wù)器要去訪問(wèn)哪一個(gè)網(wǎng)站,完全是動(dòng)態(tài)的,取決于原始通信

    ssh -D local-port tunnel-host -N
    

    注意,這種轉(zhuǎn)發(fā)采用了 SOCKS5 協(xié)議。訪問(wèn)外部網(wǎng)站時(shí),需要把 HTTP 請(qǐng)求轉(zhuǎn)成 SOCKS5 協(xié)議,才能把本地端口的請(qǐng)求轉(zhuǎn)發(fā)出去

    SOCKS(SOCKet Secure),是一種會(huì)話層協(xié)議,主要用于客戶端與外網(wǎng)服務(wù)器之間通訊的中間傳遞。當(dāng)防火墻后的客戶端要訪問(wèn)外部的服務(wù)器時(shí),就跟SOCKS代理服務(wù)器連接。這個(gè)代理服務(wù)器控制客戶端訪問(wèn)外網(wǎng)的資格,允許的話,就將客戶端的請(qǐng)求發(fā)往外部的服務(wù)器

note: SSH端口轉(zhuǎn)發(fā)完全基于基本的SSH連接,因此,凡是可以切斷SSH連接的方式都可以終止端口轉(zhuǎn)發(fā),包括但不限于通過(guò)在遠(yuǎn)程終端上執(zhí)行exit命令、暴力關(guān)閉本地終端窗口、遠(yuǎn)程主機(jī)關(guān)機(jī)、本地主機(jī)關(guān)機(jī)等

golang ssh client 實(shí)踐

package cmdchannel

import (
    "bytes"
    "fmt"
    "strings"
    "testing"
    "time"

    "golang.org/x/crypto/ssh"
)

func TestSSH(t *testing.T) {
    cmd := "ls /"

    // ssh config, use password here
    addr := "xxx.xxx.xxx.xxx:22"
    sshCFG := &ssh.ClientConfig{
        Config: ssh.Config{},
        User:   "root",
        Auth: []ssh.AuthMethod{
            ssh.Password("passwd"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         30 * time.Second,
    }

    // ssh client
    client, err := ssh.Dial("tcp", addr, sshCFG)
    if err != nil {
        err = fmt.Errorf("failed to get ssh client. %w", err)
        fmt.Println(err)
        return
    }

    // open a "session" channel, aka the session in rfc4254
    session, err := client.NewSession()
    if err != nil {
        fmt.Println(err)
        return
    }
    defer session.Close()

    var output bytes.Buffer
    session.Stdout = &output

    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 {
        fmt.Println(err)
        return
    }

    err = session.Run(cmd)
    if err != nil {
        fmt.Println(err)
        return
    }
    result := output.String()
    fmt.Println("output is:", result)
}

func TestSSH1(t *testing.T) {
    cmd := "ls /; exit 0" // must exit

    // ssh config, use password here
    addr := "xxx.xxx.xxx.xxx:22"
    sshCFG := &ssh.ClientConfig{
        Config: ssh.Config{},
        User:   "root",
        Auth: []ssh.AuthMethod{
            ssh.Password("passwd"),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Timeout:         30 * time.Second,
    }

    // ssh client
    client, err := ssh.Dial("tcp", addr, sshCFG)
    if err != nil {
        err = fmt.Errorf("failed to get ssh client. %w", err)
        fmt.Println(err)
        return
    }

    session, err := client.NewSession()
    if err != nil {
        fmt.Println(err)
        return
    }
    defer session.Close()

    cmdlist := strings.Split(cmd, ";")
    stdinBuf, err := session.StdinPipe()
    if err != nil {
        t.Error(err)
        fmt.Println(err)
        return
    }

    var outbt, errbt bytes.Buffer
    session.Stdout = &outbt

    session.Stderr = &errbt

    // get a pty for the session
    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 {
        fmt.Println("pty err:", err)
        return
    }

    // run an interactive shell. 
    err = session.Shell()
    if err != nil {
        fmt.Println(err)
        return
    }

    start := time.Now()
    for _, c := range cmdlist {
        // 在shell里面,\n代表enter鍵換行,即執(zhí)行當(dāng)前命令c
        c = c + "\n"
        stdinBuf.Write([]byte(c))
    }
    session.Wait()

    fmt.Println("output is ", outbt.String())
    fmt.Println("err is ", errbt.String())
}

python ssh client 實(shí)踐

Here is an example of how to use Python to connect to a remote server via an intermediate server using SSH:

import paramiko

# Define the intermediate server (jumper) details
jumper = paramiko.SSHClient()
jumper.set_missing_host_key_policy(paramiko.AutoAddPolicy())
jumper.connect('jumper.example.com', username='jumper_user', password='jumper_password')

# Define the remote server details
remote = paramiko.SSHClient()
remote.set_missing_host_key_policy(paramiko.AutoAddPolicy())
remote.connect('remote.example.com', username='remote_user', password='remote_password', sock=jumper.get_transport().open_channel('direct-tcpip', ('remote.example.com', 22), ('localhost', 10022)))

# Run a command on the remote server
stdin, stdout, stderr = remote.exec_command('ls -l')
print(stdout.read().decode())

This code uses the paramiko library to connect to the intermediate server (jumper) and then to the remote server. The sock parameter is used to specify the channel to the remote server via the intermediate server. The exec_command method is used to run a command on the remote server.


更多可以參考 https://github.com/wangdoc/ssh-tutorial

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容