rust-stunclient
A lightweight synchronous and async Rust client for STUN (RFC 5389) that resolves a UDP socket's public IP address and port.
Repository Health
Technical Analysis
stunclient is a minimal Rust library that implements the client side of the STUN (Session Traversal Utilities for NAT) protocol defined in RFC 5389, letting an application discover the public-facing IP address and port for one of its own UDP sockets. It builds STUN binding requests with the stun_codec crate, sends them to any conventional STUN server (the public Google server is bundled as a convenient default), retries on timeout, and decodes the XOR-MAPPED-ADDRESS or MAPPED-ADDRESS attribute out of the reply.
The crate exposes both a blocking, std::net::UdpSocket-based API and, behind the default “async” Cargo feature, a Tokio-based async API with the same configuration surface (timeout, retry interval, custom SOFTWARE attribute). It has no dependency on any particular STUN server operator, making it a small building block for NAT traversal, hole punching, and peer-to-peer connectivity code.
What You Get
- A synchronous StunClient built on std::net::UdpSocket for blocking binding requests
- An async StunClient built on tokio, gated behind the default “async” Cargo feature, with the same configuration surface
- Configurable timeout, retry_interval, and SOFTWARE attribute via builder-style setter methods
- A with_google_stun_server() convenience constructor and a just_give_me_the_udp_socket_and_its_external_address() one-liner for quick experiments
- Runnable examples (client.rs, client_async.rs) demonstrating both sync and async usage from the command line
Common Use Cases
- Discovering a host’s public IP:port before establishing a peer-to-peer UDP connection (WebRTC-style signaling, VoIP, game netcode)
- Implementing NAT hole punching by learning the externally mapped address and port of a locally bound UDP socket
- Diagnosing NAT behavior from a script or CLI tool by querying a known STUN server
- Building higher-level NAT traversal or STUN/TURN tooling on top of a small, dependency-light STUN client primitive
Under The Hood
Architecture The crate is a single-file library (src/lib.rs, roughly 300 lines) built around one StunClient struct holding configuration (timeout, retry_interval, stun_server, software) with builder-style setters. Two largely independent code paths implement the actual query: query_external_address (blocking, std::net::UdpSocket, a manual retry loop driven by socket read timeouts) and query_external_address_async (behind the “async” feature, a tokio::select! loop). Both share internal get_binding_request() and decode_address() helpers that wrap the stun_codec crate’s Message/MessageEncoder/MessageDecoder types, and all failure modes funnel into a single #[non_exhaustive] Error enum. There’s no shared trait between the sync and async query paths, so the retry/timeout logic is duplicated rather than abstracted — an acceptable tradeoff at this size, but a maintenance point if the protocol logic grows.
Tech Stack Rust, 2018 edition, published on crates.io. Core dependencies are stun_codec 0.4 (RFC 5389 message encoding/decoding) and bytecodec 0.5 (the encode/decode trait machinery stun_codec is built on), plus rand 0.9 for STUN transaction ID generation. tokio 1.x (features: time, net, macros) is an optional dependency behind the “async” feature, which is also the crate’s default feature; a dev-dependency on tokio’s “rt” feature supports the async test. No build script, ORM, database, or web framework is involved — this is a narrow protocol-client crate with a minimal dependency footprint.
Code Quality The crate has two tests in a #[cfg(test)] module (one sync, one async) that exercise the real query path against the live public Google STUN server rather than a mock — useful as smoke tests but network-dependent and not isolated unit tests. No dedicated tests/ directory and no CI configuration (no .github/workflows) were found in the repository. Error handling is explicit throughout the library code: every fallible operation returns Result<T, Error> with a typed, #[non_exhaustive] Error enum implementing std::error::Error and Display, rather than swallowing failures or using strings. The crate enforces #![deny(missing_docs)], so every public item carries a doc comment; naming follows idiomatic Rust snake_case conventions. The two convenience functions that call .unwrap() internally (with_google_stun_server, just_give_me_the_udp_socket_and_its_external_address) are explicitly documented as prototype/demo-only and “may panic,” which is an honest tradeoff rather than a hidden one.
API Design The public surface is deliberately small and easy to pick up: StunClient::new(stun_server) plus builder-style setters covers the configurable path, while with_google_stun_server() and just_give_me_the_udp_socket_and_its_external_address() let a caller get a working example running in one or two lines. The sync and async methods are named symmetrically (query_external_address / query_external_address_async), which keeps the mental model consistent when switching between blocking and async call sites. Every public item is documented thanks to the crate-level lint, and the README mirrors both usage patterns directly. On the downside, callers must import std::net (and, for async, tokio::net) themselves since the crate doesn’t re-export them, and a couple of error variants (NoAddress(()), Timeout(())) use an odd unit-typed newtype instead of a plain unit variant.