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 address
let 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 latency
let stats = measure_latency(&socket, peer_addr, 10)?;
println!("Average RTT: {:?}", stats.avg);// Discover public address (creates new socket)
pub fn get_public_addr() -> Result<SocketAddr>;
// Discover public address using existing socket (preserves NAT mapping)
pub fn get_public_addr_with_socket(socket: Option<&UdpSocket>) -> Result<SocketAddr>;// Punch a UDP hole to peer (10 second timeout)
pub fn punch_hole(socket: &UdpSocket, peer_addr: SocketAddr) -> Result<()>;// Measure RTT to peer
pub fn measure_latency(socket: &UdpSocket, peer_addr: SocketAddr, count: u32) -> Result<LatencyStats>;
// Respond to incoming pings (for building custom responders)
pub fn respond_to_ping(socket: &UdpSocket) -> Result<Option<SocketAddr>>;
pub struct LatencyStats {
pub min: Duration,
pub max: Duration,
pub avg: Duration,
pub samples: 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