Zephyr can now carry real-time media over IP: native RTP was merged in #104603,
SRTP is under review (#118474), and #100390 collected the use cases (VoIP/SIP,
WebRTC, RTSP/ONVIF cameras). All of these assume the two endpoints can reach
each other directly. In real deployments they usually cannot: the device sits
behind a home NAT and the viewer is on a cellular network behind carrier-grade
NAT.
NAT traversal is built in layers:
STUN (RFC 8489) lets a device learn its public (server-reflexive)
address and carries the connectivity checks between two peers;
TURN (RFC 8656) extends STUN with relaying through a server when no
direct path exists;
ICE (RFC 8445) combines both to find a working path.
This stack is the NAT traversal layer of WebRTC and SIP/VoIP and of ecosystem
camera protocols. For example, in the Matter 1.5 Camera device type the
controller can pass STUN/TURN servers to the camera in the WebRTC session
offer.
Zephyr has no STUN implementation today, neither in tree nor as a module. An
application that needs its public address, or wants to run ICE, has to bring
its own STUN code.
Proposed Change (Summary)
Add a small STUN library to the networking stack (subsys/net/lib/stun,
<zephyr/net/stun.h>, CONFIG_STUN):
Message codec per RFC 8489 (compatible with RFC 5389):
parse and build any method and class, with generic attribute lookup;
MESSAGE-INTEGRITY with short-term credentials (HMAC-SHA1);
FINGERPRINT, XOR-MAPPED-ADDRESS (IPv4 and IPv6), ERROR-CODE, USERNAME,
SOFTWARE;
the ICE attributes of RFC 8445.
Binding client: a transport-agnostic transaction for discovering the
server-reflexive address, with the RFC 8489 retransmission schedule.
RFC 7983 demultiplexer, so STUN can share one UDP socket with DTLS and
RTP/RTCP.
A sample that prints the device's public address.
The library uses the PSA Crypto API only and does no I/O by itself. It does
not allocate memory dynamically; the only exception is the crypto backend.
A preliminary implementation exists. It runs on hardware and has been
tested there, but it still needs a thorough review and will certainly
change on the way upstream. It is IPv4-only today; IPv6 is added for the upstream version. It runs on a test
Matter Camera device with Zephyr main, where it carries the ICE
connectivity checks of every WebRTC session. That includes live video to a
phone on a cellular network.
This is step 1 of three:
STUN — this RFC.
TURN client (RFC 8656). It is already implemented and working on the
same test device, currently IPv4-only. It will be proposed after STUN,
extended to IPv6.
ICE agent (RFC 8445), on top of both.
Proposed Change (Detailed)
Background: the existing implementation
The code was written as the NAT traversal layer of a WebRTC stack for a
test Matter Camera device. It is plain C, Apache-2.0, and runs on Zephyr
main with mbedTLS 4 / TF-PSA-Crypto.
The STUN codec is built into images for three SoCs:
ESP32-P4 (RISC-V), the test Matter Camera;
STM32N6 (Cortex-M55), an ICE-lite test camera;
STM32H750 (Cortex-M7), the controlling ICE side of a test display panel.
Validation on hardware (test Matter Camera on ESP32-P4, Matter 1.5
camera clusters, WebRTC signaling over Matter). In every session the camera sends and answers STUN
connectivity checks (short-term MESSAGE-INTEGRITY, FINGERPRINT, PRIORITY,
USE-CANDIDATE, ICE-CONTROLLING/CONTROLLED). It learns its server-reflexive
address with Binding requests, and STUN, DTLS and SRTP share one UDP socket
through the RFC 7983 demultiplexer.
Phone on a cellular network (SmartThings app), camera behind a home NAT:
the connectivity checks succeed through the phone's relay;
media flows about 2.3 s after the offer, and the video plays on the phone.
Remote WebRTC viewer (aiortc) on a public cloud VM, signaling through the
SmartThings cloud:
the selected pair is the viewer's relay to the camera's server-reflexive
address discovered by the Binding client;
720p at 25 fps with no packet loss.
LAN sessions with the SmartThings app and aiortc.
Unit tests (ztest, native_sim). Part of the code is covered by unit
tests: the codec (including the RFC 5769 test vectors §2.1 and §2.2), the
Binding client, and the ICE agent suites that exercise the codec in full
sessions. The code will nevertheless need a serious review.
Method/class helpers, generic attribute lookup, finishing a message without MI
exist, in the TURN extension file
moved into the core codec
Binding client
exists inside the ICE agent, validated on hardware
decoupled from ICE types, moved into the library
RFC 7983 demultiplexer
exists, validated on hardware
kept
FINGERPRINT CRC-32
local implementation
crc32_ieee() from <zephyr/sys/crc.h>
Sample, documentation page
—
new
Attributes after MESSAGE-INTEGRITY
parsed like any other attribute
ignored, except MESSAGE-INTEGRITY-SHA256 and FINGERPRINT (RFC 8489 §9): the HMAC does not cover them
Repeated attributes
the last occurrence wins
the first occurrence is used (RFC 8489 §14)
Unknown comprehension-required attributes
skipped silently
reported by the parser; the Binding client discards such a response and fails the transaction (RFC 8489 §6.3.3 / §6.3.4)
The upstream changes will first be made in the test device's firmware,
validated on the hardware again, and then submitted as a PR. The code
proposed is therefore the code that runs on the test device.
Design principles
Transport-agnostic. The library never opens sockets or starts timers.
The caller:
provides the buffers;
feeds received datagrams in;
sends what the library asks it to send;
calls it again when the deadline it reported expires.
An ICE agent needs this, because it must send STUN from the same socket
that carries DTLS and SRTP. It also makes every protocol path testable on
native_sim with a fake clock.
No dynamic allocation in the library. State is owned by the caller.
The exception is the crypto backend: in the default mbedTLS configuration,
importing the PSA key for each MESSAGE-INTEGRITY computation takes an
allocation from the mbedTLS heap. Parsed values are returned as pointers
into the caller's buffer. The builder writes into a caller-provided buffer with a sticky
error, so a failed append never leaves a half-written attribute.
PSA Crypto for HMAC-SHA1. Platforms with PSA hardware drivers use them
automatically.
Transaction ids come from the caller. The Binding client takes a
caller-supplied id source, so the application picks its random generator.
RFC 8489 §5 asks for cryptographically random ids; the sample will use
sys_csrand_get().
Defensive parsing. The parser handles:
bounds checks on every attribute and a 4-byte aligned message length;
fixed lengths for MESSAGE-INTEGRITY and FINGERPRINT;
duplicate ICE role attributes, rejected as malformed.
attributes after MESSAGE-INTEGRITY, ignored except MESSAGE-INTEGRITY-SHA256
and FINGERPRINT (RFC 8489 §9) — see the table above for today's state.
The Binding client accepts a response only with its transaction id, from
the server's address, and with a valid XOR-MAPPED-ADDRESS. An off-path
datagram can neither end the transaction nor plant a false address.
Retransmission follows RFC 8489 §6.2.1: RTO 500 ms, Rc = 7, Rm = 16 by
default, configurable per transaction. Failures are reported as timeout,
error response (with the ERROR-CODE value) or cancelled.
Kconfig:CONFIG_STUN and the standard log level options. It depends on
PSA_CRYPTO, with PSA_WANT_ALG_HMAC, PSA_WANT_KEY_TYPE_HMAC and
PSA_WANT_ALG_SHA_1.
Sample and tests
samples/net/stun: sends a Binding request to a configurable STUN server
from a UDP socket and prints the device's public address. It is a useful
diagnostic on its own and the first in-tree user of the library.
tests/net/lib/stun:
RFC 5769 vectors §2.1, §2.2 and §2.3 (IPv6);
byte-exact builder output and builder overflow;
parser strictness and leniency (malformed, truncated and duplicate
attributes);
ICE role attributes, ERROR-CODE, demultiplexer boundaries;
Binding client: retransmission schedule, timeout, error response,
responses with a wrong transaction id or from a wrong source ignored,
cancel.
Next steps (separate proposals)
TURN client (RFC 8656). Already implemented and working on the same
test device, currently IPv4-only:
allocations with long-term credentials on the SmartThings ecosystem TURN
server, a public third-party TURN server and coturn;
video relayed through the device's own allocation;
part of the code is covered by unit tests against a fake TURN server.
It will be proposed after STUN is merged. It will extend the codec with
the TURN attributes, the long-term credential mechanism and ChannelData,
and it will be ported to IPv6 and to struct net_sockaddr like STUN.
The questions that belong only to TURN are left for that proposal: MD5 for
the long-term key, the socket runtime, and TCP/TLS to the server.
ICE agent (RFC 8445), both roles, ICE-lite and trickle ICE (RFC 8838),
on top of STUN and TURN.
Dependencies
PSA Crypto API (TF-PSA-Crypto / mbedTLS 4): PSA_WANT_ALG_HMAC,
PSA_WANT_KEY_TYPE_HMAC and PSA_WANT_ALG_SHA_1. No MD5 in this step: the long-term credential
mechanism comes with TURN.
Networking: none for the library itself. The sample uses a UDP socket
and optionally DNS.
No changes to existing APIs. Purely additive, disabled by default.
Relation to RTP/SRTP (#104603, #118474): independent. The demultiplexer
lets an application run STUN on the same socket as its media, as WebRTC
does.
Thread safety of the crypto backend: the codec calls PSA from the
caller's thread. Applications that use PSA from several threads at once
(for example STUN checks alongside TLS) need thread-safe mbedTLS, which
Zephyr does not provide today. We will propose a Zephyr threading backend
for mbedTLS separately, as an enhancement to modules/mbedtls.
Concerns and Unresolved Questions
Placement and naming:subsys/net/lib/stun, <zephyr/net/stun.h>,
CONFIG_STUN and the stun_ prefix. Is this acceptable, or should it
live elsewhere or carry a net_ prefix?
API style. The library is transport-agnostic by design, for ICE.
Should it also offer a simple blocking helper for applications that only
want their public address, such as
stun_get_mapped_address(server, timeout) over its own socket? Or should
that stay in the sample?
Unknown comprehension-required attributes. Today the parser skips
them silently, which RFC 8489 does not allow: a client discards a
response that carries them and the transaction fails (§6.3.3, §6.3.4),
and a server rejects such a request with 420 and UNKNOWN-ATTRIBUTES
(§6.3.1). The upstream version fixes the client side (see the table).
Open question for the responder side: should the parser collect the
offending attribute types, so an ICE agent can build the 420 response
with UNKNOWN-ATTRIBUTES, or does that belong to the ICE proposal?
Long-term credentials (RFC 8489 §9.2) are only needed by TURN in
practice. Is it acceptable to add them together with the TURN client
rather than now?
Scope. The library covers the client side and the Binding responses
an ICE agent sends. A standalone STUN server is out of scope.
Alternatives Considered
Submit STUN, TURN and ICE together. Rejected for reviewability. STUN is
the foundation; it runs and has been tested on hardware, although it still
needs review and will certainly change. TURN and ICE bring their own
questions (MD5, a socket runtime, TCP/TLS, the agent's threading) that
should not block the codec.
An external module. Zephyr's module guidelines ask for code written
for Zephyr to be contributed to the main tree.
Third-party libraries integrated as external modules. They bring their
own networking, threading and crypto layers instead of Zephyr's.
Leave it to applications: every media application reimplements the
same security-sensitive parsing and integrity code, and none of it is
shared or reviewed.
Build STUN into the RTP transport: too narrow. ICE, SIP and non-RTP
users need STUN independently of RTP.