1. 程式人生 > 其它 >go 複用ssh 中的session_重新認識SSH(二)

go 複用ssh 中的session_重新認識SSH(二)

技術標籤:go 複用ssh 中的session

0f875e77832577f2dc321243fd2d84ff.png

專欄排版存在些許問題,歡迎訪問我的部落格:emous.github.io 訪問更多博文。

在認證完畢後,客戶端和服務端之間將使用SSH連線協議進行實際的任務操作,包括開啟互動式的登入會話遠端命令呼叫TCP轉發X11轉發等。在傳輸層協議之上,啟用連線協議的方式就是請求一個service namessh-connection服務。


Channel機制

連線協議裡的每個實際應用都是Channel,各方都有可能開啟Channel,大量的Channel複用同一個Connection(我認為這裡指的Connection應該是上文說的ssh-connection service)。一個Channel被雙方用自己的數字標識,所以每端不同的數字可能指向的並不是相同的Channel。其他任何和Channel相關的訊息都會包含對端的Channel標識。

sender:number1    ->    ()=========================()    <-    number2:receiver

Channel是被流控的,在被告知視窗可用之前沒有資料可以在Channel裡傳輸。

(新建)開啟一個Channel

當任意一端想要新建一個Channel時,它首先要給Channel分配一個本端的數字標識。然後將下面的訊息傳送給對端,這個訊息包括了本(傳送)端標識、初始化視窗大小等。

byte      SSH_MSG_CHANNEL_OPEN
      string    channel type in US-ASCII only
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size
      ....      channel type specific data follows

channel type是一個名字,符合SSH rfc命名規範,註冊命名(名稱)、擴充套件命名(名稱@域名);sender channel是本地的標識;initial window size則明確了在不調整視窗大小的情況下,對方一共可以傳送多少位元組的資料給(這個訊息的)傳送者maximum packet size表示對方發給 (這個訊息的)傳送者 的單次的packet的最大值是多少。

為什麼要設定window size和maximum packet size,我的理解是,有一些老舊的慢速裝置IO頻寬很低,所以如果大量的資料湧進來會導致緩衝區溢位。

對端收到訊息後需要作出決定是否同意開啟一個Channel,使用SSH_MSG_CHANNEL_OPEN_CONFIRMATION或SSH_MSG_CHANNEL_OPEN_FAILURE響應訊息。

byte      SSH_MSG_CHANNEL_OPEN_CONFIRMATION
      uint32    recipient channel
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size
      ....      channel type specific data follows

其中recipient channel請求開啟Channel端的本地Channel標識,sender channel則為當前訊息傳送方的本地Channel標識,其他的資料都是描述當前訊息傳送方的。或者傳送開啟失敗的訊息SSH_MSG_CHANNEL_OPEN_FAILURE。

byte      SSH_MSG_CHANNEL_OPEN_FAILURE
      uint32    recipient channel
      uint32    reason code
      string    description in ISO-10646 UTF-8 encoding [RFC3629]
      string    language tag [RFC3066]

比如被請求開啟Channel的一方不支援標註的channel type,那它將簡單地迴應SSH_MSG_CHANNEL_OPEN_FAILURE。請求方則或許需要顯示description給使用者。下面是一些預定義的錯誤碼:

SSH_OPEN_ADMINISTRATIVELY_PROHIBITED          1
            SSH_OPEN_CONNECT_FAILED                       2
            SSH_OPEN_UNKNOWN_CHANNEL_TYPE                 3
            SSH_OPEN_RESOURCE_SHORTAGE                    4

錯誤碼0x00000005 - 0xFDFFFFFF將按照IETF CONSENSUS的方式分配,0xFE000000 - 0xFFFFFFFF則留給個人使用。雖然IANA沒有關於0xFE000000 - 0xFFFFFFFF的控制權,但是還是將他約定成2部分使用:

  • 0xFE000000 - 0xFEFFFFFF被用在本地分配的Channel上,比如channel type為”[email protected]”(帶有@符號)的Channel開啟失敗,那麼錯誤碼應該使用由IANA分配的部分( 0x00000001 - 0xFDFFFFFF)或者本地分配相關的部分(0xFE000000 - 0xFEFFFFFF)。
    比如伺服器不認識這個channel type,哪怕這個type是本地定義(包含@)的,也必須使用0x00000003錯誤碼。然而如果,伺服器認識這個錯誤碼但是無法開啟,則應該使用0xFE000000 - 0xFEFFFFFF其中的一個錯誤碼。總的來說,參與者應該首先嚐試使用IANA分配的錯誤碼,然後在使用它們自定義的原因。
  • 對於從0xFF開始的部分,不做限制或建議。在這個範圍內的每一個值,都不被期望有任何實際操作互動性,本質上說它們是為實驗目的而預留的。

傳輸資料

上文描述了視窗大小可以用來限制另一方傳送的資料量,同時協議規定雙方都可以通過下面的訊息對視窗作出調整。

byte      SSH_MSG_CHANNEL_WINDOW_ADJUST
      uint32    recipient channel
      uint32    bytes to add

接收方接收到這個訊息以後,接收方可以根據這個給定的數增加視窗的大小,不論如何視窗最大為2^32-1位元組。具體的資料,則通過下面的訊息傳送。

byte      SSH_MSG_CHANNEL_DATA
      uint32    recipient channel
      string    data

單次可以傳送資料的最大量取決於對方當前視窗尺寸對方允許接受的最大packet值的最小值。對方每接受一個訊息,視窗都會相應減少。規範期望實現可以對傳輸層packet size做出限制(任何關於接受資料的限制必須大於等於32768位元組)。所以在連線層協議:

  • 不準將可接受的maximum packet size設定成大於傳輸層能接受的最大值。
  • 不準生成超過傳輸層能傳送的最大值,哪怕對方的連線協議能接受這麼大的packet。

同時,協議提供了一些傳遞額外資料(比如stderr資料)的方法。

byte      SSH_MSG_CHANNEL_EXTENDED_DATA
      uint32    recipient channel
      uint32    data_type_code
      string    data

目前標準定義的data_type_code型別有:

SSH_EXTENDED_DATA_STDERR               1

同時這個data_type_code的值的分發規定也與上文的channel type類似,分為IANA部分和私人使用部分。

關閉Channel

當任意一方不在往Channel傳送更多資料的時候,它應該傳送一個SSH_MSG_CHANNEL_EOF訊息。

byte      SSH_MSG_CHANNEL_EOF
      uint32    recipient channel

這個訊息不會有明確的響應,但是它依舊應該被髮送給對方不論對方是誰。需要注意的是,傳送完這個訊息後,Channel依舊是開啟著的(只不過自己這一邊不再發資料了),從另一個方向上還是可能過來更多的資料。這個訊息並不會消耗視窗大小,即使視窗已經不可以。

當任意一方希望結束Channel時,則應該傳送SSH_MSG_CHANNEL_CLOSE。另一方必須也傳送SSH_MSG_CHANNEL_CLOSE,除非它已經發送過SSH_MSG_CHANNEL_CLOSE(網路延遲)了。當一方既傳送了又接收到SSH_MSG_CHANNEL_CLOSE訊息,Channel就被關閉了,相關的資源可以被清理,本地的Channel number可以在下次開啟Channel的時候重用。任意一方都可以直接傳送SSH_MSG_CHANNEL_CLOSE而不需要與現傳送SSH_MSG_CHANNEL_EOF。

byte      SSH_MSG_CHANNEL_CLOSE
      uint32    recipient channel

同樣的,這個訊息不需要消耗視窗大小。

明確Channel資訊請求

許多的channel type包含關於該channel type的更詳細的擴充設定。比如說,為一個互動session請求一個虛擬終端。所有的明確Channel資訊請求都是如下格式。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    request type in US-ASCII characters only
      boolean   want reply
      ....      type-specific data follows

如果want reply被設定成FALSE,不會有響應被回覆給請求端。否則,響應可能包括SSH_MSG_CHANNEL_SUCCESS、SSH_MSG_CHANNEL_FAILURE或者要求繼續提供資訊的訊息。如果接收端不認識或不支援這個擴充的明細,則返回SSH_MSG_CHANNEL_FAILURE。

這個訊息也不消耗視窗大小,request type是自定義的。

byte      SSH_MSG_CHANNEL_SUCCESS
      uint32    recipient channel

      byte      SSH_MSG_CHANNEL_FAILURE
      uint32    recipient channel

上述兩個訊息也不消耗視窗大小。

互動Session

一個Session就是一個遠端的程式的執行。這個程式或許是shell、應用程式、系統呼叫或者內建的子系統。它可能沒有繫結到虛擬終端上,又或者有或沒有涉及到X11轉發。同時間,可以有多個Session正在被執行。

開啟Session Channel

使用如下訊息開啟一個Session Channel,客戶端應該拒絕來自服務端的開啟Session Channel的請求以避免被攻擊。

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

請求一個虛擬終端

通過如下訊息可以讓伺服器為Session分配一個虛擬終端,character/row的優先順序相比於pixels更高,除非他們被設定成0。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "pty-req"
      boolean   want_reply
      string    TERM environment variable value (e.g., vt100)
      uint32    terminal width, characters (e.g., 80)
      uint32    terminal height, rows (e.g., 24)
      uint32    terminal width, pixels (e.g., 640)
      uint32    terminal height, pixels (e.g., 480)
      string    encoded terminal modes

客戶端應該拒絕來自服務端的虛擬終端明確資訊請求以避免被攻擊。

X11轉發

通過如下訊息可以為Session請求X11轉發。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "x11-req"
      boolean   want reply
      boolean   single connection
      string    x11 authentication protocol
      string    x11 authentication cookie
      uint32    x11 screen number

協議推薦將x11 authentication cookie傳送成一個虛假且隨機的cookie,知道連線訊息被接收後它將被檢驗並替換成真實的cookie。當session channel被關閉的時候,X11轉發也應該停止,但是已經開啟的轉發不應該自動被關閉。如果single connection被設定為TRUE,那麼只有一個連線被轉發。

這個訊息對應的操作是: 客戶端向伺服器發出請求,伺服器在本地新建N個(如果single connection不為0)X11伺服器(只是純粹的監聽6000+server自定義offset(openssh為10)的TCP埠,建立相應的DISPLAY環境變數)。

在remote session上執行gedit &時,gedit是符合X11協議的客戶端,所以它會檢測環境變數發現存在display,就和本地的6010埠建立連線。伺服器的偽x11伺服器socket偵測到連線就很向客戶端發起SSH_MSG_CHANNEL_OPEN x11 Channel的請求。

byte      SSH_MSG_CHANNEL_OPEN
      string    "x11"
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size
      string    originator address (e.g., "192.168.7.38")
      uint32    originator port

客戶端收到請求後再與本地的X11建立連線,這樣一個X11轉發的通道就完成了(我並沒有在openssh的原始碼中發現客戶端是如何使用originator address資料的)。

傳遞環境變數

在shell或command被開始時之後,或許有環境變數需要被傳遞過去。然而在特權程式裡不受控制的設定環境變數是一個很有風險的事情,所以規範推薦實現維護一個允許被設定的環境變數列表或者只有當sshd丟棄許可權後設置環境變數。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "env"
      boolean   want reply
      string    variable name
      string    variable value

啟動一個Shell或者一個命令

一旦一個Session被設定完畢,在遠端就會有一個程式被啟動。這個程式可以是一個Shell,也可以時一個應用程式或者是一個有著獨立域名的子系統。下面的請求每個Channel(Session)只允許設定一個。

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

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

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

視窗調整訊息

當客戶端的終端視窗大小被改變時,或許需要傳送這個訊息給伺服器。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "window-change"
      boolean   FALSE
      uint32    terminal width, columns
      uint32    terminal height, rows
      uint32    terminal width, pixels
      uint32    terminal height, pixels

這個訊息沒有響應。

本地流控

在很多系統中,這是否可行取決於偽終端是否使用control-S/control-Q進行流控。如果正在使用,那麼在客戶端就應該有一個流控的功能給服務端的響應提速,這還是取決於服務端裝置的(系統)實現。下面的訊息可以讓伺服器通知客戶端是否可以提供流控的功能,如果可以的話客戶端則可以使用control-S/control-Q進行流控。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "xon-xoff"
      boolean   FALSE
      boolean   client can do

這個訊息沒有響應。

訊號

一個訊號可以被傳輸給遠端的程式或服務使用下面的訊息。有一些系統可能沒有實現訊號,所以那些系統下的服務端應該忽略這個訊息。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "signal"
      boolean   FALSE
      string    signal name (without the "SIG" prefix)

signal name在下面的預定義名稱中有描述。

返回退出狀態

當在遠端的命令結束時,下面的訊息可以用來傳遞其退出的狀態碼。傳送或收到這個訊息後Channel將被關閉。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "exit-status"
      boolean   FALSE
      uint32    exit_status

同時遠端的程式也可能因為一個訊號而被迫關閉(exit_statue 為0的時候為正常關閉),這個情況下下面的訊息將被髮送。

byte      SSH_MSG_CHANNEL_REQUEST
      uint32    recipient channel
      string    "exit-signal"
      boolean   FALSE
      string    signal name (without the "SIG" prefix)
      boolean   core dumped
      string    error message in ISO-10646 UTF-8 encoding
      string    language tag [RFC3066]

TCP/IP 埠轉發

全域性請求

協議規定了許多種可以影響遠端全域性而獨立於任何Channel的請求,一個例子就是唯一個特定的埠請求TCP/IP轉發。雙方中的任意一方都有可能在任意時間傳送全域性請求,接收方必須做出合理的響應,其格式如下。

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

響應如下,通常不包含response specific data這一欄。

byte      SSH_MSG_REQUEST_SUCCESS
      ....     response specific data

協議規定,SSH_MSG_GLOBAL_REQUESTS的響應順序必須如其傳送順序一致。有明確指示Channel的請求訊息才可以不按順序傳送。

請求埠轉發

如果其中一方期望一個發往對方埠的資料可以轉發到本地的埠,那麼他可以傳送如下的請求。

byte      SSH_MSG_GLOBAL_REQUEST
      string    "tcpip-forward"
      boolean   want reply
      string    address to bind (e.g., "0.0.0.0")
      uint32    port number to bind

address to bindport number to bind用來標明哪個具體的IP地址(或域名)和埠用於轉發(另一端端開啟對哪個IP和埠的監聽並轉發到本地),address to bind滿足以下的語法規則。

  • ""表示接受所有的被SSH實現方支援的協議。
  • "0.0.0.0"表示監聽所有IPv4埠。
  • "::"表示監聽所有IPv6埠。
  • "localhost"表示在迴環網絡卡上監聽所有被SSH實現支援的協議。
  • "127.0.0.1""::1"分別表示監聽IPv4和IPv6的迴環網絡卡。

客戶端應該拒絕這個訊息,這個訊息一般只允許客戶端傳送。同時如果客戶端傳遞的port number是0,且它設定了want reply為TRUE,那麼伺服器應該分配下一個可用的非許可權埠,並且在響應中告知客戶端。

byte     SSH_MSG_REQUEST_SUCCESS
      uint32   port that was bound on the server

埠轉發可以被下面的訊息取消,一個channel open請求可能會直到接收到該訊息的響應後收到。這個訊息只有客戶端可以傳送。

byte      SSH_MSG_GLOBAL_REQUEST
      string    "cancel-tcpip-forward"
      boolean   want reply
      string    address_to_bind (e.g., "127.0.0.1")
      uint32    port number to bind

TCP/IP轉發Channel

當服務端被設定需要監聽並轉發的埠收到了外部的連線時,服務端將傳送下面的訊息請求客戶端開啟一個TCP/IP轉發Channel。

byte      SSH_MSG_CHANNEL_OPEN
      string    "forwarded-tcpip"
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size
      string    address that was connected (外部連線到伺服器監聽地址的的IP地址)
      uint32    port that was connected 
      string    originator IP address (這個應該是服務端監聽的地址)
      uint32    originator port

客戶端應該比對是否向伺服器請求了originator port的TCP/IP埠轉發。

當客戶端本地的TCP/IP轉發埠接收到來自外部的連線(這個監聽埠是客戶端主動開啟的)時,客戶端傳送如下訊息給伺服器轉發這個TCP/IP資料。

byte      SSH_MSG_CHANNEL_OPEN
      string    "direct-tcpip"
      uint32    sender channel
      uint32    initial window size
      uint32    maximum packet size
      string    host to connect
      uint32    port to connect
      string    originator IP address
      uint32    originator port

host to connectport to connect表示服務端內部需要建立一個通往host to connectport to connect的連線。originator IP address表示客戶端監聽到的連線發起於哪個外部IP地址、originator port表示客戶端監聽到的連線發起於哪個外部主機的埠。

轉發Channel獨立於Session存在,Session關閉並不意味著轉發Channel也要被關閉。客戶端應該拒絕direct-tcpip請求。

終端模式的編碼

在請求一個終端的時候會用到終端模式的編碼(encoded terminal modes),它們被編碼進位元組流裡。這是為了他們可以方便地在不同環境間傳輸。位元組流包括以位元組為值地操作碼構成地引數對。1-159的操作碼時uint32型別的引數,160-255還未被定義,並且如果遇到它們應該停止解析。位元組流被操作碼TTY_OP_END(0x00)終止。

客戶端應該儘可能的把它知道的模式操作碼加入位元組流中,而伺服器對於它不知道的模式應該忽略。至少在用類POSIX 虛擬終端的系統間,會支援一些機器無關的特性。這個協議也能支援其他的系統,但是客戶端或許需要補充一系列合理的引數數值這樣伺服器才能為偽終端提供合理的模式。具體預定義數值如下(為了可讀性,將Byte寫成了數字):

opcode  mnemonic       description
          ------  --------       -----------
          0     TTY_OP_END  Indicates end of options.
          1     VINTR       Interrupt character; 255 if none.  Similarly
                             for the other characters.  Not all of these
                             characters are supported on all systems.
          2     VQUIT       The quit character (sends SIGQUIT signal on
                             POSIX systems).
          3     VERASE      Erase the character to left of the cursor.
          4     VKILL       Kill the current input line.
          5     VEOF        End-of-file character (sends EOF from the
                             terminal).
          6     VEOL        End-of-line character in addition to
                             carriage return and/or linefeed.
          7     VEOL2       Additional end-of-line character.
          8     VSTART      Continues paused output (normally
                             control-Q).
          9     VSTOP       Pauses output (normally control-S).
          10    VSUSP       Suspends the current program.
          11    VDSUSP      Another suspend character.
                    12    VREPRINT    Reprints the current input line.
          13    VWERASE     Erases a word left of cursor.
          14    VLNEXT      Enter the next character typed literally,
                             even if it is a special character
          15    VFLUSH      Character to flush output.
          16    VSWTCH      Switch to a different shell layer.
          17    VSTATUS     Prints system status line (load, command,
                             pid, etc).
          18    VDISCARD    Toggles the flushing of terminal output.
          30    IGNPAR      The ignore parity flag.  The parameter
                             SHOULD be 0 if this flag is FALSE,
                             and 1 if it is TRUE.
          31    PARMRK      Mark parity and framing errors.
          32    INPCK       Enable checking of parity errors.
          33    ISTRIP      Strip 8th bit off characters.
          34    INLCR       Map NL into CR on input.
          35    IGNCR       Ignore CR on input.
          36    ICRNL       Map CR to NL on input.
          37    IUCLC       Translate uppercase characters to
                             lowercase.
          38    IXON        Enable output flow control.
          39    IXANY       Any char will restart after stop.
          40    IXOFF       Enable input flow control.
          41    IMAXBEL     Ring bell on input queue full.
          50    ISIG        Enable signals INTR, QUIT, [D]SUSP.
          51    ICANON      Canonicalize input lines.
          52    XCASE       Enable input and output of uppercase
                             characters by preceding their lowercase
                             equivalents with "".
          53    ECHO        Enable echoing.
          54    ECHOE       Visually erase chars.
          55    ECHOK       Kill character discards current line.
          56    ECHONL      Echo NL even if ECHO is off.
          57    NOFLSH      Don't flush after interrupt.
          58    TOSTOP      Stop background jobs from output.
          59    IEXTEN      Enable extensions.
          60    ECHOCTL     Echo control characters as ^(Char).
          61    ECHOKE      Visual erase for line kill.
          62    PENDIN      Retype pending input.
          70    OPOST       Enable output processing.
          71    OLCUC       Convert lowercase to uppercase.
          72    ONLCR       Map NL to CR-NL.
          73    OCRNL       Translate carriage return to newline
                             (output).
          74    ONOCR       Translate newline to carriage
                             return-newline (output).
          75    ONLRET      Newline performs a carriage return
                             (output).
          90    CS7         7 bit mode.
          91    CS8         8 bit mode.
          92    PARENB      Parity enable.
          93    PARODD      Odd parity, else even.

          128 TTY_OP_ISPEED  Specifies the input baud rate in
                              bits per second.
          129 TTY_OP_OSPEED  Specifies the output baud rate in
                              bits per second.

預定義名稱

連線協議Channel型別

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]

連線協議全域性請求名

Request type                  Reference
         ------------                  ---------
         tcpip-forward                 [SSH-CONNECT, Section 7.1]
         cancel-tcpip-forward          [SSH-CONNECT, Section 7.1]

連線協議Channel明細請求名

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]

連線協議子系統名

暫無

Reference

  1. OpenSSH Specifications
    這是OpenSSH所展現的最直接的資料頁面,但是有很多細節部分的規格與實現沒有羅列。而且RFC文件錯綜複雜,有很多地方都引用不全必須靠“幸運”才能翻看到。
  2. The Secure Shell (SSH) Protocol Architecture
    SSH協議架構的rfc頁面,它將SSH分為三部分,傳輸、認證和連線。
  3. The Secure Shell (SSH) Protocol Assigned Numbers
    規定協議中各種ID(巨集)所使用的序號。
  4. The Secure Shell (SSH) Connection Protocol
    規定SSH連線協議。
  5. OpenSSH Portable
    OpenSSH的原始碼。