zpinger 0.5.0

Async, protocol-agnostic latency probe library: TCP / UDP / HTTP(S) / WebSocket / DNS / MQTT / gRPC
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`](https://crates.io/crates/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          |

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

## Install

```toml
[dependencies]
zpinger = "0.4"
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).

## Quick start

```rust
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

```rust
#[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`](https://crates.io/crates/async-trait), so
`Box<dyn Pinger>` works for heterogeneous dispatch.

## Per-protocol examples

### TCP

```rust
use zpinger::{Pinger, TcpPinger};

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

### UDP

```rust
use zpinger::{Pinger, UdpPinger};

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

### HTTP / HTTPS

```rust
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

```rust
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

```rust
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)

```rust
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](https://github.com/grpc/grpc/blob/master/doc/health-checking.md)
`grpc.health.v1.Health/Check` unary RPC. Reports success when the
server returns `SERVING`.

```rust
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?;
```

## Heterogeneous dispatch via `Box<dyn Pinger>`

```rust
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:

```rust
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.

```rust
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):

```rust
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`](https://crates.io/crates/knockknock) CLI. It also
ships an optional `knockknock-mcp` Model Context Protocol server
behind the `mcp` feature.

## License

MIT — same as `knockknock`. See [LICENSE](../LICENSE).