ESP32 — TLS, clock & troubleshooting#
The two things most likely to stop a first connection are the TLS version and the clock. Both are by-design consequences of running mTLS on a microcontroller, and both have a clean fix.
TLS version#
The protocol specifies TLS 1.3, and the relay's agent control channel defaults to requiring it. But ESP32 WiFiClientSecure (mbedTLS) historically negotiates TLS 1.2 — whether it can do 1.3 depends on your Arduino-ESP32 / ESP-IDF version.
Symptom: the handshake fails with a fatal alert — mbedTLS -30592, "SSL - A fatal alert message was received from our peer."
Fix — one of:
- Lower the relay's TLS floor to 1.2 for MCU agents (a relay setting; on managed Tollan, support enables it). This only widens what's accepted — the rest of the fleet keeps negotiating 1.3, and the 1.2 path still uses forward-secret elliptic-curve cipher suites, with no weak-cipher downgrade.
- Or use a recent ESP32 core that can negotiate TLS 1.3, which needs no relay change.
Note
Lowering the TLS floor is a per-relay setting, off by default. On managed Tollan, ask support to enable it for a relay that serves ESP32 agents; if you self-host, it's covered in your deployment runbooks.
Clock#
mTLS verifies the relay certificate, and X.509 verification checks the certificate's validity window. A board with no RTC boots near 1970, so every 2026-dated cert looks "not yet valid."
Symptom: TLS fails with mbedTLS -9984 ("X509 - Certificate verification failed").
Fix: the agent handles this for you. connect() syncs the clock over SNTP before the first TLS handshake and won't attempt TLS until the clock is real — it backs off and retries instead. Tune or disable via TollanConfig:
| Field | Default | When to change |
|---|---|---|
syncClock | true | Set false only if the board already keeps real time (external RTC, or you call configTime() yourself) |
ntpServer1 / ntpServer2 | pool.ntp.org / time.nist.gov | Point at a LAN NTP server on an isolated network with no public NTP access |
clockSyncTimeoutMs | 8000 | Raise on a slow or high-latency link |
You can check readiness in code with agent.clockReady().
WebSocket handshake notes#
The client requires an HTTP 101 Switching Protocols response with the echoed Sec-WebSocket-Protocol: tollan.v1. It does not verify Sec-WebSocket-Accept — the mTLS layer has already authenticated the relay, so the extra check is redundant.
A tollan.v1 version mismatch is terminal: the agent enters TollanState::Terminal and stops, rather than reconnect-looping against a relay it can't talk to. If you see this, the relay is speaking a different protocol version than the library.
Common issues#
| Symptom | Likely cause | Fix |
|---|---|---|
state() never leaves Disconnected, mbedTLS -30592 | Relay requires TLS 1.3; core only does 1.2 | Lower the relay's TLS floor to 1.2, or use a newer core |
mbedTLS -9984 on every attempt | Clock not synced (board thinks it's 1970) | Ensure syncClock is on and NTP is reachable |
| Stuck before TLS, no NTP reply | No route to pool.ntp.org | Set ntpServer1 to a LAN NTP server |
state() goes straight to Terminal | tollan.v1 version mismatch | Match the relay and library versions |
Build warning about tollan_config.h | Bundle header not next to the sketch | Drop the console's tollan_config.h into the sketch folder |
| Connects, but forwarded target refused | Target not on the allowlist | Add the host:port to forwardTargets / defaultTarget; keep it in sync with the route |
| Reboots / heap exhaustion under load | Too many forwarded channels or buffers too large | Lower TOLLAN_MAX_FWD_CHANNELS, tune TOLLAN_FWD_BUF and maxFrameBytes |
Verifying the wire codec#
If you suspect a framing problem, the codec is host-testable off-hardware and checked against the same vectors as the Go and Node sides:
cd embedded/tollan-agent-library/test/native && make
# -> test_protocol: PASS (0 failures)
# -> test_targets: PASS (0 failures)Green here means the C++ port is byte-compatible with the rest of the fleet — so a connection problem is transport/TLS/clock, not framing.
Serial debugging#
Both examples print state transitions to serial at 115200. Watch them while the board boots:
wifi... ok, ip=192.168.1.42
tollan agent started
tollan state -> connectedIf you never see connected, work down the common issues table — it's almost always TLS version or clock on the first bring-up.
Roadmap#
The library is a first cut. Known next steps:
- On-hardware end-to-end validation against a live relay (the transport's remaining gate).
- Enrollment mode: on-device EC keygen + CSR via mbedTLS, so the private key never leaves the board (today it's provisioned via the bundle).
- Live, signed allowlist sync so the board tracks console route edits without a re-flash — today the allowlist is the static bundle set.
- Fragmented-WebSocket-message handling and a configurable receive buffer.