It's now time for the final RFC I'll be using: RFC 4254. This RFC covers the connection protocol, which describes how to setup, multiplex, and flow-control channels of application-specific data.
Today's first step is opening what SSH calls a “connection”. SSH connections work a bit like TCP connections. An arbitrary number of connections are established between the client and server. Each side of each connection has a unique ID akin to a port number. Once established, messages sent across the connection are regulated by an adjustable window.
Opening a channel is started by picking an available ID for the client side of the connection — I'll start at 123 — and then issuing an open request from the selected ID. The open request also indicates the initial window size and maximum packet size for messages sent to the new ID.
The server responds in kind by picking an available ID for the server side of the connection, as well as an initial window size and maximum packet size for packets travelling to the server.
⌨582 c ⌨69 a ⌨205 r ⌨214 g ⌨94 o ⌨149 ⌨393 r ⌨70 u ⌨122 n ⌨147 ⌨328 b ⌨89 e ⌨114 l ⌨106 a ⌨126 f ⌨56 o ⌨140 n ⌨96 t ⌨82 e ⌨205 : ⌨244 2 ⌨148 2 ⌨335 ⌨133 r ⌨93 u ⌨120 s ⌨46 t ⌨107 ⌨218 . ⌨80 / ⌨231 k ⌨82 e ⌨48 y ⌨168 . ⌨245 k ⌨118 e ⌨113 y ⌨313 ⌨267 r ⌨80 u ⌨107 n ⌨147 ⌨465 " ⌨166 c ⌨96 a ⌨109 t ⌨128 ⌨135 / ⌨213 p ⌨96 r ⌨91 o ⌨132 c ⌨120 / ⌨139 v ⌨73 e ⌨50 r ⌨151 s ⌨43 i ⌨70 o ⌨169 n ⌨298 " ⌨315 ⏎  ⎙60 ⎙0 Finished dev [unoptimized + debuginfo] target(s) in 0.01s⏎  Running `target\debug\main.exe 'belafonte:22' rust ⏎  ./key.key run "cat /proc/version"`⏎ ⏎  ⎙1122 Authenticated user rust⏎ ▲ Open connection⏎  Initial Window: 2147483647⏎  Maximum Packet: 65536⏎  ⎙581 ▼ Connection opened⏎  Initial Window: 0⏎  Maximum Packet: 32768⏎  ␃
The exchange above opened a connection between ID 123 on the client and ID 2 on the server. Each side is responsible for selecting its own connection ID, and is free to reuse previous IDs once no longer in use. Curiously, the server starts with an initial window of 0, meaning I cannot immediately send any data messages over the channel. I, on the other hand, welcome a mountain of data — 2GiB — without confirmation.
With a channel established, all that remains is to ask for something to happen. I'm starting with command execution, as you may have noticed from the command typed above. The initial server window size of 0 doesn't prevent me from executing a command; the windowing mechanism only applies to messages classified as “data” messages, and the message to execute a shell command is not considered a data message. The messages that carry the standard input/output/error streams are classified as data, so I'd need some window space before writing to the remote standard input stream.
⌨458 c ⌨97 a ⌨90 r ⌨95 g ⌨95 o ⌨149 ⌨218 r ⌨45 u ⌨93 n ⌨138 ⌨243 b ⌨96 e ⌨108 l ⌨112 a ⌨140 f ⌨83 o ⌨136 n ⌨103 t ⌨89 e ⌨282 : ⌨164 2 ⌨131 2 ⌨369 ⌨211 r ⌨185 u ⌨92 s ⌨61 t ⌨105 ⌨205 . ⌨69 / ⌨223 k ⌨79 e ⌨34 y ⌨249 . ⌨53 k ⌨80 e ⌨38 y ⌨327 ⌨328 r ⌨110 u ⌨152 n ⌨129 ⌨208 " ⌨146 c ⌨65 a ⌨183 t ⌨117 ⌨223 / ⌨226 p ⌨80 r ⌨121 o ⌨92 c ⌨135 / ⌨181 v ⌨62 e ⌨72 r ⌨137 s ⌨52 i ⌨71 o ⌨147 n ⌨237 " ⌨169 ⏎  ⎙64 ⎙0 Finished dev [unoptimized + debuginfo] target(s) in 0.02s⏎  ⎙1 ⎙0 Running `target\debug\main.exe 'belafonte:22' rust ⏎  ./key./key run "cat /proc/version"`⏎ ⏎  ⎙1280 ▲ Open channel⏎  Initial Window: 2147483647⏎  Maximum Packet: 65536⏎  ⎙822 ▼ Channel opened⏎  Initial Window: 0⏎  Maximum Packet: 32768⏎  ⎙0 ▲ Execute: cat /proc/version⏎  ⎙59 ▼ Expand window by 2097152 bytes⏎  ⎙3 ▼ Stdout: Linux version 4.15.0-99-generic (buildd@lcy01-amd64-013)⏎  (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04))⏎  #100-Ubuntu SMP⏎  ⎙0 ▼ End of output⏎  ⎙0 ▼ Exit code 0⏎  ⎙0 ▼ Close channel⏎  ⎙0 ▲ Close channel⏎  ␃
After passing through many layers, there's finally some “useful” output! The server does end up sending a window-expansion message before I'd need to write to standard input. My implementation doesn't sit and wait for additional messages between opening a channel and issuing the execute command, so I can't say for sure what triggers the window expanding to 2 MiB. The server may be clever and select different window sizes based on the application (e.g. shell command vs X11 session). Let's find out. I'll temporarily make the client wait for messages between opening a channel and sending the command.
⌨631 c ⌨97 a ⌨60 r ⌨110 g ⌨103 o ⌨76 ⌨174 r ⌨32 u ⌨107 n ⌨110 ⌨138 b ⌨90 e ⌨112 l ⌨93 a ⌨188 f ⌨63 o ⌨155 n ⌨93 t ⌨113 e ⌨192 : ⌨290 2 ⌨148 2 ⌨335 ⌨164 r ⌨94 u ⌨94 s ⌨64 t ⌨50 ⌨159 . ⌨92 / ⌨218 k ⌨64 e ⌨66 y ⌨105 . ⌨226 k ⌨85 e ⌨60 y ⌨775 ⌨117 r ⌨75 u ⌨137 n ⌨90 ⌨146 p ⌨70 w ⌨156 d ⌨338 ⏎  ⎙0 ⎙0 Finished ⎙0 dev [unoptimized + debuginfo] target(s) in 0.01s ⎙0 ⏎  Running `target\debug\main.exe 'belafonte:22' rust ⏎  ./key.key run pwd`⏎ ⏎  ⎙1142 ▲ Open channel⏎  Initial Window: 2147483647⏎  Maximum Packet: 65536⏎  ⎙674 ▼ Channel opened⏎  Initial Window: 0⏎  Maximum Packet: 32768⏎  ⎙57 ▼ Expand window by 2097152 bytes⏎  ⎙0 ▲ Execute: pwd⏎  ⎙2 ▼ Stdout: /home/rust⏎  ⎙7 ▼ Exit code 0⏎  ⎙0 ▼ End of output⏎  ⎙0 ▼ Close channel⏎  ⎙0 ▲ Close channel⏎  ␃
It looks like the server sets its window to zero and then immediately increases it; the server doesn't select the 2 MiB window size based on what the channel will be used for. Oh well. With that curiosity satisfied, I think I'm about done for the day.