STUN and connection setup
An application only needs the match hostname and socket API.
This page explains what happens while its connect() call blocks in the system menu.
For exact JSON requests and responses, see the matchmaking HTTP API.
Three different jobs
| Component | Purpose | Carries application data? |
|---|---|---|
| STUN server | Reports the IP address and UDP port it observed for a device's socket. | No |
| Matchmaking API | Lists compatible devices and exchanges the selected pair's UDP endpoints. | No |
| Direct peer transport | Establishes a path between the devices and carries application traffic. | Yes |
The STUN server and matchmaking API can be on different hosts. Neither is a traffic relay. The API's HTTPS connection is separate from the UDP socket used for peer traffic.
1. Open the menu and discover an endpoint
Both applications resolve the same service, such as chess.v1@match.spx, open their chosen socket type,
and call connect(). The special hostname resolves locally to one reserved matchmaking IP.
Connecting to that IP opens the system menu; the application port does not select the service.
The firmware's CONNECT syscall calls a separate matchmaking module through HLCALL. That module saves the application's display state, presents the menu, and asks the controller to perform matchmaking operations. The controller owns the HTTP requests and transport orchestration. On physical hardware, the Wi-Fi co-processor supplies TLS sockets, STUN, and generic UDP/GRE transport; it does not interpret matchmaking services or peer-selection policy.
The system keeps an outer UDP socket open for discovery and the eventual peer connection. For a TCP application, this socket becomes the carrier for the TCP tunnel.
What STUN discovers
A device might send from 192.168.1.20:30000, while its router translates that to
198.51.100.10:41000 on the internet. The device cannot discover this translation just by inspecting
its own network interface. It sends a STUN Binding request, and the server replies with the source
address and port it observed, normally in the XOR-MAPPED-ADDRESS attribute.
Device A Router/NAT STUN server
| Binding request | |
|------------------------->|--------------------------->|
| | source: 198.51.100.10:41000 |
| |<---------------------------|
|<-------------------------| Binding success: |
| 198.51.100.10:41000 |
These addresses are illustrative. STUN does not open a TCP listening port or configure port forwarding. Sending the request creates or refreshes UDP NAT state; the reply tells the device how that socket appears to the STUN server. Some routers use a different mapping for another destination, so this is a candidate peer endpoint, not proof that a peer can reach it.
Spectranext runs STUN before each new address-bearing API request, including waiting refreshes and peer selection. It uses the same UDP socket throughout. An address discovered on a temporary socket that is then closed would not belong to the transport the peer needs to reach. An HTTP retry reuses its original request and address; it does not change the body by running STUN again.
2. Advertise and choose a peer
Each device posts its service, device identifier, optional display name and joining code, transport,
and discovered UDP endpoint to the API. The response lists available peers with the same service and transport.
Opening the menu automatically advertises the device; both applications follow the same connect() flow.
The user can wait or press a peer's number. If that peer requires a joining code, the menu asks for it before submitting the selection. The code does not encrypt or authenticate the transport.
Device A Matchmaking API Device B
| STUN, then advertise | |
|---------------------------------->| |
| Peers list | |
|<----------------------------------| |
| | STUN, then advertise |
| |<----------------------------------|
| | Peers list |
| |---------------------------------->|
| STUN, then connect_to B | |
|---------------------------------->| |
| Connection ID + B endpoint | |
|<----------------------------------| |
| | STUN, then next refresh |
| |<----------------------------------|
| | Connection ID + A endpoint |
| |---------------------------------->|
Selection atomically reserves both devices. A receives B's address in the selection response; B receives A's address on its next refresh. B does not need to select A or confirm a second time. The waiting menu currently refreshes about one second after the preceding refresh completes. Once paired, the endpoints are fixed for that reservation; a changed mapping requires a new attempt.
3. Establish the direct path
Both devices send UDP traffic toward the other's advertised endpoint. This outbound traffic attempts to create the NAT/filter state needed to accept packets from the peer: UDP hole punching. The first packets can be lost while the other endpoint is still waiting for its pairing response. Setup therefore retries within a bounded deadline.
For a TCP match, the controller upgrades the existing UDP socket to GRE/TCP and connects it to the peer endpoint. It retains the socket's discovered UDP mapping. The tunnel exchanges ICMP echo probes inside IP inside GRE inside UDP to check the direct path before the initiating side starts its inner TCP connection. These are tunneled probes, not public internet ICMP pings to the router.
Device A Direct UDP path Device B
| tunneled ICMP probes/replies <---------------------------------> |
| |
| inner TCP SYN ------------------------------------------------------> |
| <----------------------------------------------------- TCP SYN + ACK |
| TCP ACK ------------------------------------------------------------> |
| reliable application byte stream <================================> |
For illustration, A is the inner TCP initiator here. The system deterministically assigns internal TCP
roles from the device identifiers. One tunnel endpoint connects and the other listens internally;
both applications still call connect(). This does not assign game roles or require an application listener.
The wire format, from the application outward, is:
Application → TCP → inner IPv4 → keyed GRE → UDP → outer IPv4
The TCP stack supplies ordering, retransmissions, flow control, and connection shutdown. GRE encapsulates the inner IP packets; UDP carries them across the routers. The GRE key distinguishes the tunnel and is not an encryption key. A normal TCP server at the public endpoint does not understand this transport. Both matched applications use the same tunnel implementation, so their byte-stream protocol needs no changes.
For a UDP application there is no inner TCP/GRE stream: the system establishes the direct UDP peer path and preserves application datagram boundaries. UDP applications remain responsible for any reliability they need.
4. Return to the application
When the transport is ready, the menu restores the application screen and connect() returns success.
The applications exchange data directly; neither the API nor the STUN server carries it.
The controller reports completion to the API afterward as best-effort housekeeping.
Waiting for that HTTP acknowledgement does not delay an already usable application connection.
Applications should handle partial stream reads and writes and supply their own message framing. After finishing, close the socket normally. A failed completion report does not tear down a working peer connection.
When discovery succeeds but connection fails
STUN success proves that the STUN server answered, not that a direct peer path exists. Endpoint-dependent NAT mappings, firewall rules, blocked UDP, or VPN policies can prevent that path. Two devices behind the same router may also fail when the router cannot send traffic addressed to its public endpoint back inside the LAN (NAT hairpinning). With multiple routers, every relevant translation on the advertised path must permit the traffic.
The menu shows setup failures and the application receives an unsuccessful connect().
There is no relay fallback. For controlled LAN tests, a configured local STUN service can return reachable
LAN endpoints; it does not prove public NAT traversal. See local service and runtime configuration.