A Rust library for peer-to-peer latency measurement using STUN and UDP hole punching.
- Custom STUN implementation - Discovers your public IP:port via RFC 5389
- UDP hole punching - Establishes direct connections between NAT'd peers
- Bidirectional latency measurement - Both peers can measure RTT simultaneously
- No dependencies on external crates (except
randfor transaction IDs)
# On both machines, run:
cargo run --example peer_ping
# Each machine displays its public address# Exchange addresses, then enter the peer's address when prompted# Both must enter addresses within a few seconds of each other════════════════════════════════════════════
Your address: 203.0.113.45:54321
════════════════════════════════════════════
Share this with your peer, then enter their address.
(Press 'q' to quit)
> 198.51.100.22:12345
Connecting to 198.51.100.22:12345...
Punching hole...
Connected to 198.51.100.22:12345!
Measuring latency...
Ping 1: 42.78ms
Ping 2: 40.35ms
Ping 3: 41.22ms
─────────────────────
Min: 40.35ms
Avg: 41.45ms
Max: 42.78ms
cargo run --example peer_pingBoth peers run this command, exchange addresses, and enter them simultaneously.
# On the server (e.g., EC2)
cargo run --example peer_ping -- --listen 9999
# On the client
cargo run --example peer_ping
# Then enter: <server-public-ip>:9999use std::net::UdpSocket;use pingo::{get_public_addr_with_socket, punch_hole, measure_latency};// Bind a socket and discover public addresslet socket = UdpSocket::bind("0.0.0.0:0")?;let my_addr = get_public_addr_with_socket(Some(&socket))?;println!("My public address: {}", my_addr);// Exchange addresses with peer out-of-band, then:let peer_addr = "198.51.100.22:12345".parse()?;// Punch hole (both peers must do this simultaneously)punch_hole(&socket, peer_addr)?;// Measure latencylet stats = measure_latency(&socket, peer_addr,10)?;println!("Average RTT: {:?}", stats.avg);// Discover public address (creates new socket)pubfnget_public_addr() -> Result<SocketAddr>;// Discover public address using existing socket (preserves NAT mapping)pubfnget_public_addr_with_socket(socket:Option<&UdpSocket>) -> Result<SocketAddr>;// Punch a UDP hole to peer (10 second timeout)pubfnpunch_hole(socket:&UdpSocket,peer_addr:SocketAddr) -> Result<()>;// Measure RTT to peerpubfnmeasure_latency(socket:&UdpSocket,peer_addr:SocketAddr,count:u32) -> Result<LatencyStats>;// Respond to incoming pings (for building custom responders)pubfnrespond_to_ping(socket:&UdpSocket) -> Result<Option<SocketAddr>>;pubstructLatencyStats{pubmin:Duration,pubmax:Duration,pubavg:Duration,pubsamples:Vec<Duration>,}STUN Query: Each peer queries a public STUN server (Google's
stun.l.google.com:19302) to discover their public IP:port as seen from the internet.Address Exchange: Peers exchange their public addresses through some out-of-band mechanism (copy/paste, signaling server, etc.).
Hole Punching: Both peers simultaneously send UDP packets to each other's public addresses. This creates NAT mappings on both sides, allowing bidirectional traffic to flow.
Latency Measurement: Once connected, peers exchange timestamped ping/pong packets to measure round-trip time.
- Symmetric NATs: Hole punching may fail with symmetric NATs (~10-15% of networks), which assign different external ports for different destinations.
- UDP only: This library uses UDP. TCP hole punching is significantly more complex and not supported.
- IPv4 only: Currently only supports IPv4 addresses.
src/
├── lib.rs # Public API
├── error.rs # Error types
├── stun/
│ ├── message.rs # STUN message encoding/decoding
│ ├── attributes.rs # XOR-MAPPED-ADDRESS parsing
│ └── client.rs # STUN client
├── punch/
│ └── hole.rs # UDP hole punching
└── ping/
└── latency.rs # RTT measurement
examples/
├── get_public_addr.rs # Simple STUN query example
└── peer_ping.rs # Full P2P latency tool
MIT