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
[]
= "0.7"
= { = "1", = ["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.
= { = "0.7", = false, = ["tcp", "udp", "dns"] }
# HTTPS but no gRPC.
= { = "0.7", = false, = ["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 Duration;
use ;
async
The Pinger trait
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 ;
let p = new;
p.ping.await?;
UDP
use ;
let p = new;
p.ping.await?;
HTTP / HTTPS
use ;
// Plain HTTP GET
new
.ping
.await?;
// HTTPS POST — TLS handled automatically via the scheme
new
.ping
.await?;
WebSocket / WSS
use ;
// ws:// runs the RFC 6455 upgrade + a control PING/PONG round trip
new
.ping
.await?;
// wss:// reuses the rustls + webpki-roots TLS stack
new
.ping
.await?;
DNS
use ;
new
.with_record_type
.ping
.await?;
MQTT (3.1.1 default, MQTT 5 opt-in)
use ;
// Plain mqtt:// (default port 1883), MQTT 3.1.1
new
.ping
.await?;
// mqtts:// (default port 8883) with MQTT 5 + custom client id
new
.with_client_id
.with_version
.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 ;
// Plaintext H2C
new
.ping
.await?;
// TLS via webpki-roots default trust
new
.with_service
.ping
.await?;
TLS handshake only
For monitoring just the TLS handshake (cert validation + ServerHello
- Finished) without conflating HTTP response time:
use ;
new
.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 ;
new.ping.await?; // port 123
new.ping.await?; // port 3478
new.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 ;
// rtsp:// runs over TCP/554; OPTIONS is the spec-mandated keepalive
new
.ping
.await?;
// rtsps:// runs over TLS/322 (RFC 7826), reuses the rustls layer
new
.ping
.await?;
RTMP
use ;
// rtmp:// runs over TCP/1935; just the Adobe §5.2.1 handshake, no
// AMF connect afterwards — that's enough to validate ingest liveness.
new
.ping
.await?;
// rtmps:// runs over TLS/443
new
.ping
.await?;
QUIC
use ;
// Plain HTTP/3 endpoint with default ALPN h3
new
.ping
.await?;
// Custom ALPN list — useful for non-h3 stacks
new
.with_alpn
.ping
.await?;
Heterogeneous dispatch via Box<dyn Pinger>
use Duration;
use ;
let pingers: = vec!;
for p in &pingers
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 Arc;
use ;
// Build whatever rustls ClientConfig you like — e.g. a custom
// trust anchor for a self-signed test endpoint.
let config: = build_my_test_config;
new
.with_tls_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 Duration;
use ;
new
.with_timeout
.ping
.await?;
Resolve helper
For showing what the pinger will actually connect to (the CLI uses
this for the DNS lookup: ... banner):
let addrs = resolve.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.