Channels and flow control
After user authentication, the ssh-connection service multiplexes many
logical byte streams over one transport. Each stream is a
channel with its own identifiers, flow-control windows, requests, and
close state.
RFC 4254 specifies this layer. The next two chapters apply its common rules to sessions and TCP forwarding.
One transport, many channels
SSH multiplexes independent activities into the transport packet stream.
Closing one channel does not close the others or end the SSH transport.
Channel numbers are local
Each peer chooses its own number for the same logical channel. Suppose the client calls a new channel 7 and the server calls it 42:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: CHANNEL_OPEN(sender=7, window=C_recv, max=C_max)
S-->>C: OPEN_CONFIRMATION(recipient=7, sender=42,<br/>window=S_recv, max=S_max)
C->>S: CHANNEL_DATA(recipient=42, data)
S-->>C: CHANNEL_DATA(recipient=7, data)
The sender channel field introduces the number chosen by the sender. Subsequent messages carry the recipient’s local number. The same logical channel therefore has a pair of identifiers, not one universal channel ID:
local channel 7 <-> remote channel 42
The words sender and recipient are relative to the message carrying the field. Each channel’s identifier pair keeps its data, windows, and lifecycle separate from every other channel.
Opening a channel
Either peer may open a channel. The generic message is:
byte SSH_MSG_CHANNEL_OPEN
string channel type
uint32 sender channel
uint32 initial window size
uint32 maximum packet size
... channel-type-specific fields
The recipient returns SSH_MSG_CHANNEL_OPEN_CONFIRMATION, which introduces its own channel number and receive limits, or SSH_MSG_CHANNEL_OPEN_FAILURE, which contains a reason code and description. Standard failure reasons distinguish administrative prohibition, connection failure, unknown type, and resource shortage. See RFC 4254, Section 5.1.
stateDiagram-v2
[*] --> Opening: OPEN sent with sender ID and receive limits
Opening --> Open: OPEN_CONFIRMATION establishes peer ID and limits
Opening --> Failed: OPEN_FAILURE
Open --> HalfClosedLocal: send EOF
Open --> HalfClosedRemote: receive EOF
HalfClosedLocal --> Closing: send or receive CLOSE
HalfClosedRemote --> Closing: send or receive CLOSE
Open --> Closing: send or receive CLOSE
Closing --> Closed: CLOSE sent and received
Failed --> [*]
Closed --> [*]
A channel number does not become reusable merely because one side has sent CLOSE. Under RFC 4254, the channel is closed for a peer only once that peer has both sent and received SSH_MSG_CHANNEL_CLOSE.
Windows and flow control
TCP already has flow control, but one TCP receive buffer is shared by all multiplexed SSH traffic. SSH adds a receive window to each channel and each direction, so a slow consumer can stop its own stream without requiring every channel to stop.
The window advertised in CHANNEL_OPEN or OPEN_CONFIRMATION is a promise: “you may send me this many bytes of channel data.” It describes the sender’s receive capacity, not its send capacity.
For each direction, sending follows this arithmetic:
allowed_channel_data = min(remote_window, remote_max_packet)
remote_window -= bytes_sent
When the receiver has made capacity available, it sends:
SSH_MSG_CHANNEL_WINDOW_ADJUST(recipient_channel, bytes_to_add)
The sender adds that credit without allowing the 32-bit window to overflow. Data must stop when the remote window reaches zero, but control messages such as window adjustments, EOF, CLOSE, and channel requests do not consume window space.
sequenceDiagram
participant A as Sender
participant B as Receiver
Note over A: remote_window = 10
A->>B: CHANNEL_DATA(6 bytes)
Note over A: remote_window = 4
A->>B: CHANNEL_DATA(4 bytes)
Note over A: remote_window = 0<br/>pause this channel
Note over B: application consumes 8 bytes
B-->>A: CHANNEL_WINDOW_ADJUST(+8)
Note over A: remote_window = 8<br/>resume
Both SSH_MSG_CHANNEL_DATA and SSH_MSG_CHANNEL_EXTENDED_DATA consume the same channel window. For session channels, extended-data type 1 is stderr. The maximum channel packet size is independent from the transport packet ceiling, and both limits apply. RFC 4254, Section 5.2 defines these rules.
The receiver decides when and by how much to replenish the window. This ties permission to send to the receiver’s capacity to consume data. Until WINDOW_ADJUST adds credit, a zero-window sender must pause that channel even if the underlying TCP connection remains writable.
Requests are not data
SSH has two request scopes:
| Scope | Message | Example | Reply if requested |
|---|---|---|---|
| Whole connection | SSH_MSG_GLOBAL_REQUEST | Ask the server to listen for remote forwarding | SSH_MSG_REQUEST_SUCCESS or FAILURE |
| One channel | SSH_MSG_CHANNEL_REQUEST | Allocate a PTY or start a command | SSH_MSG_CHANNEL_SUCCESS or FAILURE |
Both include a want reply boolean. When it is false, no success or failure reply is sent. When it is true, the generic reply contains no request identifier, so its meaning comes from ordering. Global replies preserve global request order; channel-request replies preserve order within that channel, while replies for different channels may be interleaved. See RFC 4254, Sections 4 and 5.4.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: CHANNEL_REQUEST(ch=42, "pty-req", want_reply=true)
C->>S: CHANNEL_REQUEST(ch=42, "shell", want_reply=true)
Note over S: Reply order on channel 42 is significant
S-->>C: CHANNEL_SUCCESS(ch=7)
S-->>C: CHANNEL_SUCCESS(ch=7)
For setup operations whose success affects correctness, request and check a reply. A client that sends exec with no reply and immediately treats bytes as command output cannot distinguish refusal from delayed output or later channel closure.
EOF and CLOSE
SSH_MSG_CHANNEL_EOF means “I will send no more data in this direction.” It is a half-close. No protocol reply is required, and the other direction remains usable.
SSH_MSG_CHANNEL_CLOSE means “terminate this channel.” A recipient must send CLOSE back unless it already did. EOF is conventional before a graceful close but is not required.
sequenceDiagram
participant C as Client stdin side
participant S as Remote command
C->>S: CHANNEL_DATA("request body")
C->>S: CHANNEL_EOF
Note over C,S: Client-to-server data direction has ended
S-->>C: CHANNEL_DATA("final response")
S-->>C: CHANNEL_REQUEST("exit-status", 0)
S-->>C: CHANNEL_EOF
S-->>C: CHANNEL_CLOSE
C->>S: CHANNEL_CLOSE
Note over C,S: Both sent and received CLOSE<br/>IDs may be reused
Local stdin reaching EOF ends only the client-to-server data direction; the remote command may still send final output and an exit status. EOF and CLOSE can be sent even when the data window is zero, so exhausted data credit does not make shutdown impossible. See RFC 4254, Section 5.3.
Protocol review
The connection protocol has these invariants:
- one logical channel has different local IDs at its two endpoints;
- each direction has independently advertised windows and maximum packet sizes;
- ordinary and extended data consume the same receive credit;
- a zero-window channel does not block control traffic or unrelated channels;
- request replies derive their meaning from scope and ordering;
- EOF half-closes one direction without discarding the other; and
- channel IDs are reused only after CLOSE has been both sent and received.
Lab: simulate two channels
Open two channels on paper. Give the client and server different local IDs for each. Then trace this sequence:
- The first channel exhausts its server-advertised window.
- The second channel continues to deliver data.
- The first receiver consumes data and replenishes only that channel.
- The second channel’s client sends EOF while still receiving final data.
- Both channels complete independent CLOSE handshakes.
At each step, record the recipient channel number and both directional window values. If one exhausted window stalls the other channel, the design is not actually multiplexed.