Layer: L5 Session · Build flags: PC_ENABLE_SSH
What this example teaches
This stands up a real SSH 2.0 server (RFC 4253/4252/4254) on port 22: it loads an RSA host key from NVS, authenticates clients by password and/or public key, and echoes channel data back. The crypto runs on the ESP32 hardware accelerator and no connection state touches the heap.
Listening on a non-HTTP protocol. The same PC can host other L5 protocols. You open an SSH listener with server.listen(port, PROTO_SSH) and then begin() (no port argument needed when you have explicitly added listeners):
int32_t result = server.begin();
@ PROTO_SSH
SSH (RFC 4253/4252/4254).
void pc_ssh_conn_setup()
One-time setup: install the dispatcher's binary-packet emit callback.
The host key lives in NVS, not in RAM. pc_ssh_rsa_load_pubkey() loads only the public half at startup; the private key is read per-signature into a stack buffer and wiped, so it is never held in static RAM. You must provision the DER key once per device (namespace ssh_host_key, key priv_der) - see docs/SSH.md:
Serial.println("No SSH host key in NVS - see docs/SSH.md (Host key provisioning)");
return;
}
int pc_ssh_rsa_load_pubkey(void)
Load the public portion of the RSA host key into ssh_host_pubkey.
Auth via callbacks. You decide who gets in. A password callback and a public-key callback are installed before begin(); the server verifies the client's signature itself once your pubkey callback accepts the key:
void pc_ssh_auth_set_pubkey_cb(SshPubkeyCb cb)
Install the publickey-authorization callback (nullptr → all fail).
void pc_ssh_auth_set_password_cb(SshPasswordCb cb)
Install the password-verification callback (nullptr → all fail).
void pc_ssh_channel_set_data_cb(SshChannelDataCb cb)
Install the inbound-data callback (session channels).
Channel echo. The data callback receives the channel id the bytes arrived on and sends them back with pc_ssh_conn_send(slot, channel, data, len) - the skeleton for a remote console. With PC_SSH_MAX_CHANNELS > 1 the client can open several channels over one connection and each is tagged by id.
**TCP port forwarding (ssh -L).** Define PC_SSH_PORT_FORWARD (and give it channel + client-pool room) to let the board act as a local-forward tunnel: when a client opens a direct-tcpip channel, the ssh_forward owner opens the outbound TCP connection through the client transport and bridges bytes both ways. It is opt-in twice over - compiled out by default, and inert until you call pc_ssh_forward_begin() - because any authenticated client could otherwise make the board connect anywhere (an open proxy). Restrict the reachable targets with a policy callback:
static bool pc_ssh_forward_policy(const char *host, uint16_t port) {
return port == 80 || port == 443;
}
pc_ssh_forward_set_policy_cb(pc_ssh_forward_policy);
pc_ssh_forward_begin();
Then ssh -L 8080:example.com:80 admin@<ip> and curl localhost:8080 reaches example.com through the board.
Hardening. Define PC_SSH_ALLOW_PASSWORD 0 to compile password auth out entirely (publickey-only), and failed attempts are bounded by SSH_MAX_AUTH_ATTEMPTS. Use a constant-time compare and real credential storage in production - the demo's strcmp is illustrative only.
Build and run
PC_ENABLE_SSH must reach the library build, so pass it as a build flag:
pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPC_ENABLE_SSH=1" \
--lib="." examples/L5-Session/SSH/SSH.ino
To build with port forwarding, add the forwarding flags:
pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPC_ENABLE_SSH=1 -DPC_SSH_PORT_FORWARD=1 -DPC_SSH_MAX_CHANNELS=4 -DPC_CLIENT_CONNS=3 -DPC_SSH_FWD_MAX=3" \
--lib="." examples/L5-Session/SSH/SSH.ino
Provision a host key first (see docs/SSH.md), then:
ssh -p 22 admin@<ip> # password: s3cret (type; the server echoes it back)
# with forwarding built in:
ssh -L 8080:example.com:80 admin@<ip> # then in another shell:
curl http://localhost:8080/ # reaches example.com:80 through the board
Annotated source
The complete sketch (SSH.ino), reproduced verbatim with added explanatory comments:
#define PC_ENABLE_SSH 1
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
static bool ssh_password_auth(const char *user, const char *pass)
{
return strcmp(user, "admin") == 0 && strcmp(pass, "s3cret") == 0;
}
static bool ssh_pubkey_auth(const char *user, const uint8_t *blob, size_t blob_len)
{
(void)blob;
(void)blob_len;
return strcmp(user, "admin") == 0;
}
static void ssh_on_data(uint8_t slot, uint32_t channel, const uint8_t *data, size_t len)
{
}
#if PC_SSH_PORT_FORWARD
static bool pc_ssh_forward_policy(const char *host, uint16_t port)
{
return port == 80 || port == 443;
}
#endif
void setup()
{
Serial.begin(115200);
Serial.print("Connecting to WiFi");
{
delay(250);
Serial.print('.');
}
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
{
Serial.println("No SSH host key in NVS - see docs/SSH.md (Host key provisioning)");
return;
}
int32_t result = server.
begin();
if (result < 0)
{
Serial.printf("begin() failed (error %d)\n", result);
return;
}
#if PC_SSH_PORT_FORWARD
pc_ssh_forward_set_policy_cb(pc_ssh_forward_policy);
pc_ssh_forward_begin();
Serial.println("SSH port forwarding enabled (ssh -L to ports 80/443)");
#endif
Serial.println("SSH server started on port 22");
}
void loop()
{
}
Single-port HTTP server with deterministic, zero-allocation execution.
int32_t listen(uint16_t port, ConnProto proto=ConnProto::PROTO_HTTP)
Register a port to listen on when begin() is called.
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
void handle()
Drive the server - call every Arduino loop() iteration.
bool init_wifi_physical(const char *, const char *)
Connect to a WiFi access point.
uint32_t pc_net_egress_ip(void)
IPv4 (network byte order) of the current egress interface, or 0 if none.
bool wifi_ready()
True if the WiFi station link is up (associated + an IP is assigned).
Layer 1 (Physical) - link bring-up and live egress-interface reporting.
Layer 7 (Application) - public HTTP routing API.
SSH user-authentication layer (RFC 4252).
SSH connection protocol - multiplexed "session" channels (RFC 4254).
int pc_ssh_conn_send(uint8_t ssh_slot, uint32_t channel, const uint8_t *data, size_t len)
Send application data to the client over an SSH channel.
Glue between the TCP transport (conn_pool) and the SSH protocol stack.
SSH direct-tcpip port-forwarding owner (the ssh -L target side).
SSH RSA host-key layer: NVS-backed host key, host-key signing, and "ssh-rsa" blob encoding.