zpinger 0.7.0

Async, protocol-agnostic latency probe library: TCP / UDP / HTTP(S) / WebSocket / DNS / MQTT / gRPC / HLS / TLS / NTP / STUN / TURN / RTSP / RTMP / QUIC
Documentation

zpinger

Async, protocol-agnostic latency probe library. Every protocol implements the same Pinger trait — call ping().await and get real round-trip time across the application layer, not just whether a TCP socket opens.

Powers the knockknock CLI; also usable directly from any Rust async application.

Supported protocols

Struct Schemes Measures
TcpPinger host:port TCP connect + 1-byte probe + read
UdpPinger host:port UDP send + recv from ephemeral local socket
HttpPinger http://, https:// Full HTTP/1.1 request + status-line validation
WebSocketPinger ws://, wss:// RFC 6455 upgrade + control PING/PONG round trip
DnsPinger host or host:port (default port 53) UDP query + response validation (ID / QR / RCODE / question echo)
MqttPinger mqtt://, mqtts:// (3.1.1 default; v5 opt-in) CONNECT/CONNACK + PINGREQ/PINGRESP + DISCONNECT
GrpcPinger grpc:// / http:// plaintext, grpcs:// / https:// TLS grpc.health.v1.Health/Check unary RPC
TlsPinger host[:port] or https:// (default port 443) TCP connect + TLS handshake (no application data)
NtpPinger host or host:port (default port 123) NTP v4 client packet + server-mode response validation
StunPinger host or host:port (default port 3478) UDP Binding Request + Binding Success Response
TurnPinger host or host:port (default port 3478) UDP Allocate Request + expected 401 Unauthorized reply
RtspPinger rtsp://, rtsps:// TCP + RFC 2326 OPTIONS request + RTSP/1.0 200 validation
RtmpPinger rtmp://, rtmps:// TCP + Adobe RTMP §5.2.1 simple handshake (C0/C1/S0/S1/S2/C2)
QuicPinger quic://, https://, or host:port (port 443) UDP + RFC 9000 QUIC v1 handshake (TLS 1.3 + ALPN agreement)

TLS for https:// / wss:// / mqtts:// / grpcs:// is handled by rustls with the Mozilla root CA bundle from webpki-roots — pure Rust, no system trust store dependency.

Install

[dependencies]
zpinger = "0.7"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

zpinger requires a tokio runtime — the Pinger trait is async, so your application needs to be async too. Any tokio runtime works (current-thread or multi-thread).

Pick only the protocols you need

Every protocol is its own Cargo feature. The default is all, which matches pre-0.6 behavior — upgrade and you keep getting everything. But if you only want, say, TCP probing in an embedded-style binary, opt out of the default and pick what you need:

# TCP / UDP / DNS only — no TLS, no HTTP stack, no tonic.
zpinger = { version = "0.7", default-features = false, features = ["tcp", "udp", "dns"] }
# HTTPS but no gRPC.
zpinger = { version = "0.7", default-features = false, features = ["http"] }
Feature Pinger struct(s) exposed What pulls in
tcp TcpPinger nothing extra (tokio is always there)
udp UdpPinger nothing extra
dns DnsPinger, RecordType nothing extra
http HttpPinger, HttpMethod rustls + tokio-rustls + webpki-roots
ws WebSocketPinger http TLS + tokio-tungstenite + futures-util
mqtt MqttPinger, MqttVersion http TLS (shared)
hls HlsPinger http TLS (shared)
grpc GrpcPinger, GrpcStreamPinger tonic + tonic-health + (tonic's own TLS stack)
tls TlsPinger http TLS (shared)
ntp NtpPinger nothing extra
stun StunPinger nothing extra
turn TurnPinger nothing extra (shares STUN's packet builder internally)
rtsp RtspPinger http TLS (shared) for rtsps://
rtmp RtmpPinger http TLS (shared) for rtmps://
quic QuicPinger quinn + (its own rustls-ring TLS stack)
all all of the above all of the above

The Pinger trait, timed, resolve, and the URI parser are always compiled regardless of which features you pick — they're the crate's core surface.

Quick start

use std::time::Duration;
use zpinger::{Pinger, TcpPinger, timed};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let p = TcpPinger::new("example.com:80").with_timeout(Duration::from_secs(2));
    let elapsed = timed(&p).await?;
    println!("TCP RTT: {elapsed:?}");
    Ok(())
}

The Pinger trait

#[async_trait::async_trait]
pub trait Pinger: Send + Sync {
    async fn ping(&self) -> std::io::Result<()>;
}

Ok(()) means the protocol-level exchange completed; Err carries the underlying I/O or protocol error. Use zpinger::timed(pinger) for the elapsed time of a single ping. The trait is object-safe via async-trait, so Box<dyn Pinger> works for heterogeneous dispatch.

Per-protocol examples

TCP

use zpinger::{Pinger, TcpPinger};

let p = TcpPinger::new("example.com:80");
p.ping().await?;

UDP

use zpinger::{Pinger, UdpPinger};

let p = UdpPinger::new("203.0.113.1:5353");
p.ping().await?;

HTTP / HTTPS

use zpinger::{HttpMethod, HttpPinger, Pinger};

// Plain HTTP GET
HttpPinger::new(HttpMethod::Get, "http://example.com/")
    .ping()
    .await?;

// HTTPS POST — TLS handled automatically via the scheme
HttpPinger::new(HttpMethod::Post, "https://api.example.com/v1/echo")
    .ping()
    .await?;

WebSocket / WSS

use zpinger::{Pinger, WebSocketPinger};

// ws:// runs the RFC 6455 upgrade + a control PING/PONG round trip
WebSocketPinger::new("ws://localhost:8080/")
    .ping()
    .await?;

// wss:// reuses the rustls + webpki-roots TLS stack
WebSocketPinger::new("wss://echo.websocket.events/")
    .ping()
    .await?;

DNS

use zpinger::{DnsPinger, Pinger, RecordType};

DnsPinger::new("8.8.8.8", "example.com")
    .with_record_type(RecordType::Aaaa)
    .ping()
    .await?;

MQTT (3.1.1 default, MQTT 5 opt-in)

use zpinger::{MqttPinger, MqttVersion, Pinger};

// Plain mqtt:// (default port 1883), MQTT 3.1.1
MqttPinger::new("mqtt://broker.hivemq.com")
    .ping()
    .await?;

// mqtts:// (default port 8883) with MQTT 5 + custom client id
MqttPinger::new("mqtts://broker.example.com:8883")
    .with_client_id("my-client")
    .with_version(MqttVersion::V5)
    .ping()
    .await?;

gRPC

Calls the standard gRPC Health Checking Protocol grpc.health.v1.Health/Check unary RPC. Reports success when the server returns SERVING.

use zpinger::{GrpcPinger, Pinger};

// Plaintext H2C
GrpcPinger::new("grpc://localhost:50051")
    .ping()
    .await?;

// TLS via webpki-roots default trust
GrpcPinger::new("grpcs://api.example.com:443")
    .with_service("my.package.Service")
    .ping()
    .await?;

TLS handshake only

For monitoring just the TLS handshake (cert validation + ServerHello

  • Finished) without conflating HTTP response time:
use zpinger::{Pinger, TlsPinger};

TlsPinger::new("api.example.com:443")
    .ping()
    .await?;

NTP / STUN / TURN

UDP infra pingers — all share the same shape (host or host:port, default to the protocol's well-known port):

use zpinger::{NtpPinger, Pinger, StunPinger, TurnPinger};

NtpPinger::new("time.cloudflare.com").ping().await?;        // port 123
StunPinger::new("stun.l.google.com:19302").ping().await?;   // port 3478
TurnPinger::new("turn.example.com").ping().await?;          // port 3478

TurnPinger is the unusual one: it sends an unauthenticated Allocate Request and considers the expected 401 Unauthorized Allocate Error Response a successful liveness check (RFC 5766 §6.2 mandates that response). No relay state is allocated server-side, so it's safe to spam against shared TURN infrastructure.

RTSP

use zpinger::{Pinger, RtspPinger};

// rtsp:// runs over TCP/554; OPTIONS is the spec-mandated keepalive
RtspPinger::new("rtsp://camera.example.com:554/")
    .ping()
    .await?;

// rtsps:// runs over TLS/322 (RFC 7826), reuses the rustls layer
RtspPinger::new("rtsps://secure-camera.example.com/")
    .ping()
    .await?;

RTMP

use zpinger::{Pinger, RtmpPinger};

// rtmp:// runs over TCP/1935; just the Adobe §5.2.1 handshake, no
// AMF connect afterwards — that's enough to validate ingest liveness.
RtmpPinger::new("rtmp://ingest.example.com:1935/live")
    .ping()
    .await?;

// rtmps:// runs over TLS/443
RtmpPinger::new("rtmps://secure-ingest.example.com/live")
    .ping()
    .await?;

QUIC

use zpinger::{Pinger, QuicPinger};

// Plain HTTP/3 endpoint with default ALPN h3
QuicPinger::new("https://www.cloudflare.com")
    .ping()
    .await?;

// Custom ALPN list — useful for non-h3 stacks
QuicPinger::new("quic://relay.example.com:8443")
    .with_alpn(vec![b"hq-29".to_vec()])
    .ping()
    .await?;

Heterogeneous dispatch via Box<dyn Pinger>

use std::time::Duration;
use zpinger::{HttpMethod, HttpPinger, Pinger, TcpPinger, timed};

let pingers: Vec<Box<dyn Pinger>> = vec![
    Box::new(TcpPinger::new("example.com:80")),
    Box::new(HttpPinger::new(HttpMethod::Get, "https://example.com/")),
];

for p in &pingers {
    let elapsed = timed(p.as_ref()).await?;
    println!("RTT: {elapsed:?}");
}

TLS configuration

Every TLS-aware pinger (HttpPinger, WebSocketPinger, MqttPinger, GrpcPinger) ships with a sensible default that trusts public CAs via webpki-roots. For self-signed test endpoints, inject a custom config:

use std::sync::Arc;
use zpinger::{ClientConfig, HttpMethod, HttpPinger, Pinger};

// Build whatever rustls ClientConfig you like — e.g. a custom
// trust anchor for a self-signed test endpoint.
let config: Arc<ClientConfig> = build_my_test_config();

HttpPinger::new(HttpMethod::Get, "https://localhost:8443/health")
    .with_tls_config(config)
    .ping()
    .await?;

GrpcPinger uses with_ca_cert(pem_bytes) instead — tonic's TLS config takes a different shape.

Timeouts

Every pinger struct has .with_timeout(Duration). The default is 5 seconds. The whole ping() call respects the timeout (not just each I/O op individually) — if your handshake stalls halfway, you still get the timeout error.

use std::time::Duration;
use zpinger::{Pinger, TcpPinger};

TcpPinger::new("slow.example.com:80")
    .with_timeout(Duration::from_millis(500))
    .ping()
    .await?;

Resolve helper

For showing what the pinger will actually connect to (the CLI uses this for the DNS lookup: ... banner):

let addrs = zpinger::resolve("https://example.com").await;
// ↑ defaults to port 443 because of the https scheme.

resolve returns an empty Vec on failure rather than panicking — the actual pinger surfaces the real error when you call it.

CLI + MCP

If you want to use the same probes from a shell or from an AI agent without writing Rust, install the knockknock CLI. It also ships an optional knockknock-mcp Model Context Protocol server behind the mcp feature.

License

MIT — same as knockknock. See LICENSE.