EPICS PVAccess packet capture, decoding, and real-time traffic monitoring.
spvirit-scry captures live PVAccess network traffic and decodes it in real time. It tracks connection state (channels, operations, PV names) and provides a TUI dashboard or InfluxDB/Loki export for production monitoring.
spvirit-scry— CLI/TUI packet capture with real-time traffic statistics.spvirit-scry_influx_collector— packet capture with InfluxDB and Loki export.
This crate is part of the spvirit project:
| Crate | Description |
|---|---|
spvirit-codec |
PVAccess wire-format codec and connection state tracking |
spvirit-tools |
PVA client/server tools (pvget, pvput, pvmonitor, pvinfo, pvlist, pvexplore, pvsearch, pva_server, etc.) |
spvirit-types |
Shared Normative Type data model |
- Live packet capture with rate and request-type statistics.
- TUI view for interactive monitoring.
- PVA connection state tracking — channels, operations, PV name resolution.
- Filtering and tracking options (
--exclude-cmds,--tracked-only). - InfluxDB and Loki export via the collector binary.
- Passive source fingerprinting — infers which process is actually producing a channel's values from wire behaviour alone (
--fingerprint).
- Rust toolchain (edition 2024).
- Linux:
libpcap-devinstalled and available. - Windows: Npcap runtime and the Npcap SDK. The build script auto-detects the SDK, or set
NPCAP_SDKto the extracted SDK root. - Linux: Packet capture requires either
sudoorCAP_NET_RAWcapability (see below).
After installing, grant the binary the CAP_NET_RAW capability so it can capture without sudo:
sudo setcap cap_net_raw=eip $(which spvirit-scry)
sudo setcap cap_net_raw=eip $(which spvirit-scry_influx_collector)This only needs to be done once per install (re-run after cargo install upgrades the binary).
cargo install spvirit-scrygit clone https://github.com/ISISNeutronMuon/spvirit-scry.git
cd spvirit-scry
cargo build --releaseOutput binaries:
target/release/spvirit-scrytarget/release/spvirit-scry_influx_collector
The build script searches these locations automatically:
NPCAP_SDKenvironment variable (explicit override)npcap-sdk/relative to the project rootC:\npcap\,C:\npcap-sdk\,C:\Program Files\Npcap\(including versioned subdirs)%LOCALAPPDATA%\npcap-sdk\
spvirit-scry --device eth0 --tuiOptions:
--device/-i: network interface to capture from.--integration-time/-t: stats integration period in seconds.--buffer-size/-b: capture buffer size (100–50000).--resolve/-r: resolve IP addresses.--tui: enable TUI.--debug/-d: debug logging.--version/-v: print version and exit.--exclude-cmds/-x: comma-separated PVA commands to exclude (e.g.PUT,MONITOR).--pva-ttl: PVA channel state TTL in seconds (default 300).--pva-max-channels: max PVA channels to track per connection (default 40000).--tracked-only: only show packets for tracked connections.--pva-max-update-rate: max update timestamps kept per channel for rate calculation (default 10000).--fingerprint: enable passive source fingerprinting (off by default). Results appear on the TUI's Fingerprints page (key5).--fp-window: fingerprint window in seconds (default 900).--fp-min-samples: minimum samples before a fingerprint is emitted (default 30).--fp-reservoir: per-channel reservoir size per window (default 200).--fp-round-robin-slots: round-robin channel slots;N > 1extends time-to-detection N-fold (default 1).--fp-max-channels: maximum channels fingerprinted concurrently (default 5000).--fp-d1: enable D1 source-change detection (off by default, see below).--fp-suppress-secs: suppress D1 for this many seconds from startup (maintenance window).
spvirit-scry_influx_collector --device eth0 --config-file influxdb_config.tomlOptions:
--device/-i: network interface to capture from.--integration-time/-t: stats integration period in seconds.--buffer-size/-b: capture buffer size (100–50000).--resolve/-r: resolve IP addresses.--debug/-d: debug logging.--version/-v: print version and exit.--config-file/-c: path to InfluxDB config file.--fingerprint: enable passive source fingerprinting (off by default).--fp-window: fingerprint window in seconds (default 900).--fp-min-samples: minimum samples before a fingerprint is emitted (default 30).--fp-reservoir: per-channel reservoir size per window (default 200).--fp-round-robin-slots: round-robin channel slots;N > 1extends time-to-detection N-fold (default 1).--fp-max-channels: maximum channels fingerprinted concurrently (default 5000).--fp-d1: enable D1 source-change detection (off by default, see below).--fp-suppress-secs: suppress D1 for this many seconds from startup (maintenance window).
--fingerprint derives a per-channel fingerprint from four observables of the
traffic itself, so it holds regardless of what any process, configuration file
or directory service claims:
- Sub-second timestamp phase — where in the second a source stamps its updates, computed with circular statistics so a source phase-locked near the second boundary is not mistaken for an unlocked one. On real ISIS traffic two channels sat 434 ms apart with a within-channel spread under 0.2 ms.
- Value quantisation lattice — the GCD of successive differences, which recovers the digitiser LSB (0.0175 °C, 0.01 pH on the reference channels).
- Update cadence — modal interval plus p50/p95, a joint fingerprint of the scan rate and the deadband policy.
- Arrival minus stamp — available only to a wire observer, and the best single signal for telling bridge-sourced from device-sourced data.
A fingerprint accumulates per (channel, server address) over a rolling window
(default 15 minutes) and is emitted once at least 30 samples are in. Results
appear on the TUI's Fingerprints page (key 5; v cycles list, clusters
and the discontinuity log), as a badge column in the Connections detail table,
and from the collector as InfluxDB measurements plus Loki events.
pv_fingerprint — one point per channel per closed window. Tags: host,
channel, server, struct_id (unknown when the channel carries no
normative-type id), sampling (full, reservoir or round_robin). Fields:
phase_mean_ms, phase_spread_us, phase_circular_var, value_lsb,
lsb_confidence, cadence_modal_s, cadence_p50_s, cadence_p95_s,
arrival_delta_mean_ms, arrival_delta_var_ms, sample_count,
observed_count, has_nan_limits, struct_id_changed. value_lsb and
arrival_delta_var_ms are omitted rather than zeroed when they could not be
computed: a lattice of zero and an unknown lattice are different facts.
pv_fingerprint_coverage — what the observer actually looked at: channels
tracked, admitted, deferred at capacity, reservoir-sampled and rotated out,
with monotonic totals and saturation flags.
pv_fingerprint_detect — everything the detector declined to emit: firings
suppressed by the maintenance window, repeats, incomparable pairs, channels
skipped by the D2 pair cap, populations too small for D4, and events dropped by
the per-ingest cap.
pv_fingerprint_event (Loki) — one structured event per rule firing, carrying
both the prior and current fingerprint so the event is self-contained. Rules:
D1 source change, D2 server/fingerprint disagreement, D3 synthesised metadata,
D4 clock-offset outlier.
These are limitations of the method, not of this implementation, and they do not go away with more data:
- A fingerprint is not an identity. It establishes sameness and difference. It can say two channels share a source, or that a channel's source changed; it cannot name an unknown source. Two unrelated IOCs started at the same moment with the same scan rate will look alike, so the fingerprint is strongest when it says "these differ" and weakest when it says "these match". There is no cryptographic assurance: traffic can be shaped to mimic a fingerprint.
- Planned failover is indistinguishable from silent re-sourcing. A
legitimate gateway failover produces exactly the D1 signature, and nothing on
the wire separates the two. This is why D1 is opt-in behind
--fp-d1, with--fp-suppress-secsas a startup maintenance window. D2, D3 and D4 run whenever fingerprinting is on. - Coverage is partial, and is reported rather than assumed. A passive
observer only sees the segments it is placed on; on a multi-node swarm with an
internal gateway mesh one observation point sees a subset of channels. The
pv_fingerprint_coveragemeasurement says what this observer saw — it cannot say what it was never positioned to see. - Absence of a fingerprint is not evidence of anything. A hardware-clocked
or event-driven source shows no narrow phase; a computed channel (calc
record, unit conversion, aggregate) may show no lattice, or a lattice that
reflects the arithmetic rather than the digitiser. The TUI keeps a measured
phase,
unlocked(measured but not a signature),—(computed, genuinely absent) and…n/m(still filling the window) as four distinct states for exactly that reason. - D3 is one-sided today. PVAccess carries no declared-provenance field, so
D3 fires on the observable half of the contradiction — synthesised NaN or
all-zero
valueAlarm/displaylimits — with nothing to compare a declaration against.
Fingerprinting adds per-update work to the capture path, which is why it is off by default. Under load it degrades in three stages and always says which one it is in: reservoir sampling within a channel, then a round-robin subset of channels, with channels that have no fingerprint yet always admitted. The regime is recorded on every exported fingerprint, so a partial view is never presented as a complete one.
The collector reads a TOML configuration file. Required keys:
url = "http://localhost:8086"
token = "your-token"
org = "your-org"
bucket = "your-bucket"
loki_url = "http://localhost:3100"Note: Do not commit secrets (tokens) to the repository. Use environment-specific secret management.
- No packets captured: verify the device name (
--device) and ensure capture permissions (sudoor NIC capabilities). - InfluxDB errors: verify
url,token,org, andbucketin the config file and confirm network connectivity. - Buffer size errors:
--buffer-sizemust be between 100 and 50000. - Diagnostics: use
--versionto confirm build version,--debugfor verbose logging.
Licensed under BSD-3-Clause.