ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_flow_control.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ssh_flow_control.h
6 * @brief SSH channel flow control - the RFC 4254 sec 5.2 window pair and nothing else.
7 *
8 * Every channel carries two independent counters: how many bytes the peer may still send us before
9 * we replenish (local), and how many we may still send it (peer). Getting either wrong desynchronizes
10 * the session - overrun the peer's window and it drops the channel, forget to replenish ours and the
11 * transfer stalls forever with both sides waiting.
12 *
13 * The accounting lived in four places before this file existed: ssh_channel.cpp held the counters and
14 * the rules, ssh_forward.cpp read `peer_window` directly to size its reads, ssh_server.cpp routed the
15 * adjust message, and ssh_client.cpp carried a second implementation of the same arithmetic. One
16 * concern, one owner: the counters are only correct if a single piece of code decides what they mean.
17 *
18 * Channel multiplexing stays in ssh_channel.* - this file knows nothing about channel ids, types, or
19 * the pool. It is the arithmetic and the rules, over one channel's pair of counters.
20 */
21
22#ifndef PROTOCORE_SSH_FLOW_CONTROL_H
23#define PROTOCORE_SSH_FLOW_CONTROL_H
24
25#include <stddef.h>
26#include <stdint.h>
27
28/** @brief One channel's flow-control state (RFC 4254 sec 5.2). */
29struct SshFlow
30{
31 uint32_t local_window; ///< Bytes the peer may still send us before we WINDOW_ADJUST.
32 uint32_t local_max; ///< The window we advertised, and replenish back up to.
33 uint32_t peer_window; ///< Bytes we may still send the peer.
34 uint32_t peer_max_pkt; ///< Peer's maximum packet size; caps a single send independently of the window.
35};
36
37/**
38 * @brief Start a channel's windows: ours at @p local_window, the peer's at what it advertised.
39 *
40 * The local window is a parameter rather than a baked-in constant because the server and the
41 * client advertise different sizes, and the value we replenish to must be the value we told the
42 * peer about. Passing it here keeps the two from drifting apart.
43 *
44 * @param local_window what we advertise in CHANNEL_OPEN / CONFIRMATION; also the replenish target.
45 * @param peer_window the peer's initial window from CHANNEL_OPEN / CONFIRMATION.
46 * @param peer_max_pkt the peer's maximum packet size from the same message.
47 */
48void pc_ssh_flow_init(SshFlow *f, uint32_t local_window, uint32_t peer_window, uint32_t peer_max_pkt);
49
50/**
51 * @brief Account @p n inbound bytes against our window.
52 *
53 * @return false if @p n exceeds what we advertised - the peer overran the window (RFC 4254 sec 5.2)
54 * and the caller must fail the channel. The window is left untouched on failure.
55 */
56bool pc_ssh_flow_recv_take(SshFlow *f, uint32_t n);
57
58/**
59 * @brief Decide whether a WINDOW_ADJUST is due, and for how much. Does not mutate.
60 *
61 * Replenishes once the window has drained past half, which keeps a bulk transfer from stalling
62 * without emitting an adjust per packet.
63 *
64 * Pair it with pc_ssh_flow_local_credit() only after the adjust has actually gone out. Deciding and
65 * crediting are separate because on some paths the send can fail, and crediting first would leave us
66 * believing we advertised bytes the peer never heard about - the peer then stops at its smaller
67 * window while we wait for data, and the transfer deadlocks.
68 *
69 * @return true if a WINDOW_ADJUST is due; @p *add receives the delta to advertise.
70 */
71bool pc_ssh_flow_replenish_due(const SshFlow *f, uint32_t *add);
72
73/** @brief Credit our window by @p add, once that WINDOW_ADJUST has actually been sent. */
74void pc_ssh_flow_local_credit(SshFlow *f, uint32_t add);
75
76/** @brief True if @p len bytes fit both the peer's remaining window and its maximum packet size. */
77bool pc_ssh_flow_send_allows(const SshFlow *f, size_t len);
78
79/**
80 * @brief Clamp a would-be send to what the peer currently permits.
81 *
82 * A producer that pulls from a local source sizes its read to this, so it never reads bytes it
83 * cannot legally forward. Returns 0 when the window is closed, which the caller treats as
84 * "stop pumping until a WINDOW_ADJUST arrives".
85 *
86 * @return min(@p want, peer window, peer maximum packet size).
87 */
88uint32_t pc_ssh_flow_send_cap(const SshFlow *f, uint32_t want);
89
90/** @brief Account @p n outbound bytes against the peer's window (call only after send_allows()). */
91void pc_ssh_flow_send_take(SshFlow *f, uint32_t n);
92
93/**
94 * @brief Credit the peer's window from an inbound WINDOW_ADJUST.
95 *
96 * Saturates at UINT32_MAX rather than wrapping: a peer advertising a total past 2^32 is out of spec,
97 * and wrapping would hand us a tiny window and stall the transfer.
98 */
99void pc_ssh_flow_peer_add(SshFlow *f, uint32_t add);
100
101/** @brief Bytes we may still send the peer - the bound a producer sizes its next read to. */
102uint32_t pc_ssh_flow_peer_window(const SshFlow *f);
103
104// ---------------------------------------------------------------------------
105// Channel signaling (RFC 4254 sec 5)
106//
107// Every channel-related message is a transition on the window state above: OPEN / OPEN_CONFIRMATION
108// establish it (they carry the initial window and maximum packet size), WINDOW_ADJUST increments it,
109// DATA consumes it, EOF / CLOSE terminate it. Because the transitions and the state are the same
110// concern, they live together - the RFC's rule that no data may be sent until the window allows it is
111// only enforceable where the window is.
112//
113// These take the flow plus the ids the wire carries, never a channel struct: resolving a recipient
114// channel number to a channel is multiplexing, and that stays in ssh_channel.
115// ---------------------------------------------------------------------------
116
117/** @brief CHANNEL_OPEN_FAILURE. @p reason: 1 admin-prohibited, 2 connect-failed, 3 unknown-type, 4 resource. */
118int32_t pc_ssh_sig_build_open_failure(uint8_t *out, size_t cap, uint32_t peer_id, uint32_t reason, size_t *out_len);
119
120/** @brief CHANNEL_OPEN_CONFIRMATION, advertising our current window and maximum packet size. */
121int32_t pc_ssh_sig_build_open_confirm(const SshFlow *f, uint32_t peer_id, uint32_t local_id, uint8_t *out, size_t cap,
122 size_t *out_len);
123
124/**
125 * @brief CHANNEL_DATA carrying @p len bytes, and account them against the peer's window.
126 *
127 * Refuses when the send would exceed the peer's window or its maximum packet size, so the RFC 4254
128 * sec 5.2 limit cannot be violated by any caller - the check and the debit are one step here.
129 */
130int32_t pc_ssh_sig_build_data(SshFlow *f, uint32_t peer_id, const uint8_t *data, size_t len, uint8_t *out, size_t cap,
131 size_t *out_len);
132
133/** @brief CHANNEL_WINDOW_ADJUST granting @p add more bytes. Credit the window only once this is sent. */
134int32_t pc_ssh_sig_build_window_adjust(uint32_t peer_id, uint32_t add, uint8_t *out, size_t cap, size_t *out_len);
135
136/** @brief CHANNEL_EOF followed by CHANNEL_CLOSE, as one 10-byte pair. */
137int32_t pc_ssh_sig_build_close(uint32_t peer_id, uint8_t *out, size_t cap, size_t *out_len);
138
139#endif // PROTOCORE_SSH_FLOW_CONTROL_H
void pc_ssh_flow_init(SshFlow *f, uint32_t local_window, uint32_t peer_window, uint32_t peer_max_pkt)
Start a channel's windows: ours at local_window, the peer's at what it advertised.
void pc_ssh_flow_local_credit(SshFlow *f, uint32_t add)
Credit our window by add, once that WINDOW_ADJUST has actually been sent.
int32_t pc_ssh_sig_build_window_adjust(uint32_t peer_id, uint32_t add, uint8_t *out, size_t cap, size_t *out_len)
CHANNEL_WINDOW_ADJUST granting add more bytes. Credit the window only once this is sent.
bool pc_ssh_flow_recv_take(SshFlow *f, uint32_t n)
Account n inbound bytes against our window.
int32_t pc_ssh_sig_build_open_confirm(const SshFlow *f, uint32_t peer_id, uint32_t local_id, uint8_t *out, size_t cap, size_t *out_len)
CHANNEL_OPEN_CONFIRMATION, advertising our current window and maximum packet size.
int32_t pc_ssh_sig_build_open_failure(uint8_t *out, size_t cap, uint32_t peer_id, uint32_t reason, size_t *out_len)
CHANNEL_OPEN_FAILURE. reason: 1 admin-prohibited, 2 connect-failed, 3 unknown-type,...
uint32_t pc_ssh_flow_send_cap(const SshFlow *f, uint32_t want)
Clamp a would-be send to what the peer currently permits.
int32_t pc_ssh_sig_build_data(SshFlow *f, uint32_t peer_id, const uint8_t *data, size_t len, uint8_t *out, size_t cap, size_t *out_len)
CHANNEL_DATA carrying len bytes, and account them against the peer's window.
bool pc_ssh_flow_send_allows(const SshFlow *f, size_t len)
True if len bytes fit both the peer's remaining window and its maximum packet size.
void pc_ssh_flow_send_take(SshFlow *f, uint32_t n)
Account n outbound bytes against the peer's window (call only after send_allows()).
uint32_t pc_ssh_flow_peer_window(const SshFlow *f)
Bytes we may still send the peer - the bound a producer sizes its next read to.
void pc_ssh_flow_peer_add(SshFlow *f, uint32_t add)
Credit the peer's window from an inbound WINDOW_ADJUST.
bool pc_ssh_flow_replenish_due(const SshFlow *f, uint32_t *add)
Decide whether a WINDOW_ADJUST is due, and for how much. Does not mutate.
int32_t pc_ssh_sig_build_close(uint32_t peer_id, uint8_t *out, size_t cap, size_t *out_len)
CHANNEL_EOF followed by CHANNEL_CLOSE, as one 10-byte pair.
One channel's flow-control state (RFC 4254 sec 5.2).
uint32_t peer_max_pkt
Peer's maximum packet size; caps a single send independently of the window.
uint32_t local_max
The window we advertised, and replenish back up to.
uint32_t peer_window
Bytes we may still send the peer.
uint32_t local_window
Bytes the peer may still send us before we WINDOW_ADJUST.