Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

ScopeMessageExampleReply if requested
Whole connectionSSH_MSG_GLOBAL_REQUESTAsk the server to listen for remote forwardingSSH_MSG_REQUEST_SUCCESS or FAILURE
One channelSSH_MSG_CHANNEL_REQUESTAllocate a PTY or start a commandSSH_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:

  1. The first channel exhausts its server-advertised window.
  2. The second channel continues to deliver data.
  3. The first receiver consumes data and replenishes only that channel.
  4. The second channel’s client sends EOF while still receiving final data.
  5. 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.

References