Documentation menu
Agent CLI reference
Every flag on the routespear-agent binary and every rsctl subcommand, with the defaults and the reasoning behind them.
Most agents are deployed from the install pack the portal generates, which writes a command line for you and wraps it in a service. This page is for the cases where that does not fit: a container image with its own entrypoint, a configuration-management module that owns the unit file, an air-gapped host that will never see the portal, or a foreground run you are watching while you work out why a measurement looks wrong.
Everything here is flags. routespear-agent has no subcommands; it takes
flags only, and a positional argument is refused rather than ignored, because a
mistyped flag that silently became an argument is a deployment that looks
healthy and measures the wrong thing. The companion tool rsctl is the one
with subcommands, and it is covered further down.
Worked examples
A foreground run against a control plane. The configuration arrives over HTTP, the signing key is pinned so that terminating TLS as the control plane is not enough to configure this agent, and enrollment supplies the credential the receiver derives this agent’s identity from:
Linux
./routespear-agent \
-config-url https://ingest.example.com:9080/v1/agent/config \
-config-signing-key "$(cat /etc/routespear/config-signing.pub)" \
-enroll-bootstrap /etc/routespear/bootstrap.json \
-spool /var/lib/routespear/spool \
-ingest ingest.example.com:9443 \
-ingest-ca /etc/routespear/ca.pem \
-log-format textmacOS
./routespear-agent \
-config-url https://ingest.example.com:9080/v1/agent/config \
-config-signing-key "$(cat /etc/routespear/config-signing.pub)" \
-enroll-bootstrap /etc/routespear/bootstrap.json \
-spool /var/lib/routespear/spool \
-ingest ingest.example.com:9443 \
-ingest-ca /etc/routespear/ca.pem \
-log-format textWindows
$dir = "$env:ProgramData\RouteSpear\Agent"
.\routespear-agent.exe `
-config-url https://ingest.example.com:9080/v1/agent/config `
-config-signing-key (Get-Content "$dir\config-signing.pub" -Raw).Trim() `
-enroll-bootstrap "$dir\bootstrap.json" `
-spool "$dir\spool" `
-ingest ingest.example.com:9443 `
-ingest-ca "$dir\ca.pem" `
-log-file "$dir\agent.log" -log-format text-log-file is worth adding on Windows even in the foreground, because it is
required once the same command line is registered as a service: Session 0 has
no console for stderr to reach.
A fully local run with no control plane and no ingest tier at all. The configuration is a file, and every record is written as one JSON object per line. This is the shape to reach for when you want to see what the agent produces before deciding where to send it:
Linux
./routespear-agent -config agent.json -ndjson records.jsonl -log-format textmacOS
./routespear-agent -config agent.json -ndjson records.jsonl -log-format textWindows
.\routespear-agent.exe -config agent.json -ndjson records.jsonl -log-format text-ndjson - writes to stdout instead, which composes with jq. Neither form
needs privilege, a spool, or a network path to anything but the targets.
Finding out what a host will actually give you, before you decide it is worth a placement:
Linux
./routespear-agent -print-capabilitiesmacOS
./routespear-agent -print-capabilitiesWindows
.\routespear-agent.exe -print-capabilitiesThis assembles the agent as far as opening the preferred probe transport and then exits without measuring, so it needs no configuration and no record destination, but it does need to run as the user the agent will run as, since whether a raw socket can be opened is a property of that user rather than of the host.
Identity and labelling
| Flag | Default | What it does |
|---|---|---|
-agent-id |
the hostname | This agent’s identifier for its own bookkeeping. |
-namespace |
empty | Advisory network scope hint (routing, VRF or site) carried on records. |
-labels |
empty | Agent-wide record labels, as key=value pairs separated by commas. |
The identifier defaults to the hostname rather than to a generated value because a generated one would change on every restart, and the agent’s own logs would then be uncorrelatable across exactly the window you most want to read. A host with no resolvable hostname has to be told who it is, and refuses to start until it is.
The identifier does not reach a record. Identity on the wire is derived by the
receiver from the credential the connection authenticated with, which is what
stops one agent from filing records as another. -agent-id is what the agent
calls itself locally, and what -enroll-name and -enroll-fingerprint default
from.
-namespace is a hint and is labelled as one. Two agents in different VRFs can
legitimately measure the same address and get different answers, and the
network scope is how a reader tells those apart; nothing in the agent enforces it.
Configuration
| Flag | Default | What it does |
|---|---|---|
-config |
empty | Path to the configuration document; watched for changes and reapplied atomically. |
-config-url |
empty | Control-plane configuration endpoint, for example https://ingest.example:9080/v1/agent/config. |
-config-poll |
5s |
How often the configuration source is checked for a change. |
-config-signing-key |
empty | Base64-encoded ed25519 public key the served configuration is verified against. Required with -config-url. |
-config-cache |
a file beside -enroll-state |
Where the last verified control-plane configuration is cached. Only meaningful with -config-url. |
One of -config or -config-url is required, and giving both is refused. An
agent with no configuration document has nothing to measure, and defaulting to
an empty one would produce a process that starts cleanly, logs nothing unusual,
and does no work.
-config-url without -config-signing-key is also refused. An unpinned key
would mean anything able to terminate TLS as the control plane could configure
this agent, and the signature is what reduces the control plane to a delivery
mechanism for a document you can verify independently of the transport that
carried it. The key comes from whoever ran
routespear-ingest -generate-config-signing-key.
-config-cache exists for the restart during an outage. Without it, an agent
that restarts while the control plane is unreachable has no configuration and
measures nothing; with it, the last document that passed signature verification
is reapplied and the agent carries on. Only verified documents are ever cached,
so the cache cannot become a way to persist something the agent would have
refused.
The five-second poll default is a compromise. A local file edit should take effect while you are still looking at the terminal, and a stat plus a hash of a document this size costs nothing, while still being long enough that a whole fleet polling a control plane does not need a separate interval.
Enrollment
| Flag | Default | What it does |
|---|---|---|
-enroll-bootstrap |
empty | Path to the bootstrap file the portal Enrollment page (or routespear-admin secret create -bootstrap) writes. The preferred way to supply an enrollment secret. |
-enroll-secret |
empty | Enrollment secret inline, as clientid:secret. |
-enroll-client-id |
empty | Public half of the enrollment secret, when the private half comes from the configured secret backend under the reference routespear-enrollment. |
-control-plane |
from the bootstrap file | Control plane base URL to enroll against. Overrides the endpoint recorded in -enroll-bootstrap. |
-enroll-state |
an enroll directory beside -spool |
Directory holding the issued credential. |
-enroll-name |
-agent-id |
Operator-facing label for this agent’s control-plane record. |
-enroll-fingerprint |
a hash of -agent-id |
Stable local identity the control plane keys this agent’s record on. |
-enroll-insecure-skip-verify |
false |
Do not verify the control plane’s TLS certificate. |
-enroll-bootstrap is preferred over -enroll-secret for one concrete reason:
on Linux any local user can read another process’s argument list from /proc, so
a secret on the command line is a secret shared with every account on the box.
The agent warns rather than refuses, because an inline secret is genuinely the
right shape for a systemd unit using LoadCredential.
Enrollment flags without -ingest are refused, and so is combining them with
-ingest-cert. The first is a half-finished command line: enrollment exists to
authenticate shipping, so a credential that is never presented to anything is
almost certainly not what was meant. The second is worse than redundant,
both would work, and which one authenticated a given push would depend on the
receiver’s TLS policy rather than on anything you wrote down.
Enrollment is lazy, and that is the point
The agent does not block at startup waiting for the control plane. It constructs its enrollment session during assembly, performing no network I/O at all, and the first real enrollment happens inside the credential callback on the shipper’s first push. Everything in between (opening the transport, applying the configuration, scheduling tests, measuring, spooling) proceeds regardless of whether the control plane is reachable.
The alternative would be to enroll at startup and refuse to start if it failed, which for a process whose whole purpose is to measure networks that break is backwards. The host that most needs an agent running is the one that cannot currently reach anything. An agent started in the middle of a partition measures, spools, and enrolls when the partition heals, with no operator intervention and no special case anywhere in the code.
The cost is that a bad enrollment secret is discovered on the first push rather than at startup, so everything checkable locally is checked early instead: the bootstrap file parses, the endpoint is a URL, and an already-expired secret is reported as an error in the startup log rather than left for later.
The issued credential lands in -enroll-state, which defaults to an enroll
directory beside -spool, next to the spool rather than inside it, since the
spool directory is scanned for segment files and an unrelated file in it is an
invitation for a future reader to mistake one for the other. It is written with
owner-only permissions, because that credential is this agent’s identity. With
no -spool at all, -enroll-state becomes required: the credential has to
persist somewhere, or the agent would enroll afresh on every restart and exhaust
its tenant’s enrollment rate limit.
-enroll-fingerprint is worth setting deliberately in one situation. It
defaults to a hash of the agent identifier, which defaults to the hostname, and
the control plane keys the agent record on it, so two hosts presenting the same
value are one agent whose records interleave. A cloned VM image that kept its
hostname is exactly that case, and a cloud instance id or a TPM-derived value is
a better answer where one is available.
-enroll-insecure-skip-verify presents a fleet-wide enrollment secret to
whoever answers, which is why it is for a local stack and tests only. A secret
captured that way enrolls an attacker’s agent into your tenant.
Shipping to ingest
| Flag | Default | What it does |
|---|---|---|
-ingest |
empty | Ingest tier address in gRPC target syntax. Empty disables shipping entirely. |
-ingest-insecure |
false |
Ship without transport security. |
-ingest-ca |
empty | PEM bundle the receiver’s certificate is verified against. |
-ingest-cert |
empty | Client certificate for mTLS. |
-ingest-key |
empty | Private key for -ingest-cert. |
-ingest-server-name |
empty | Override the name verified in the receiver’s certificate. |
-ingest requires -spool. The shipper drains the write-ahead spool rather
than sending records as they are produced, and that indirection is what makes a
delivery outage survivable instead of lossy.
-ingest also requires one of -ingest-ca, -ingest-cert, or an explicit
-ingest-insecure. Measurement data describes the topology of your network,
and shipping it in the clear is a decision rather than a default, so the flag
exists, and you have to write it down. -ingest-cert and -ingest-key go
together, and neither is accepted alone.
-ingest-server-name is for the case where the address you dial and the name in
the receiver’s certificate legitimately differ, such as a load balancer address
or a hostname resolved differently inside the network being measured.
Spool and local output
| Flag | Default | What it does |
|---|---|---|
-spool |
empty | Durable write-ahead spool directory. Required to ship, and survives a crash. |
-spool-max-bytes |
536870912 (512 MiB) |
Cap on the whole spool. |
-spool-fsync |
interval |
Durability policy: always, interval, or never. |
-ndjson |
empty | Optional JSON debug copy: 32 MiB per file, two backups (96 MiB total), or - for stdout. |
At least one of -spool and -ndjson is required, and an agent given neither
is refused at startup rather than defaulted to a discard sink. An agent that
measured correctly and threw the results away looks healthy in every log line it
emits, and its absence would be discovered downstream as missing data with
nothing pointing back at the host.
The spool cap is not optional and not unbounded, because an unbounded spool turns a shipping outage into a full disk, and a full disk is a worse incident than a gap in a time series, since it takes the rest of the host with it.
-spool-fsync interval is the default because per-record fsync costs more
than the durability it buys at this scale: the records at risk are the last
fraction of a second’s worth, and an agent that syncs on every record is an
agent whose measurement cadence is bounded by disk latency. always is there
for a host where losing a second of records genuinely matters, and never for a
throwaway container where the spool is a buffer rather than a durability
mechanism.
-spool and -ndjson compose, but install packs and the development Compose
agent enable only the spool by default. NDJSON is a duplicate debugging stream,
not an upload queue: successful uploads do not remove its records, and the agent
does not replay it.
Updated agents bound file-based NDJSON output to 32 MiB plus two numbered
backups (.1 is newest). Agent diagnostic logs use 10 MiB plus three backups.
Each write stays whole; a diagnostic write larger than its file limit is refused
without suppressing the separate spool write. Restart appends to a file within
its limit. An oversized file or numbered backup from an older version is reduced
to complete recent lines within the limit when opened. Older local diagnostic
history is discarded, including when NDJSON is the only configured sink.
Existing installations must remove -ndjson from their launch arguments to stop
writing the copy. Stop the agent before removing an old debug file; retain its
spool and enrollment directories. The rsctl spool -drain -ndjson command is an
explicit offline export and does not rotate away exported records.
For stdout/stderr, configure retention in the service manager. The development Compose agent definitions use four Docker log files of 10 MiB each; existing containers need recreation for that setting to apply. These diagnostic limits are separate from the spool cap and do not include binaries, browser caches, managed runtimes, or other application files.
Probe transport
| Flag | Default | What it does |
|---|---|---|
-transport |
auto |
Probe transport: auto, none, raw_icmp, linux_udp_recverr, unpriv_dgram_icmp, tcp_ttl, or windows_iphelper. |
-families |
ipv4,ipv6 |
Address families to open transports for. |
auto opens the best transport this host and this user will allow, and reports
what it settled for rather than pretending. Naming a kind explicitly opens that
one and fails if it cannot be opened, which is usually what you want on a host
you care about: auto quietly succeeding with a worse transport is convenient
during evaluation and undesirable in production, where finding out at startup is
better than finding out from a measurement that could not resolve the
difference you were asking about.
none runs everything that needs no probe socket (the check types, the
self-reporting types) which is the right setting for an agent whose job is
HTTP and DNS from a particular vantage point and which has no business holding a
raw socket.
An address family the host does not have is not an error. The transport for it
is simply absent, and what that costs is visible in -print-capabilities and in
the capability report the control plane receives. See
capabilities.
Rate limits and target policy
| Flag | Default | What it does |
|---|---|---|
-max-pps |
200 |
Agent-wide probe rate ceiling, in the runner’s abstract cost units. |
-max-per-target-pps |
20 |
Per-target pacing rate. |
-max-concurrency |
256 |
Cap on simultaneous test executions. |
-max-tests |
2000 |
Cap on accepted test definitions. |
-min-interval |
1s |
Shortest measurement period this agent will accept. |
-allow-targets |
empty | Exhaustive CIDR allow list for measurement targets. Empty means anything not denied. |
-deny-targets |
empty | CIDRs refused even when allowed, added to the built-in list. |
These are the ceilings a served configuration is checked against, and the asymmetry is deliberate: a configuration may lower any of them and can never raise one. A control plane throttling a single agent from the server is a legitimate instruction; a control plane raising an agent’s rate ceiling would be the first move in turning a well-placed measurement fleet into a distributed packet generator.
The -max-pps default of 200 cost units per second is roughly what a few dozen
path tests at a one-minute cadence consume. It is well below what would be
noticed as traffic anywhere, while still being high enough that shedding is
unusual rather than constant.
The ceiling is checked twice, and the reason is worth knowing. A configuration whose declared aggregate demand exceeds the ceiling is refused up front, which is a failure you can act on; the runner’s own budget then holds at admission time regardless of whether the declaration was honest, which is what makes the ceiling safe against a configuration that under-declares its cost. Refusing up front matters because the alternative is an agent that accepts the configuration and then sheds continuously, so every series runs at some unpredictable fraction of its configured rate, which is indistinguishable in stored data from intermittent loss.
-max-per-target-pps exists so that the agent does not trip the ICMP rate
limits it is measuring around. A hop that rations the ICMP it generates looks
lossy to TTL-limited probes and flawless to a direct echo at the same instant,
and an agent probing hard enough to cause that is reporting its own behaviour as
a network fault.
-allow-targets is empty by default because a mandatory allow list cannot be
pre-populated, the addresses an agent should measure are exactly what you are
about to configure. It is worth setting on an agent whose control plane you
treat as less trusted than the host, or on a public-facing agent.
-deny-targets is appended to the built-in deny list rather than replacing it.
Deny is evaluated first and wins, so a broad allow list can be carved out rather
than enumerated around, and narrowing the allow list cannot silently re-permit
something the defaults refuse. The built-in list covers only ranges that are
never a legitimate measurement target:
| Prefix | Why |
|---|---|
224.0.0.0/4, ff00::/8 |
Multicast is not a measurement target, and a probe to one is an amplification vector. |
0.0.0.0/8, ::/128 |
This-network and the unspecified address. |
255.255.255.255/32 |
Limited broadcast: one probe, every host on the segment replies. |
169.254.169.254/32, fd00:ec2::254/128 |
Cloud instance metadata. Never a network measurement, and a control plane able to aim an agent here would be asking it to fetch credentials on its behalf. |
Private ranges are deliberately not denied. Measuring the local gateway, the local subnet, and internal infrastructure is the primary use of this product, and a default that broke it would be turned off wholesale rather than narrowed.
Both lists apply to a resolved address as well as to a configured literal. A hostname cannot be decided when the configuration is read (its addresses are not known until it resolves, and they change afterwards) so the guarded resolver applies the same policy to whatever it actually resolves to. Without that second check, naming a host instead of an address would bypass the allow list entirely.
Adaptive probing and coalescing
| Flag | Default | What it does |
|---|---|---|
-adaptive |
true |
Run adaptive probing: burst a hop whose statistics departed from their own baseline. |
-burst-share |
0.25 |
Fraction of the rate ceiling adaptive probing may draw. |
-coalesce-window |
2s |
How long one measurement may be reused by another test presenting the same coalescing key. 0 disables sharing entirely. |
Adaptive probing spends extra packets on the hop that just became interesting, so a degradation is sampled densely while it is happening rather than at the ordinary cadence that was chosen for a healthy path. Every sample taken inside a grant is marked as burst on the wire, so a consumer reads the density change as sampling rather than as a change in the network.
-burst-share is composed under the rate ceiling rather than added beside it,
and cannot raise it. A quarter is a deliberate minority share: at a half, one
fleet-wide event would halve every ordinary series on the box at the moment
those series are the evidence, and the agent would be reporting a network
problem that is partly its own scheduler.
Coalescing lets dns_check, http_check, tcp_connect, ssh_check,
dhcp_check, pmtu_check and api_flow share one measurement when two tests
present the same coalescing key within the window, instead of measuring twice.
The two-second default is well under every check type’s own minimum interval, so
sharing only ever catches jitter-staggered due times inside the same round and
never spans a meaningful slice of either test’s cadence. Zero disables sharing,
which is always safe and always costs more packets. rsctl check prints the
coalescing key, which is the only place a human can see it.
Secrets
| Flag | Default | What it does |
|---|---|---|
-secrets-dir |
empty | Directory holding one file per credential, named after its reference. |
-secrets-env |
false |
Also resolve credentials from ROUTESPEAR_SECRET_* variables. |
A credential reference in a configuration resolves from somewhere you nominate, never from the configuration itself. That is what keeps a database password out of anything the control plane serves and out of anything that gets logged.
-secrets-dir is the stronger of the two. Environment variables are inherited
by every plugin child process the agent starts, so -secrets-env widens the
blast radius of a credential to processes that have no business holding it; it
exists because some deployment systems have no good way to place files.
With neither configured, every credential reference fails at test construction, so a test type that requires a secret genuinely cannot run on that agent, and says so in its capability report rather than failing per execution.
Capability grants
| Flag | Default | What it does |
|---|---|---|
-allow-insecure-tls |
false |
Permit an http_check or api_flow that asks to skip certificate verification. |
-ca-bundle |
empty | PEM bundle certificates are verified against instead of the platform store. |
-allow-skip-host-key-verify |
false |
Permit an ssh_check that asks to skip host key verification. |
-known-hosts |
empty | Agent-local host key store, as a JSON object mapping host, host:port or [host]:port to accepted SHA256 fingerprints or public key lines. |
-allow-dhcp-lease-consumption |
false |
Permit a dhcp_check in full_dora mode, which takes a real address out of the pool on every execution and releases it immediately. |
-dhcp-lease-grant-reason |
empty | Recorded with the DHCP grant, so an agent found consuming leases can be traced to the decision that allowed it. |
-browser-plugin |
empty | Path to the routespear-browser executable. Empty means no browser test types. |
-browser-chrome-auto-download |
false |
If the plugin finds no Chrome, Chromium, or Edge on the host, fetch and cache a pinned Chrome for Testing build instead of failing. Ignored without -browser-plugin. |
-browser-plugin-user |
empty | Run the browser plugin as this local account or uid instead of the agent’s own. Required on Linux whenever the agent runs as root; on Ubuntu 23.10+ it also needs an AppArmor profile, see below. Refused on Windows. |
-browser-allow-unsandboxed |
false |
Permit Chromium to run with its own sandbox disabled. |
-browser-allow-insecure-tls |
false |
Permit a browser navigation to a host whose certificate does not verify. |
-script-plugin |
empty | Path to the routespear-script executable. Empty means no script test types. |
-script-execute |
false |
Permit the script plugin to run signed bundles. |
-plugin-sandbox-disabled |
false |
Run plugins unconfined, for a deployment that has already confined the agent itself. |
-browser-plugin and -browser-chrome-auto-download can also be set remotely
Every other grant on this page is fixed for the life of an install: the only
way to change it is to edit the command line and restart. The browser plugin
is the one exception, and only through the portal’s Reconfigure dialog on an
already-running agent, which fetches routespear-browser and writes a local
override file next to the agent’s own binary before restarting it, not
through the config poll every agent already refuses to take a grant from.
This works because a Reconfigure browser change is dispatched the same way
“Update agent” and “Restart agent” already are: a one-shot, operator-
authenticated command for one named agent, at the same trust tier those two
already sit at, not the fleet-wide document every agent polls forever. See
the browser plugin’s RUNTIME.md for the override file’s exact location.
An explicit -browser-plugin on this process’s own command line always wins
over that override file, checked before it is ever read. The one asymmetry
this creates: an agent installed with -browser-plugin baked into its
service definition cannot be switched to no browser plugin from Reconfigure
alone; that flag has to come out of the service definition by hand. An
agent installed through the download wizard’s browser picker, which never
puts the flag on the command line, does not have this problem.
A root agent needs -browser-plugin-user
Chromium will not start its sandbox as uid 0; it prints Running as root without --no-sandbox is not supported and exits. That matters because a
bare-metal agent under systemd normally is root: raw sockets, ebpf_flow,
and ebpf_host all need privilege the browser does not. Left alone, every
browser check on such a host fails at launch even though the plugin started
and Chromium is installed.
-browser-plugin-user drops just that one plugin to an unprivileged account,
which is what the plugin’s own container image already does by running as uid
10001:
routespear-agent \
-browser-plugin /opt/routespear/routespear-browser \
-browser-plugin-user routespear-browser
Either a local account name or a bare uid. A uid with no /etc/passwd entry
is accepted, since that is a normal shape for a hardened deployment. 0 is
refused: this flag exists to give up privilege, and root is the state that
makes Chromium refuse in the first place.
The agent handles the consequences rather than leaving them to you, the
plugin’s callback socket is chowned to that account (keeping mode 0700 rather
than being widened, because anything that can reach that socket can forge
records), root’s supplementary groups are dropped, and the auto-download cache
is created and handed over. See the browser plugin’s RUNTIME.md for the
reasoning in full.
Windows refuses this flag rather than ignoring it. Chromium’s sandbox has no objection to the account a Windows service runs as, so it is not needed there, and a plugin quietly running with more privilege than you asked for is the outcome worth failing over.
On Ubuntu 23.10 and later, that is only half of it
These releases set kernel.apparmor_restrict_unprivileged_userns=1, which stops
an unconfined binary from creating the unprivileged user namespace Chromium
builds its sandbox out of. Adding -browser-plugin-user on such a host does not
make browser checks start working; it changes the message they fail with:
FATAL:zygote_host_impl_linux.cc] No usable sandbox!
That is not a regression and not a second unrelated problem. Chromium needs a
sandbox it can construct, and neither uid 0 nor a uid barred from unshare can
construct one; you are seeing the same requirement from the other side.
Grant that one capability with the AppArmor profile the repository ships:
install -m 0644 deploy/apparmor/routespear-browser-chrome \
/etc/apparmor.d/routespear-browser-chrome
apparmor_parser -r /etc/apparmor.d/routespear-browser-chrome
The profile is declared flags=(unconfined), so it adds no confinement of its
own: the browser is left exactly as restricted as before, plus the ability to
build its sandbox. Prefer it to clearing the sysctl, which grants unprivileged
user namespaces to every binary on the host in order to fix one, and to
-browser-allow-unsandboxed, which turns the sandbox off for the one process
whose entire job is loading pages you do not control.
Only a host using -browser-chrome-auto-download needs the profile. Point the
plugin at a distro-packaged browser instead and the package’s own AppArmor
profile already covers it.
Grants can never come from served configuration
This is the single most important behaviour on this page, and it is a structural property of the binary rather than a policy that could be relaxed by a setting.
Each of the behaviours above is expressed as two independent halves. A request lives in a test definition, and is therefore fully controlled by whoever controls the control plane. A grant is a value supplied by the code that builds the test registry, and every grant in this binary comes from the command line above. A request without its grant does not construct a test at all: it is refused with a message naming the grant it needed. There is deliberately no code path from a configuration document, a test definition, or a test’s parameters to any grant, and that invariant is asserted by tests at the assembly level as well as per package.
What this buys you is concrete. A compromised control plane cannot turn your
agent fleet into a remote execution service, because switching on script
execution requires editing the command line on each host. It cannot make your
agents skip certificate verification and thereby make the verified flag on
every HTTPS record meaningless. It cannot start consuming DHCP leases from a
production pool. It cannot point the browser or script plugin at a different
executable, which would be remote code execution written as a configuration
field. Everything dangerous is decided by whoever administers the host, and a
fleet of measurement agents sitting inside private networks is exactly the sort
of instrument where that distinction matters.
-ca-bundle and -known-hosts are grants in everything but name, which is why
they are in this section. A control plane able to supply a trust anchor could
make any certificate or any host key verify, and the verified flag on every
record would then mean nothing. Both are loaded at startup, so an unreadable
bundle is a refusal to start rather than a per-execution failure that would look
like the destination’s fault. A -known-hosts file with no entries is also
refused: an empty store answers “this host is unknown” for everything, which
reads in a log exactly like a store that loaded and silently verifies nothing.
Each flag is spelled as an allowance rather than as a mode, so reading a service
unit’s arguments answers “what has this agent been permitted to do” without
cross-referencing anything. The allowances that are on are also named in the
startup log and in the capability report, which means most of an agent’s grant
configuration can be audited from its own telemetry rather than only by logging
into the host. -print-capabilities prints that list, and prints an explicit
line saying none of them is reachable from a served configuration. Two things
are not in that list: the plugin executable paths, which show up instead in the
report’s plugins section, and -plugin-sandbox-disabled, whose effect is
visible as the sandbox kind in rsctl capabilities.
-browser-allow-unsandboxed deserves its own warning. Chromium’s sandbox is
load-bearing, and disabling it is only defensible in a throwaway container with
a read-only root filesystem, dropped capabilities, and no secrets on disk.
Logging
| Flag | Default | What it does |
|---|---|---|
-log-level |
info |
debug, info, warn, or error. |
-log-format |
json |
json or text. |
-log-file |
empty (stderr) | Rotate logs at 10 MiB per file, retaining three backups (40 MiB total). |
Logs go to stderr or to a file and never into the record stream. An unreachable target is a record, not a log line, and an agent that logged measurements would have its log volume track the health of whatever it watches, so the noisiest possible moment would also be the moment the logs became useless.
JSON is the default because these lines are read by a log pipeline far more
often than by a person. -log-format text is there for the person, and is
worth setting for a foreground run.
-log-file is required when running as a Windows service, because Session 0 has
no console. -install-service fills it in automatically when it is missing.
Windows service
| Flag | Default | What it does |
|---|---|---|
-install-service |
false |
Register this command line as the RouteSpear Agent Windows service, start it, and configure it to run at boot. Requires administrator. |
-uninstall-service |
false |
Stop and remove the RouteSpear Agent Windows service. Files on disk are left in place. |
Both flags are Windows-only. On Linux and macOS they parse and then refuse
with a message pointing at the install-service and uninstall-service scripts
in the install pack, which write a systemd unit or a launchd job. There is no
-install-service behaviour on those platforms to configure.
Giving both flags at once is refused. -uninstall-service needs nothing else
on the command line; -install-service records the rest of it.
Installation stores the current command line as the service’s ImagePath, with
two modifications. The install and uninstall flags are stripped, so the service
does not re-enter the installer on every start. And every flag whose value is a
filesystem path (-config, -config-cache, -ndjson, -spool, -log-file,
-ingest-ca, -ingest-cert, -ingest-key, -enroll-bootstrap,
-enroll-state, -secrets-dir) is rewritten to an absolute path. A Windows
service’s working directory is System32, so a relative -config would open
the wrong file and the agent would fail at boot with nothing on a console to say
why. If -log-file was not given, one is added beside the executable, for the
same reason.
The service is created with automatic delayed start and with restart actions at
five, fifteen and sixty seconds. Installation also adds an inbound ICMPv4 Time
Exceeded firewall rule, because that is the one moment the process is known to
be elevated and because a measurement agent that rewrote the host firewall on
every start is not something you should have to discover. A failure to add the
rule is reported and not fatal: the agent still runs, but Windows drops the
message that TTL-limited probes depend on, so it falls back to IcmpSendEcho2
and cannot sample ECMP. Uninstalling removes the rule again and leaves the
host’s firewall as it was found.
Diagnostics
| Flag | Default | What it does |
|---|---|---|
-version |
false |
Print version information and exit. |
-print-capabilities |
false |
Print the capability report this agent would advertise, then exit without measuring. |
-version prints one line carrying the build version, the platform, and the
record schema version this build speaks. The build version also rides on every
record, so a fleet running a mix of builds can be told apart in the data rather
than only in a deployment tool. A local build honestly reports itself as dev
with no commit.
-print-capabilities is the flag to run before deciding a host is a poor
vantage point. It needs no -config and no record destination, and it prints:
- the version, platform, Go version and build commit;
- the preferred transport, the timing resolution actually achieved on this host, a degradation reason if there is one, and every transport that was graded;
- the per-capability grading;
- every test type this build registered, with whether it is available here, degraded here, or unavailable, and why;
- each plugin and its version, or the reason it is unavailable;
- the agent-local grants that are on, or an explicit statement that none is.
Nothing in that report re-detects anything. The transports, per-family support,
privilege, flow-identifier control and timing profile all come from the single
detection performed at startup, and the test types come from the registry this
build actually populated. Two opinions about whether this host holds
cap_net_raw is exactly the sort of disagreement that would make a capability
report untrustworthy, so there is only ever one.
Exit codes
| Code | Meaning |
|---|---|
0 |
Clean shutdown after a signal, or a diagnostic flag that completed. |
1 |
The agent could not start, or a component failed unrecoverably. |
2 |
The command line, or the local policy on it, is unusable. |
The difference between 1 and 2 is the difference between “restart me” and “a
human has to edit something”, and a supervisor unit is entitled to act on that.
A flag the binary does not recognise, a flag combination that cannot produce a
working agent, and -help all exit 2. A control plane that cannot be reached
is neither: enrollment is lazy and shipping retries, so an unreachable control
plane is not a startup failure.
The agent handles SIGINT and SIGTERM, and SIGTERM specifically because
that is what a container runtime and a service manager send. An agent that only
handled Ctrl-C would be killed hard on every ordinary restart and lose whatever
the spool had not yet synced.
Running the binary with no arguments at all exits 2 and prints a short note
saying that this is the agent rather than an installer, and pointing at the
install pack’s scripts. That exists because the most common way to encounter
this binary is to double-click it.
rsctl
rsctl is the companion CLI. It is how you verify what a host can measure, and
what a particular test definition does, without privileges you do not need, a
control plane, or the ingest tier. It is not a client for a running agent: it
answers questions about the binary and the host it is invoked on.
rsctl -version prints the version and schema version. An unrecognised
subcommand exits 1.
Two of the subcommands (view and adapt) run against a scripted
network rather than a real one, and that is worth being blunt about before you
read their output. They are for understanding what the tooling does with a
diamond, a tunnel, a silent hop, a policing router or a route change. They are
not measuring your production network and their numbers are not about it.
rsctl capabilities
What this host can measure and how well: the timing source and the resolution actually achieved here, every transport graded best-first, the preferred one, the per-capability grading, and every test type this build registered with its requirements and its placement verdict.
| Flag | What it does |
|---|---|
-json |
Print the AgentCapabilities message as JSON. |
-v |
Include capabilities and test types that are fully held. |
By default only the degraded, absent and undetermined capabilities are printed,
because someone running this is usually asking what is wrong, and a list where
eight held capabilities push the one degraded capability off the screen answers a
different question. The report comes from the same code path the agent puts on
the wire, so rsctl cannot agree with your expectation while the agent reports
something else.
rsctl has no secret backend and no plugin supervisor, and says so rather than
inheriting an answer it cannot see. For the grants and plugins of a real agent,
use routespear-agent -print-capabilities.
rsctl context
This host’s own network position: hostname and platform, gateways, the egress address and interface the stack would actually use per family, every interface with its addresses, resolvers and search domains, the associated wireless link, and the connected subnets that are the evidence behind a network scope.
| Flag | What it does |
|---|---|
-json |
Print the LocalContext message as JSON. |
-records |
Print every emitted record as JSON, including events. |
-rounds |
Collections to take, so change detection is observable. Default 1. |
-interval |
Wait between collections. Default 2s. |
Every field this host declines to answer is listed at the end with the mechanism that refused it, which is the point of the command: “this laptop has no gateway” and “this laptop’s route table could not be read” look identical once either is rendered as a blank cell, and they lead to opposite conclusions.
-rounds exists for the vantage change detector. One snapshot cannot
demonstrate anything about change detection, and a genuine change is only
reported on the second consecutive observation of it, so watching two or three
rounds is what makes the pending-versus-confirmed distinction visible.
rsctl health
The self-telemetry an agent ships as its own agent_health record: uptime,
goroutines, heap, timing and clock quality, scheduler counters and lag
percentiles, per-transport probe accounting, the rate ceiling and the draw
against it, the burst budget, spool depth and shipper state, and the
configuration generation in effect.
| Flag | What it does |
|---|---|
-json |
Print the AgentHealth message as JSON. |
-records |
Print the emitted record as JSON. |
Run from rsctl there is no spool and no shipper, and the output says
undeterminable for them rather than zero. That is the record’s central
property rather than a limitation of the command: a spool depth of zero is the
healthiest possible durability reading, an unwatched spool is a blind spot, and
charting the second as the first would make an unmonitored agent look like the
best one in the fleet.
The reason this record matters at all is that without it, “the network degraded” and “the agent was starved” are indistinguishable in stored data. A scheduler running 40% behind produces sparse samples with a widened RTT distribution, which is exactly what a congested link produces.
rsctl run
One path_health definition, executed for the number of cycles you ask for,
printing the hop table per flow, the flow-consistency accounting read back off
the transport, the path change if one occurred, and the Track A metrics.
| Flag | What it does |
|---|---|
-transport |
fake for the scripted network (the default), or auto, raw_icmp, linux_udp_recverr, unpriv_dgram_icmp, tcp_ttl or windows_iphelper for a real one. |
-target |
Destination to trace to. |
-family |
ipv4 or ipv6. Default ipv4. |
-flows |
Concurrent runs per cycle, each on a distinct flow id. |
-first-ttl, -max-ttl |
Lowest and highest hop limit to probe. |
-probes-per-hop |
Probes per TTL position per run. |
-gap-limit |
Consecutive silent hops that end a run. |
-flow-seed |
Seed for flow id allocation; 0 selects a random one. |
-cycles |
Discovery cycles to run. Default 2. |
-no-flow-control |
Make the scripted transport report flow_id_control=none, as windows_iphelper does. |
-records |
Also print the envelopes written to the sink, as JSON. |
-v |
Log the engine’s own diagnostics to stderr. |
Unlike view and adapt, this one takes a real transport. -transport auto
traces to a live destination through the host’s own resolver, with the same
registry, the same dependency set and the same printing as the scripted run, so
the two outputs are comparable line for line. What it is not is an agent: there
is no spool, no shipper, no configuration source, no adaptive stage and no local
target policy. It runs the cycles it was asked for and exits, which is the
difference between a diagnostic and a daemon.
The scripted default is a deliberate choice rather than a limitation. It carries ECMP fan-out from hop 3, an MPLS tunnel at hop 5 with RFC 4950 labels, RFC 5837 interface identification at hop 4, a silent hop at 6, and a route change that lands in the second cycle, every one of them a real behaviour that a naive implementation misreads as a fault. Nothing about a real network is that cooperative.
The flow-consistency block is read back off the transport rather than off the output, because it is the property that separates this from a traceroute and the hop table cannot evidence it: a run whose every probe carried one flow identifier is what makes its hops a path rather than a list of routers.
rsctl check
One execution of an in-process check type against a real target, with the parameters you would put in a configuration. It prints what was constructed, the coalescing key, the plan for the types that have one, and then the metrics, optionally with the Track B detail payload and the published envelope.
| Flag | What it does |
|---|---|
-type |
http_check, dns_check, tcp_connect, api_flow, ssh_check, dhcp_check or pmtu_check. |
-target |
A URL, a host, or host:port, per the type. |
-family |
ipv4, ipv6, or empty where the type sends no packets of its own. Default ipv4. |
-params |
Type parameters as a JSON object. |
-params-file |
Read -params from a file instead. |
-timeout |
Deadline for the whole command. Default 30s. |
-repeat |
Run this many times, one after another. |
-validate |
Construct the check and print what it would do, without measuring anything. |
-detail |
Print the Track B detail payload as JSON. |
-records |
Print the published envelope as JSON. |
-coalesce-window |
With -repeat, reuse a measurement across repeats within this window. 0 measures every repeat. |
-params-file exists because an api_flow parameter document is several steps
long, and quoting one through a shell (PowerShell in particular, which strips
the outer quotes) is where an afternoon goes.
Construction goes through the same registry the agent uses, so a configuration
rsctl accepts is a configuration the agent accepts, and -validate is the
cheap way to find out that a parameter document would be refused. That symmetry
runs the other way too: rsctl grants itself nothing. It refuses
insecure_skip_verify and insecure_skip_host_key_verify for the same reason
the agent does, it refuses a dhcp_check in full_dora mode because that mode
consumes a real lease and the grant for it belongs to an agent’s own command
line, and an ssh_check has to carry its own host_key_fingerprint because
there is no agent-local known-hosts store behind this command.
The answer a check gives here is genuinely local, which is why the command is worth running from the host in question rather than from CI: the resolver, the egress path, the proxy and the trust store are all this host’s.
Two of the plans are worth reading before you run the check at all. A
dhcp_check prints its mode, the client hardware address it will present, and
whether the mode takes a real address out of the pool. A pmtu_check prints
whether this transport can set the do-not-fragment bit, because that is the one
check with a failure mode where it works, publishes a number, and the number is
wrong.
rsctl view
Per-hop health across two windows against the scripted network: loss, latency percentiles, jitter, queueing delay above each hop’s own baseline, the link-delay estimates derived from adjacent hops, the confidence verdict on each row, and a path-change timeline.
| Flag | What it does |
|---|---|
-target |
Destination to measure. |
-family |
ipv4 or ipv6. Default ipv4. |
-flows |
Concurrent runs per discovery cycle. Default 2. |
-rounds |
Per-hop probe rounds per window. Default 15. |
-flow-seed |
Seed for flow id allocation. |
-records |
Also print the health reports and the raw sample batch, as JSON. |
-v |
Log the engine’s own diagnostics to stderr. |
This is a teaching tool and a way to reason about the confidence model, not a measurement of anything you operate. It runs on a controlled clock, because every quantity in the per-hop stage is defined over intervals (a probe every two seconds, a window every thirty, a baseline over fifteen minutes) and the alternatives are to sleep for several minutes or to print statistics gathered over no time at all.
What it is genuinely useful for is the distinction the confidence column encodes. The scripted path has a hop that is really dropping traffic and a hop that forwards perfectly while rationing the ICMP it generates. Those two look identical in a loss column and lead to opposite actions, and seeing the verdict next to the numbers that produced it is faster than reading about it. See the trust model.
rsctl adapt
What adaptive probing decided, again against the scripted network: which hops it bursted, which it refused to burst and why, the trigger and resolve timeline, the probes actually handed to the transport per hop, and the burst budget’s accounting.
| Flag | What it does |
|---|---|
-target |
Destination to measure. |
-family |
ipv4 or ipv6. Default ipv4. |
-rounds |
Per-hop probe rounds per phase. Default 15. |
-pps |
Agent-wide probe rate ceiling, as local policy would set it. Default 200. |
-burst-share |
Fraction of that ceiling adaptive probing may draw. Default 0.25. |
-records |
Also print the granted trigger event, as JSON. |
-v |
Log the detector’s own diagnostics to stderr. |
The run has three phases (clean, then one hop queueing, then recovered) because an anomaly is a departure from a history, and a run that started at the degradation would have nothing to call it a departure from.
Two things in the output are worth understanding before you tune anything. A
trigger is never a claim that a hop is broken; it is a record that the detector
looked, and what it saw. And a hop whose two measurement channels disagree is
never bursted, because that reading says a router is rationing the ICMP it
generates, and more probes into a rationed limiter buy a worse number rather
than a better one. -pps and -burst-share here mirror -max-pps and
-burst-share on the agent, so this is also the cheap way to see what a
particular share would actually permit.
rsctl spool
A spool directory’s depth, segments, oldest owed record and corruption; and with
-drain, delivery of what it holds.
Linux
./rsctl spool /var/lib/routespear/spoolmacOS
./rsctl spool /var/lib/routespear/spoolWindows
.\rsctl.exe spool "$env:ProgramData\RouteSpear\Agent\spool"| Flag | What it does |
|---|---|
-json |
Print the report as JSON. |
-v |
Print per-segment detail for clean segments too. |
-drain |
Deliver the spool’s contents. Requires -to or -ndjson. |
-to |
gRPC target of an IngestService to drain to. |
-insecure |
Permit a plaintext connection to -to. |
-ndjson |
Drain to an NDJSON file, or - for stdout. |
-max |
Stop after this many records. 0 drains everything. |
-batch |
Records per batch. Default 256. |
-timeout |
Overall deadline for a drain. Default 2m. |
Inspection is read-only and stays read-only. The command you reach for when an agent is misbehaving must not be the thing that repairs (or destroys) the evidence, so inspection describes what a recovery would do rather than doing it, and it is safe to run against a spool a live agent is writing.
The number to diagnose with is usually “oldest owed”: a spool whose oldest undelivered record is four hours old is an agent that has been unable to deliver for four hours, whatever its depth happens to be.
Draining requires the agent to be stopped. Two processes appending to one active segment will corrupt it, and nothing in the command can detect a second writer; there is no lock file, because a lock the agent does not take is a lock that lies. So the command prints the requirement rather than assuming you know it. Draining opens the spool for real, which performs recovery, and it deletes records as they are delivered; a drain that stops early leaves the spool intact and re-drainable, and says so, because reading “done” and then deleting the directory would be the tool misleading you rather than the agent.
-to drains through the real shipper rather than a simplified copy of it. A
drain that reimplemented batching and rejection handling would be a second
delivery path that could disagree with the agent’s, and it would disagree
exactly on the cases you are running the command to understand.
Next
- Installing an agent: the ordinary path
- Capabilities: what a host can honestly measure
- Troubleshooting: when an agent measures and nothing arrives