Layer: L5 Session ยท Build flags: PROTOCORE_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):
server.listen(22, ProtoConn::PROTO_SSH);
int32_t result = server.begin();
protocore_ssh_conn_setup();
The host key lives in NVS, not in RAM. protocore_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:
if (protocore_ssh_rsa_load_pubkey() != 0) {
Serial.println("No SSH host key in NVS - see docs/SSH.md (Host key provisioning)");
return;
}
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:
protocore_ssh_channel_set_data_cb(ssh_on_data);
void protocore_ssh_auth_set_pubkey_cb(uint8_t *work)
void protocore_ssh_auth_set_password_cb(uint8_t *work)
Channel echo. The data callback receives the channel id the bytes arrived on and sends them back with protocore_ssh_conn_send(slot, channel, data, len) - the skeleton for a remote console. With PROTOCORE_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 PROTOCORE_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 protocore_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 protocore_ssh_forward_policy(const char *host, uint16_t port) {
return port == 80 || port == 443;
}
protocore_ssh_forward_set_policy_cb(protocore_ssh_forward_policy);
protocore_ssh_forward_begin();
Then ssh -L 8080:example.com:80 admin@<ip> and curl localhost:8080 reaches example.com through the board.
Hardening. Define PROTOCORE_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
PROTOCORE_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=-DPROTOCORE_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=-DPROTOCORE_ENABLE_SSH=1 -DPROTOCORE_SSH_PORT_FORWARD=1 -DPROTOCORE_SSH_MAX_CHANNELS=4 -DPROTOCORE_CLIENT_CONNS=3 -DPROTOCORE_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 PROTOCORE_ENABLE_SSH 1
#include "protocore.h"
#include "network_drivers/presentation/ssh/auth/ssh_auth.h"
#include "network_drivers/presentation/ssh/connection/ssh_channel.h"
#include "network_drivers/presentation/ssh/connection/ssh_conn.h"
#include "network_drivers/presentation/ssh/connection/ssh_forward.h"
#include "network_drivers/tls/ssh_rsa.h"
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
PC server;
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)
{
protocore_ssh_conn_send(slot, channel, data, len);
}
#if PROTOCORE_SSH_PORT_FORWARD
static bool protocore_ssh_forward_policy(const char *host, uint16_t port)
{
return port == 80 || port == 443;
}
#endif
void setup()
{
Serial.begin(115200);
Physical.wifi->init(SSID, PASSWORD);
Serial.print("Connecting to WiFi");
while (!Physical.wifi->ready())
{
delay(250);
Serial.print('.');
}
uint32_t ip = Physical.link->egress_ip();
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
if (protocore_ssh_rsa_load_pubkey() != 0)
{
Serial.println("No SSH host key in NVS - see docs/SSH.md (Host key provisioning)");
return;
}
protocore_ssh_channel_set_data_cb(ssh_on_data);
server.listen(22, ProtoConn::PROTO_SSH);
int32_t result = server.begin();
if (result < 0)
{
Serial.printf("begin() failed (error %d)\n", result);
return;
}
protocore_ssh_conn_setup();
#if PROTOCORE_SSH_PORT_FORWARD
protocore_ssh_forward_set_policy_cb(protocore_ssh_forward_policy);
protocore_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()
{
server.handle();
}
Layer 1 (Physical) - link bring-up, the interface registry, and live egress reporting.