Documentation
¶
Overview ¶
Package netdiag is a pure-Go, CGO-free network diagnostic engine for probing self-hosted services from an external vantage point (a VPS outside the edge) or from inside the cluster. It exists to answer "where does a flow break?" for services that sit behind a home edge router (a UniFi UDM) and a Cilium LoadBalancer: a Minecraft server, an HTTP file-server, etc.
Everything here is active measurement using only the Go standard library plus golang.org/x/sys/unix (for PMTU discovery). There is no libpcap / gopacket dependency — packet capture, where genuinely needed, is done by shelling out to tcpdump on the host that already has it (see the capture coordinator). This keeps the shipped binary a single static artifact with zero runtime dependencies on the target, which is the whole point: scp and run.
The centerpiece is Download, which classifies *how* a transfer ended (complete / short / reset / stall / timeout / …). That distinction is what separates otherwise-identical "the download is flaky" symptoms into their real causes — a mid-stream RST from an edge conntrack eviction looks nothing like an idle stall from a backing-store read pause, even though a browser reports both as "failed, try again".
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ParseSize ¶
ParseSize parses a byte-count string with an optional k/m/g suffix (case insensitive) into bytes, returning def on empty or invalid input. Shared by the CLI (tcpflow's --size/--rate/... flags) and the reference server's HTTP query parsing (?size=/&rate=...), so both halves speak the same shorthand.
Types ¶
type ConnectResult ¶
type ConnectResult struct {
Addr string `json:"addr"`
OK bool `json:"ok"`
Kind string `json:"kind"` // ok|refused|reset|timeout|no_route|dns_error|error
RTTMs float64 `json:"rtt_ms"`
LocalAddr string `json:"local_addr,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
Err string `json:"err,omitempty"`
}
ConnectResult is the outcome of a single TCP connect attempt.
func TCPConnect ¶
TCPConnect opens a TCP connection to addr ("host:port") and immediately closes it, reporting whether the handshake succeeded and how it failed if not. This is the outside-the-edge sibling of debug-mcp's node-side `probe tcp`: run it from a VPS to answer "does a real internet client get through the UDM to this LoadBalancer, or does the SYN die at the edge?".
type DownloadOpts ¶
type DownloadOpts struct {
// IdleTimeout is the maximum gap between successive bytes before the transfer
// is called a stall. This is what turns "it hung" into a classifiable
// Outcome instead of just an overall timeout. Default 10s.
IdleTimeout time.Duration
// TotalTimeout caps the whole request. Default 120s.
TotalTimeout time.Duration
// SampleInterval controls throughput sampling cadence. Default 500ms.
SampleInterval time.Duration
// Host overrides the HTTP Host header — lets you hit a raw IP (e.g. the
// Cilium gateway LoadBalancer) while presenting the real vhost, so you can
// A/B the same backend with the edge in and out of the path.
Host string
// ServerName overrides TLS SNI (defaults to Host, else the URL host).
ServerName string
// InsecureTLS skips cert verification (needed when hitting an IP directly).
InsecureTLS bool
// ExpectSize, if > 0, lets short-read detection work even when the server
// sends no Content-Length (e.g. chunked). Optional.
ExpectSize int64
// MaxSamples caps the retained throughput series. Default 2000.
MaxSamples int
}
DownloadOpts tunes a single Download. Zero values get sane defaults via withDefaults, so callers can pass DownloadOpts{} and only set what they mean.
func DefaultMatrixOpts ¶
func DefaultMatrixOpts() DownloadOpts
DefaultMatrixOpts returns download options tuned for a large-file matrix: a shorter idle timeout so a stall is caught quickly, and a total cap that still allows a slow-but-real transfer to finish.
type DownloadResult ¶
type DownloadResult struct {
URL string `json:"url"`
Outcome Outcome `json:"outcome"`
HTTPStatus int `json:"http_status,omitempty"`
Bytes int64 `json:"bytes"`
ContentLength int64 `json:"content_length"` // -1 if the server didn't declare one
TTFBMs float64 `json:"ttfb_ms"`
TotalMs float64 `json:"total_ms"`
MeanKBps float64 `json:"mean_kbps"`
MinWindowKBps float64 `json:"min_window_kbps"` // slowest sampled window — the bufferbloat / near-stall tell
RemoteAddr string `json:"remote_addr,omitempty"`
LocalAddr string `json:"local_addr,omitempty"`
ConnReused bool `json:"conn_reused"`
Samples []Sample `json:"samples,omitempty"`
Err string `json:"err,omitempty"`
}
DownloadResult is the full record of one transfer attempt.
func Download ¶
func Download(ctx context.Context, url string, opts DownloadOpts) DownloadResult
Download performs one HTTP GET and reports not just success/failure but the terminal Outcome — the crux of the whole tool. It reads the body with a per-read deadline equal to IdleTimeout, so a silent wedge surfaces as OutcomeStall while a mid-stream RST surfaces as OutcomeReset and a clean early close surfaces as OutcomeShort. Those three are indistinguishable to a browser ("download failed") but point at three different culprits.
type MatrixResult ¶
type MatrixResult struct {
Target MatrixTarget `json:"target"`
Attempts int `json:"attempts"`
Succeeded int `json:"succeeded"`
SuccessRate float64 `json:"success_rate"`
OutcomeCounts map[Outcome]int `json:"outcome_counts"`
MeanKBps float64 `json:"mean_kbps"` // over successful transfers
MedianKBps float64 `json:"median_kbps"` // over successful transfers
MinKBps float64 `json:"min_kbps"` // slowest successful transfer's mean
WorstWindow float64 `json:"worst_window_kbps"` // slowest window across all attempts
Runs []DownloadResult `json:"runs,omitempty"`
}
MatrixResult aggregates repeated Download attempts against one target. The point is the histogram: "18/20 complete, 2/20 reset" localizes an intermittent failure far better than a single pass/fail, and the outcome mix names the cause (reset vs stall vs short).
func RunMatrix ¶
func RunMatrix(ctx context.Context, target MatrixTarget, repeats int, opts DownloadOpts, keepRuns bool) MatrixResult
RunMatrix downloads target `repeats` times sequentially (sequential is deliberate: parallel requests would mask a WAN-upstream bottleneck by sharing it) and aggregates the outcomes. keepRuns controls whether the per-attempt detail (including throughput samples) is retained in the result.
type MatrixTarget ¶
type MatrixTarget struct {
Name string `json:"name"`
URL string `json:"url"`
Host string `json:"host,omitempty"`
ServerName string `json:"server_name,omitempty"`
InsecureTLS bool `json:"insecure_tls,omitempty"`
ExpectSize int64 `json:"expect_size,omitempty"`
}
MatrixTarget names one thing to hammer: a URL, optionally with a Host/SNI override so the same backend can be tested with the edge in or out of the path.
type Outcome ¶
type Outcome string
Outcome is the terminal classification of a probe or transfer. The values are deliberately fine-grained: the diagnostic value is in telling them apart.
const ( // OutcomeComplete: the transfer finished and delivered everything expected // (Content-Length satisfied, or a clean EOF with no declared length). OutcomeComplete Outcome = "complete" // OutcomeShort: a *clean* EOF arrived before Content-Length was satisfied — // the server closed the stream gracefully but early. Points at the origin // (the app/backend), not the network path. OutcomeShort Outcome = "short" // OutcomeReset: the connection was reset (RST / ECONNRESET) mid-stream. // Something actively killed the flow — an edge conntrack eviction, a DPI // engine, or the peer. This is the "connection reset by peer" fingerprint. OutcomeReset Outcome = "reset" // OutcomeStall: bytes stopped arriving and the idle gap exceeded the // threshold with no FIN/RST — the flow silently wedged. Classic backing- // store read pause (e.g. an NFS stall) or a black-hole in the path. This is // the "it hangs, then works if you retry" fingerprint. OutcomeStall Outcome = "stall" // OutcomeTimeout: the overall deadline elapsed. OutcomeTimeout Outcome = "timeout" // OutcomeRefused: connection refused at dial (RST to the SYN, or no listener). OutcomeRefused Outcome = "refused" // OutcomeNoRoute: host/network unreachable. OutcomeNoRoute Outcome = "no_route" // OutcomeDNSError: name resolution failed. OutcomeDNSError Outcome = "dns_error" // OutcomeTLSError: the TLS handshake failed. OutcomeTLSError Outcome = "tls_error" // OutcomeStatusError: an HTTP response arrived with status >= 400. OutcomeStatusError Outcome = "status_error" // OutcomeError: anything not covered above. OutcomeError Outcome = "error" )
type TCPFlowOpts ¶
type TCPFlowOpts struct {
// Dir is "down" (server sends Size bytes, client reads — default) or "up"
// (client sends Size bytes, server reads). "up" is the direction that
// actually failed in the wild: the Minecraft server's own read is what saw
// "Connection reset by peer", not a write.
Dir string
// Size is the number of bytes to transfer.
Size int64
// Rate throttles the sender to at most this many bytes/sec; 0 = unlimited.
// Applies to whichever side is sending (server for "down", client for "up").
Rate int64
// StallAfter/Stall: down-direction only. The server pauses once StallAfter
// bytes have been sent, for Stall duration — used to calibrate stall
// detection against a known pathology.
StallAfter int64
Stall time.Duration
// Abort/AbortAfter: down-direction only. "reset" forces an RST after
// AbortAfter bytes; "close" ends the stream early with a clean FIN.
Abort string
AbortAfter int64
// IdleTimeout is the max gap between successive reads (or, for "up", the
// per-write deadline) before the attempt is classified as a stall.
IdleTimeout time.Duration
// TotalTimeout caps the whole attempt.
TotalTimeout time.Duration
// DialTimeout caps the initial connect.
DialTimeout time.Duration
}
TCPFlowOpts tunes a single TCPFlow attempt.
type TCPFlowResult ¶
type TCPFlowResult struct {
Addr string `json:"addr"`
Dir string `json:"dir"`
Outcome Outcome `json:"outcome"`
Bytes int64 `json:"bytes"`
Requested int64 `json:"requested"`
TotalMs float64 `json:"total_ms"`
MeanKBps float64 `json:"mean_kbps"`
LocalAddr string `json:"local_addr,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
Err string `json:"err,omitempty"`
}
TCPFlowResult is the terminal record of one TCPFlow attempt — the bare-TCP sibling of DownloadResult.
func TCPFlow ¶
func TCPFlow(ctx context.Context, addr string, o TCPFlowOpts) TCPFlowResult
TCPFlow dials addr (the reference server's raw-TCP port, not its HTTP port), sends a one-line request describing the transfer (mirroring the HTTP /drip query-string vocabulary, so the mental model transfers), then either writes Size bytes (Dir "up") or reads them (Dir "down"), classifying the terminal Outcome the same way Download does.
type TLSResult ¶
type TLSResult struct {
Addr string `json:"addr"`
OK bool `json:"ok"`
Kind string `json:"kind"`
HandshakeMs float64 `json:"handshake_ms"`
Version string `json:"version,omitempty"`
CipherSuite string `json:"cipher_suite,omitempty"`
PeerCN string `json:"peer_cn,omitempty"`
DNSNames []string `json:"dns_names,omitempty"`
NotAfter time.Time `json:"not_after,omitempty"`
Err string `json:"err,omitempty"`
}
TLSResult is the outcome of a TLS handshake attempt.
func TLSHandshake ¶
func TLSHandshake(ctx context.Context, addr, serverName string, insecure bool, timeout time.Duration) TLSResult
TLSHandshake dials addr and completes a TLS handshake, reporting negotiated version/cipher and the leaf cert identity. serverName sets SNI (and is what the cert is verified against unless insecure is true). Use insecure to probe reachability/handshake health independently of cert validity — a handshake that fails on cert but completes the exchange still tells you the TLS path is open end-to-end.