ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_comp.cpp
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_comp.cpp
6 * @brief SSH server-to-client compression owner - per-connection state + activation.
7 */
8
10
11#if PC_ENABLE_SSH_ZLIB
12
15#include <string.h>
16
17// The per-connection compressor holds a window-sized work buffer + a window-sized hash chain (tens
18// of KB); the pool does not fit internal DRAM alongside the SSH crypto stack, so it lives in PSRAM
19// (PC_SSH_ZLIB_IN_PSRAM). Same mechanism/caveat as the TLS arena and HTTP/2 pool: it needs a
20// framework built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y (the stock arduino-esp32 core
21// ships it OFF, so EXT_RAM_BSS_ATTR would silently no-op); see tools/psram/README.md.
22#if PC_SSH_ZLIB_IN_PSRAM && defined(ARDUINO)
23#include <esp_attr.h> // pulls in sdkconfig.h -> CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY
24#if !defined(CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY)
25#error \
26 "PC_SSH_ZLIB_IN_PSRAM needs a framework built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y. The stock arduino-esp32 core ships it OFF, so EXT_RAM_BSS_ATTR silently no-ops and the compressor pool would overflow internal DRAM. Rebuild the core (tools/psram/README.md) or unset PC_SSH_ZLIB_IN_PSRAM."
27#endif
28#if defined(EXT_RAM_BSS_ATTR)
29#define PC_SSH_COMP_ATTR EXT_RAM_BSS_ATTR // IDF v5 / arduino-esp32 3.x
30#elif defined(EXT_RAM_ATTR)
31#define PC_SSH_COMP_ATTR EXT_RAM_ATTR // IDF v4 / arduino-esp32 2.x
32#else
33#define PC_SSH_COMP_ATTR
34#endif
35#else
36#define PC_SSH_COMP_ATTR
37#endif
38
39// Per-connection compression state (large buffers -> PSRAM; the flags are trivial).
40struct SshCompState
41{
42 SshDeflate z; ///< streaming compressor (bound to the buffers below).
43 uint8_t work[SSH_ZLIB_WORK_SIZE]; ///< history + input work buffer.
44 uint16_t head[SSH_ZLIB_HASH_SIZE]; ///< hash bucket heads.
45 uint16_t prev[SSH_ZLIB_WORK_SIZE]; ///< hash chain (absolute-position indexed).
46 uint16_t ll_code[288]; ///< fixed lit/length Huffman codes.
47 uint8_t ll_len[288]; ///< their bit lengths.
48 uint16_t d_code[30]; ///< fixed distance Huffman codes.
49 uint8_t d_len[30]; ///< their bit lengths.
50 SshInflate inf; ///< streaming decompressor (bound to inf_window below).
51 uint8_t inf_window[SSH_INFLATE_WINDOW]; ///< 32 KB context-takeover window for c2s inflate.
52 SshCompAlg s2c_alg; ///< negotiated server-to-client algorithm.
53 bool s2c_active; ///< true once the s2c (deflate) stream has started.
54 SshCompAlg c2s_alg; ///< negotiated client-to-server algorithm.
55 bool c2s_active; ///< true once the c2s (inflate) stream has started.
56};
57
58// All SSH compression state, owned by one instance (internal linkage): the per-connection
59// deflate stream table. One named owner, unreachable from any other translation unit.
60struct SshCompCtx
61{
62 SshCompState comp[MAX_SSH_CONNS];
63};
64static PC_SSH_COMP_ATTR SshCompCtx s_ssh_comp;
65
66static void start_s2c(SshCompState *c)
67{
68 ssh_deflate_init(&c->z, c->work, c->head, c->prev, c->ll_code, c->ll_len, c->d_code, c->d_len);
69 c->s2c_active = true;
70}
71
72static void start_c2s(SshCompState *c)
73{
74 ssh_inflate_init(&c->inf, c->inf_window);
75 c->c2s_active = true;
76}
77
78void ssh_comp_reset(uint8_t i)
79{
80 if (i >= MAX_SSH_CONNS)
81 {
82 return;
83 }
84 SshCompState *c = &s_ssh_comp.comp[i];
85 c->s2c_alg = SshCompAlg::SSH_COMP_NONE;
86 c->s2c_active = false;
87 c->c2s_alg = SshCompAlg::SSH_COMP_NONE;
88 c->c2s_active = false;
89}
90
91void ssh_comp_set_s2c(uint8_t i, SshCompAlg alg)
92{
93 if (i >= MAX_SSH_CONNS)
94 {
95 return;
96 }
97 s_ssh_comp.comp[i].s2c_alg = alg;
98}
99
100void ssh_comp_set_c2s(uint8_t i, SshCompAlg alg)
101{
102 if (i >= MAX_SSH_CONNS)
103 {
104 return;
105 }
106 s_ssh_comp.comp[i].c2s_alg = alg;
107}
108
109// "zlib" (non-delayed) starts both directions at NEWKEYS; "zlib@openssh.com" waits for auth success.
110void ssh_comp_on_newkeys(uint8_t i)
111{
112 if (i >= MAX_SSH_CONNS)
113 {
114 return;
115 }
116 SshCompState *c = &s_ssh_comp.comp[i];
117 if (c->s2c_alg == SshCompAlg::SSH_COMP_ZLIB && !c->s2c_active)
118 {
119 start_s2c(c);
120 }
121 if (c->c2s_alg == SshCompAlg::SSH_COMP_ZLIB && !c->c2s_active)
122 {
123 start_c2s(c);
124 }
125}
126
127void ssh_comp_on_auth_success(uint8_t i)
128{
129 if (i >= MAX_SSH_CONNS)
130 {
131 return;
132 }
133 SshCompState *c = &s_ssh_comp.comp[i];
134 if (c->s2c_alg == SshCompAlg::SSH_COMP_ZLIB_DELAYED && !c->s2c_active)
135 {
136 start_s2c(c);
137 }
138 if (c->c2s_alg == SshCompAlg::SSH_COMP_ZLIB_DELAYED && !c->c2s_active)
139 {
140 start_c2s(c);
141 }
142}
143
144bool ssh_comp_s2c_active(uint8_t i)
145{
146 return i < MAX_SSH_CONNS && s_ssh_comp.comp[i].s2c_active;
147}
148
149int ssh_comp_s2c(uint8_t i, const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, size_t *out_len)
150{
151 if (i >= MAX_SSH_CONNS || !s_ssh_comp.comp[i].s2c_active)
152 {
153 return -1;
154 }
155 return ssh_deflate_packet(&s_ssh_comp.comp[i].z, src, src_len, dst, dst_cap, out_len);
156}
157
158bool ssh_comp_c2s_active(uint8_t i)
159{
160 return i < MAX_SSH_CONNS && s_ssh_comp.comp[i].c2s_active;
161}
162
163int ssh_comp_c2s(uint8_t i, const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, size_t *out_len)
164{
165 if (i >= MAX_SSH_CONNS || !s_ssh_comp.comp[i].c2s_active)
166 {
167 return -1;
168 }
169 return ssh_inflate_packet(&s_ssh_comp.comp[i].inf, src, src_len, dst, dst_cap, out_len);
170}
171
172#endif // PC_ENABLE_SSH_ZLIB
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
SSH per-connection compression owner (server-to-client zlib / zlib@openssh.com).
SSH client-to-server decompression: a resumable, context-takeover INFLATE (no heap).
SSH server-to-client compression: a context-takeover DEFLATE stream (no heap).