Skip to content

Series C — -scan-resolvers subcommand#2

Open
Isusami wants to merge 3 commits into
mainfrom
series-c-scan-resolvers
Open

Series C — -scan-resolvers subcommand#2
Isusami wants to merge 3 commits into
mainfrom
series-c-scan-resolvers

Conversation

@Isusami
Copy link
Copy Markdown
Owner

@Isusami Isusami commented May 12, 2026

Summary

Adds a -scan-resolvers subcommand that probes the configured resolver list outside the normal client startup path and writes surviving resolvers to stdout. Useful for sanity-checking resolvers, scripting, and as a foundation for future -doctor / re-discovery flows.

What changed

  1. C1 — Extract ProbeResolverMTU + ScanOnce (refactor) — splits the inline probe path inside RunInitialMTUTests into a single-resolver primitive (ProbeResolverMTU, decoupled from *Client via an options struct) and a multi-resolver helper (ScanOnce). RunInitialMTUTests now drives ProbeResolverMTU through its existing worker pool so per-completion logging and counter ordering are unchanged.
  2. C2 — -scan-resolvers flag + caller — new -scan-resolvers flag in cmd/client/main.go dispatches (after Bootstrap, before banner / runtime loop) to internal/client/scancmd.go, which loads the resolver list, calls ScanOnce, and writes results to stdout.
  3. C3 — Output format + exit codes — output honors MTU_SERVERS_FILE_FORMAT if set (default {IP}), reusing the existing formatMTULogLine placeholder vocabulary so {IP}, {DOMAIN}, {UP_MTU}, {DOWN_MTU}, etc. all work. Exit codes: 0 = wrote at least one resolver, 2 = empty result (warn on stderr), 1 = setup / I/O failure.

How it works

  • The single-resolver primitive ProbeResolverMTU(ctx, conn, opts) Result is the unit of work — one resolver, one probe outcome. It takes a ProbeResolverMTUOptions struct (rather than *Client) so it can be reused outside the normal client lifecycle. The seam is intentionally narrow: just the per-domain max upload payload, the upload/download probe callables, and an optional transport-opener for tests.
  • The multi-resolver helper ScanOnce(ctx, opts) []Result iterates the candidate resolvers and aggregates per-resolver results in input order, with worker-pool fan-out clamped to [1, len(connections)]. ScanOnce is intentionally side-effect free — no balancer mutation, no logging, no rendering — so callers layer their own post-processing on top.
  • The -scan-resolvers subcommand short-circuits before the normal client startup: it loads config + connections via the existing Bootstrap path, builds the scan options via Client.buildScanOnceOptions, calls ScanOnce, renders the surviving resolvers using MTU_SERVERS_FILE_FORMAT (default {IP}), prints them to stdout, and exits with the documented code.
  • The existing RunInitialMTUTests path now calls ProbeResolverMTU indirectly through runConnectionMTUTest — no observable behavior change for normal client startup.

API layering

  • ProbeResolverMTU (single-resolver primitive) is the one a future -doctor flow (D3) is expected to reuse — each resolver probed once at MIN_UPLOAD_MTU via a fixed-MTU UploadProbe/DownloadProbe instead of the binary-search variants used today.
  • ScanOnce (multi-resolver helper) is what -scan-resolvers (this PR) and any future re-discovery trigger call.
  • The two are kept distinct on purpose so future doctor / re-discovery work doesn't have to drag the wrong abstraction along.

Example usage

# Basic — print one IP per line (default {IP})
./client -config client_config.toml -scan-resolvers

# With a custom format from your config
# (client_config.toml: MTU_SERVERS_FILE_FORMAT = "{IP}")
./client -config client_config.toml -scan-resolvers > resolvers.txt

# Scripting on exit code
if ./client -config client_config.toml -scan-resolvers > resolvers.txt; then
    echo "Found at least one healthy resolver"
fi
# exit 0 = at least one resolver
# exit 2 = empty result (no resolver survived) — warning on stderr
# exit 1 = setup / I/O failure

Test plan

  • go vet ./...
  • go build ./...
  • go test ./...
  • ScanOnce matches an inline ProbeResolverMTU loop on a shared fixture across several worker counts.
  • ProbeResolverMTU produces the expected per-connection result for accepted, upload-rejected, download-rejected, and transport-failure cases.
  • -scan-resolvers writes the expected resolver lines to stdout with the default {IP} format.
  • Custom MTU_SERVERS_FILE_FORMAT roundtrips through the output renderer (placeholders include the per-result {UP_MTU} / {DOWN_MTU} measurements).
  • Blank MTU_SERVERS_FILE_FORMAT falls back to the raw resolver IP.
  • Empty ScanOnce result and all-rejected paths return exit code 2 with a warning on stderr.
  • Write-failure path returns exit code 1 with the underlying error surfaced on stderr.
  • Nil-client guard on ScanResolvers returns exit code 1.

Isusami added 3 commits May 12, 2026 19:37
Splits the inline probe path inside RunInitialMTUTests into a
single-resolver primitive ProbeResolverMTU(ctx, conn, opts) Result
and a multi-resolver helper ScanOnce(ctx, opts) []Result that
iterates over candidate resolvers.

RunInitialMTUTests now drives ProbeResolverMTU through its existing
worker pool (via runConnectionMTUTest) so per-completion logging
and counter ordering are unchanged. ScanOnce is the side-effect-free
multi-resolver entry point that the upcoming -scan-resolvers
subcommand and any future re-discovery trigger will call.

The single-resolver primitive is decoupled from *Client through a
ProbeResolverMTUOptions struct that carries only what it needs:
the per-domain max upload payload, the upload/download probe
callables, and an optional transport-opener seam. This narrow seam
lets the future -doctor per-resolver tunnel-readiness probe (D3)
swap in fixed-MTU upload/download variants without dragging the
whole client lifecycle along.

The pre-existing per-result side effects (balancer mutation, success
log line, accept/reject counter and log) stay inside
runConnectionMTUTest; the optimizeMTUResolvers post-processing stays
inside RunInitialMTUTests. ScanOnce intentionally does not touch any
of them so it can be reused outside the normal client startup path.

The refactor lands slightly above the 150 LOC estimate because
preserving the partial-measurement semantics on rejection and
keeping the worker-pool ordering inside RunInitialMTUTests required
collapsing mtuConnectionProbeResult into ProbeResolverMTUResult and
adding a Client.buildMTUProbeOptions helper plus a
Client.buildScanOnceOptions helper for the upcoming subcommand
caller.

No behavior change. Tests assert ProbeResolverMTU produces the
expected per-connection result for accepted, upload-rejected,
download-rejected and transport-failure cases, and that ScanOnce
matches an inline ProbeResolverMTU loop on a shared fixture across
several worker counts.
Introduces a new -scan-resolvers flag in cmd/client/main.go that
loads the configured resolver list via the existing Bootstrap path,
calls ScanOnce (added in C1), and writes the surviving resolvers
to stdout, one IP per line.

Useful for sanity-checking a resolver list outside the normal client
startup path. The dispatch fires after Bootstrap (so the resolver
catalog is fully loaded into the balancer) but before PrintBanner
and the runtime loop, so no tunnel traffic, ping manager, or async
runtime ever starts in scan mode.

Output format and exit codes are intentionally minimal here: every
accepted result writes "{IP}" to stdout and the command exits 0
on success or 1 if the client cannot be initialized. Wiring of
MTU_SERVERS_FILE_FORMAT and the empty-result / I/O-failure exit
codes lands in the follow-up C3 commit.

The renderer is split into ScanResolvers (high-level entry that
takes a *Client) and an inner scanResolvers (takes pre-built
ScanOnceOptions) so tests can drive the output path with stub
upload/download probes without touching the network or building a
fully-bootstrapped client.
Wires MTUServersFileFormat (TOML key MTU_SERVERS_FILE_FORMAT) into
the -scan-resolvers output renderer; defaults to {IP} (the raw
resolver IP) when unset or trimmed to empty. The renderer reuses
formatMTULogLine so operators get the same placeholder vocabulary
they already use for the MTU success log file ({IP}, {DOMAIN},
{UP_MTU}, {DOWN_MTU}, etc.). Per-result MTU measurements are
spliced onto the Connection copy passed to the formatter so
{UP_MTU} / {DOWN_MTU} resolve to the values just measured by
ProbeResolverMTU rather than stale catalog defaults.

Exit codes (now part of the public contract for scripting):

  0 = wrote at least one surviving resolver
  1 = setup / config-load / I/O failure (warning on stderr)
  2 = empty result, i.e. no resolver survived the scan (warning
      on stderr)

Lets operators script around the scan command and reuse the same
resolver-file format they already use elsewhere.

Tests cover: format roundtrip with a custom MTU_SERVERS_FILE_FORMAT
template; blank-format fallback to {IP}; empty result and
all-rejected paths returning exit code 2 with the warning on
stderr; write-failure path returning exit code 1 with the
underlying error surfaced on stderr; nil-client guard on
ScanResolvers returning exit code 1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant