Diagnostics System
SkillMonitoring & opsUse when modifying the diagnostics scan pipeline, ScanRequest/ScanReport types, ProbeTask families, ripdpi-monitor-engine / ripdpi-diagnostics-* crates, strategy-probe candidates, the diagnostics catalog (packs/profiles), wire-schema contracts between Rust and Kotlin, DIAGNOSTICS_ENGINE_SCHEMA_VERSION, golden contract tests, or adding a new probe type / profile. Triggers on diagnostics scans, strategy probes, automatic audit, dpi-detector profiles, or anything in core/diagnostics or native/rust/crates/ripdpi-monitor-*.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Diagnostics System skill
What this skill tells your AI
The instructions your AI receives, as published by po4yka/ripdpi in .agents/skills/diagnostics-system/SKILL.md and read by ahel’s review.
1. Overview
The diagnostics system is a two-tier pipeline:
- Rust engine (
native/rust/crates/ripdpi-monitor-engine/plus theripdpi-diagnostics-*crates) -- executes network probes in a background thread, producing aScanReportwith structuredProbeResultandProbeObservationvalues. - Kotlin orchestration (
core/diagnostics/) -- manages lifecycle, catalog loading, JNI bridge, report enrichment, persistence, and policy application.
The Rust engine is stateless per scan; the Kotlin layer owns state, scheduling, and cross-scan coordination (remembered network policies, resolver overrides).
Telemetry primitives live in native/rust/crates/ripdpi-telemetry/ and provide
LatencyHistogram and LatencyDistributions for in-process latency recording.
2. Scan Types
Two ScanKind variants exist:
| Kind | Purpose | Typical profile |
|---|---|---|
Connectivity | Tests reachability of domains, DNS, TCP, QUIC, services, circumvention tools, Telegram, and throughput targets. Results are bucketed into healthy/attention/failed/inconclusive. | default, dpi-detector-full, ru-* profiles |
StrategyProbe | Evaluates path optimization strategy candidates (TCP and QUIC) against a target set, then recommends the best configuration. Requires RAW_PATH mode (outside the VPN tunnel). | automatic-probing (quick_v1), automatic-audit (full_matrix_v1) |
Connectivity scans are general-purpose. Strategy probe scans are the automatic calibration mechanism -- they test multiple bypass configurations and select the most effective one for the current network.
3. Rust Engine Pipeline
Entry point
MonitorSession::start_scan() in lib.rs validates the wire request, spawns
a worker thread, and calls run_engine_scan() (in engine.rs).
Stage-based execution model
The engine uses a plan-then-execute architecture:
-
Plan (
engine/plan.rs):build_execution_plan()creates anExecutionPlanwith an orderedVec<ExecutionStageId>. For connectivity scans the order is derived fromprobe_tasksfamilies (or a default sequence). For strategy probes the order is fixed: Environment -> StrategyDnsBaseline -> StrategyTcpCandidates -> StrategyQuicCandidates -> StrategyRecommendation. -
Coordinate (
engine/runtime.rs):ExecutionCoordinatorholds aBTreeMap<ExecutionStageId, Box<dyn ExecutionStageRunner>>and iteratesplan.stage_order, invoking each runner. It checks cancellation and deadline between stages. -
Run (
engine/runners/): Each runner implementsExecutionStageRunnerwithid(),phase(),total_steps(), andrun(). Runners produceRunnerArtifacts(probe results + observations + events) and callruntime.record_step()which increments progress and publishes it to shared state. -
Report (
engine/report.rs):build_report()assembles the finalScanReportfrom accumulated results, observations, and optional strategy probe report. Includesengine_analysis_version,classifier_version, andpack_versions.
Cancellation and deadlines
ExecutionRuntime checks is_cancelled() (cooperative via AtomicBool) and
is_past_deadline() (360s default hard deadline) between stages. A cancelled scan
still produces a partial report.
Progress reporting
Progress flows through SharedState (behind Arc<Mutex<>>) and is polled
by Kotlin via poll_progress_json(). The EngineProgressWire includes
phase, step counts, and optional StrategyProbeLiveProgress for candidate-
level tracking.
4. Strategy Probe System
This is the most complex subsystem. It automatically selects the best DPI bypass configuration for the user's network.
Candidate generation (candidates.rs)
build_strategy_probe_suite() creates a StrategyProbeSuite with TCP and
QUIC candidate lists. Two suites exist:
quick_v1-- subset for background automatic probingfull_matrix_v1-- complete matrix for manual audit
Each candidate is a StrategyCandidateSpec with an id, label, family,
eligibility rule, warmup requirements, and a ProxyUiConfig describing the
bypass strategy parameters.
Candidate inventory
Candidate membership is capability-dependent (TFO, raw-packet and
IP-fragmentation support). Do not copy a fixed list or count into docs. Read
build_strategy_probe_suite() and the TCP/QUIC builder families in
ripdpi-diagnostics-candidates, then exercise the registry tests for the
runtime capabilities under review.
DNS baseline (strategy.rs)
detect_strategy_probe_dns_tampering() runs before any candidate evaluation.
It compares system DNS answers against encrypted DNS (DoH) answers for each
target domain. If DNS tampering is detected (NXDOMAIN or substitution), the
scan short-circuits -- it skips all TCP/QUIC candidates and recommends a
resolver override instead.
The encrypted DNS context is resolved via strategy_probe_encrypted_dns_context()
which prefers the user's configured resolver, falling back to AdGuard.
DNS fallback for strategy probes
When the primary encrypted resolver fails, detect_strategy_probe_dns_tampering()
invokes build_fallback_encrypted_dns_endpoints() (in dns.rs) to try
alternative resolvers in order: AdGuard, DNS.SB, Google IP, Mullvad. This
prevents a failure of the preferred resolver from being misclassified as
DNS tampering.
TCP/QUIC candidate evaluation (engine/runners/strategy.rs)
For each candidate:
- Build a
ProxyUiConfigwith the candidate's bypass parameters - Probe each target domain (HTTP/HTTPS/QUIC depending on lane)
- Record success/failure per target, compute weighted quality score
- Jitter-based pauses between candidates (
candidate_pause_ms()) to avoid network-level rate limiting
Tournament bracket (Round 1 qualifier)
Before the full matrix (Round 2), each TCP candidate is tested against a single representative domain. Candidates that fail both HTTP and HTTPS probes in Round 1 are eliminated and do not enter the full cross-product evaluation. On censored networks this typically eliminates ~70% of candidates, significantly reducing total scan time.
Within each candidate's domain set, up to 3 domains are tested concurrently
via thread::scope inside execute_tcp_candidate().
Stage timeouts
StrategyProbeStageTimeoutMs = 300_000 (5 minutes) applies to the
automatic_audit and dpi_strategy stages. The native engine's scan deadline
is set via scan_deadline_ms in ScanRequest; it defaults to
stageTimeout - 30_000 (30s grace) when not explicitly provided.
Partial results recovery
When a strategy probe stage times out, ActiveScanRegistry.cancelActiveScan()
does not immediately discard results. Instead it polls for a 3-second grace
period, giving the native engine time to finish writing its current candidate
batch. If a partial report is available it is retrieved and persisted. This is
surfaced as StrategyProbeCompletionKind::PartialResults in the finalized
report, indicating the scan was interrupted mid-execution but produced usable
candidate data.
Recommendation output
StrategyRecommendationRunner selects the TCP and QUIC winners by quality
score, produces a StrategyProbeRecommendation with the winning candidate
IDs, labels, and recommended proxyConfigJson. An AuditAssessment is
attached with coverage metrics and confidence level (Low/Medium/High).
5. Diagnostics Catalog
The catalog defines what targets and profiles ship with the app.
Build-time generation
Located in build-logic/convention/src/main/kotlin/DiagnosticsCatalog*.kt:
DiagnosticsCatalogDomain.kt-- domain model:TargetPackDefinition,DiagnosticsProfileDefinition,CatalogScanKind, enums for profile families (GENERAL, WEB_CONNECTIVITY, MESSAGING, CIRCUMVENTION, etc.)DiagnosticsCatalogPackSource.kt--DefaultDiagnosticsCatalogPackSourcedefines target packs (e.g.,ru-independent-media,ru-global-platforms,ru-messaging,ru-circumvention,ru-throttling,neutral-control)DefaultDiagnosticsCatalogProfileSource.kt--DefaultDiagnosticsCatalogProfileSourcedefines profiles that reference packs and configure scan behaviorDiagnosticsCatalogAssembler.kt-- loads packs, loads profiles (with pack index for cross-referencing), validates, then renders to JSONDiagnosticsCatalogDefinitions.kt-- top-level entry point calling the assembler
How to add a new target pack
- Add a
TargetPackDefinitiontoDefaultDiagnosticsCatalogPackSource.load()with an id, version, and target lists (domain, DNS, TCP, QUIC, etc.) - Reference it from profiles via
index.requirePack("your-pack-id")
How to add a new diagnostic profile
- Add a function to
DefaultDiagnosticsCatalogProfileSourcethat returns aDiagnosticsProfileDefinition - Include it in the
load()list - Set
kind,family,executionPolicy,packRefs, and target lists - For strategy probe profiles: set
kind = CatalogScanKind.STRATEGY_PROBEand provide aStrategyProbeDefinitionwith the suite ID - Rebuild to regenerate
default_profiles.jsonasset
6. DiagnosticsHome Composite Run
DiagnosticsHomeViewModel (or the equivalent run coordinator) executes a
composite run of multiple profiles in sequence. As of the current version the
run has 4 stages:
| Stage | Profile | Notes |
|---|---|---|
automatic_audit | automatic-audit (full_matrix_v1 strategy probe) | 5-min timeout via StrategyProbeStageTimeoutMs |
default_connectivity | default | Standard connectivity check |
dpi_full | dpi-detector-full | Full DPI detection sweep |
dpi_strategy | ru-dpi-strategy | Runs STRATEGY_PROBE with Russian-specific domains; 5-min timeout |
The ru-dpi-strategy profile uses the full_matrix_v1 suite scoped to
Russian-domain target packs. It shares the same 5-minute stage timeout as
automatic_audit.
7. Kotlin Orchestration Layer
Call chain
DiagnosticsScanController.startScan()
-> ScanAdmissionService.admitManualStart() -- checks no active scan
-> DiagnosticsScanRequestFactory.prepareScan() -- builds PreparedDiagnosticsScan
-> BridgeExecutionService.createHandle() -- creates JNI bridge
-> BridgeExecutionService.start() -- calls bridge.startScan(requestJson)
-> DiagnosticsScanExecutionCoordinator.execute() -- launched in coroutine
-> BridgePollingService.awaitCompletion() -- polls progress + report
-> ScanFinalizationService.finalize() -- enriches, persists, applies policies
Key classes
| Class | Responsibility |
|---|---|
DefaultDiagnosticsScanController | Entry point for manual and automatic scans. Manages hidden probe conflicts. |
ScanAdmissionService | Guards against concurrent scans. Resolves profile from settings. |
ActiveScanRegistry | Tracks active bridges, execution jobs, cancellation state, fingerprints. Implements 3s grace-period partial results polling on cancel. |
BridgeExecutionService | Creates and destroys NetworkDiagnosticsBridge (JNI). |
BridgePollingService | Polls pollProgressJson()/takeReportJson() on interval. Timeout: 360s. |
ScanFinalizationService | Enriches report (classifier, resolver recommendation, strategy validation), persists results, applies remembered network policies. |
DiagnosticsScanWorkflow | Pure-logic orchestration: report enrichment, resolver override decisions, background auto-persist eligibility, network policy construction. |
RuntimeSessionCoordinator | Manages bypass usage sessions (not scan sessions). Tracks connection lifecycle, telemetry sampling, failure recording. |
DiagnosticsScanRequestFactory | Builds wire-format request JSON from profile + settings. |
Automatic probing
AutomaticProbeCoordinator and AutomaticProbeScheduler trigger background
scans on policy handover events. These use launchAutomaticProbe() on the
controller. Background probes run hidden (no UI progress) and auto-persist
results when audit confidence is high enough (coverage >= 75%, winner coverage
= 50%).
DNS-corrected re-probe
When a strategy probe is short-circuited by DNS tampering and a temporary resolver override is applied, the system automatically re-probes after a 2s delay to evaluate candidates with corrected DNS.
8. Logcat Capture
Logcat collection uses two capture scopes:
| Scope | Flag | Use case |
|---|---|---|
app_visible_snapshot | legacy (no -T) | Short scans where log rotation is not a concern |
time_bound_snapshot | -T <timestamp> | Long-running scans (e.g., strategy probe stages) |
The time_bound_snapshot scope passes the -T sinceTimestampMs flag set to
the earliest session start time across all active sessions. This prevents log
rotation loss on scans that exceed the default logcat ring-buffer window.
9. Wire Protocol
Rust and Kotlin communicate via JSON serialization over JNI. The wire types mirror each other:
| Rust type | Kotlin type | Direction |
|---|---|---|
EngineScanRequestWire | EngineScanRequestWire | Kotlin -> Rust |
EngineProgressWire | EngineProgressWire | Rust -> Kotlin (poll) |
EngineScanReportWire | EngineScanReportWire | Rust -> Kotlin (take) |
EngineObservationWire | ObservationFact | Embedded in report |
EngineProbeResultWire | EngineProbeResultWire | Embedded in report |
Schema version is tracked via DIAGNOSTICS_ENGINE_SCHEMA_VERSION (Rust,
wire.rs) and DiagnosticsEngineSchemaVersion (Kotlin,
contract/engine/EngineContract.kt). Both must be equal; this is enforced
by contract tests.
The ScanRequest field scan_deadline_ms is optional; when absent the engine
uses its internal default (stage timeout minus 30s).
See references/wire-protocol.md for field-level details.
10. Adding a New Probe Type
End-to-end walkthrough for adding a hypothetical "ping" probe:
Rust side
- Define target type in
types/target.rs:pub struct PingTarget { pub host: String, pub count: u8 } - Add to ScanRequest in
types/request.rs:pub ping_targets: Vec<PingTarget>, - Add wire mapping in
wire.rs-- add field toEngineScanRequestWire, updateFrom<EngineScanRequestWire> for ScanRequest. - Add ProbeTaskFamily variant in
types/request.rs:Ping, - Create execution stage -- add
ExecutionStageId::Pinginengine/runtime.rs. CreatePingRunnerimplementingExecutionStageRunnerinengine/runners/connectivity.rs. - Register runner in
engine/runners/mod.rs:Box::new(PingRunner), - Add stage to plan in
engine/plan.rs-- addProbeTaskFamily::Ping => ExecutionStageId::Pingmapping and include in the default connectivity order. - Add observation mapping in
observations.rsif structured facts are needed. - Update contract fixtures -- add outcome tokens, update field manifests.
Kotlin side
- Add target model in
core/diagnostics/.../Models.kt. - Mirror wire types in
contract/engine/EngineContract.kt-- add toEngineScanRequestWireandEngineProbeTaskFamily. - Add to catalog domain in
DiagnosticsCatalogDomain.kt-- add target definition type and include inTargetPackDefinition. - Update catalog rendering to serialize the new target list.
- Add contract fixture entries and update golden tests.
Tests
- Add outcome tokens to
outcome_taxonomy_current.json. - Rust:
contract_fixtures.rswill catch missing outcomes. - Kotlin:
DiagnosticsWireContractTestwill catch field mismatches.
11. Adding a New Diagnostic Profile
- Open
build-logic/convention/src/main/kotlin/DefaultDiagnosticsCatalogProfileSource.kt. - Add a private function returning
DiagnosticsProfileDefinition:private fun myNewProfile(index: DiagnosticsCatalogIndex): DiagnosticsProfileDefinition { val myPack = index.requirePack("my-pack") return DiagnosticsProfileDefinition( id = "my-new-profile", name = "My New Profile", family = CatalogDiagnosticProfileFamily.GENERAL, executionPolicy = policy(manualOnly = false, allowBackground = false, requiresRawPath = false), domainTargets = myPack.domainTargets, // ... ) } - Add it to the
load()list. - If you need a new target pack, add it to
DiagnosticsCatalogPackSource.kt. - Rebuild. The catalog assembler validates uniqueness, pack references, and schema constraints.
- Update
diagnostics-contract-fixtures/profile_catalog_current.jsonby running contract tests withRIPDPI_BLESS_GOLDENS=1.
12. Testing
Golden contract tests
The contract test framework ensures Rust and Kotlin stay in sync:
- Shared fixtures in
contract-fixtures/(repo root) -- schema version, field manifests for progress and report types. - Diagnostics fixtures in
diagnostics-contract-fixtures/-- full wire payloads for request, report, progress, profile catalog, outcome taxonomy. - Rust side (
tests/contract_fixtures.rs): decodes all shared fixtures, verifies schema version matches, checks outcome tokens cover all emitted probe outcomes. - Kotlin side (
DiagnosticsContractGovernanceTest.kt): decodes the same fixtures, verifies schema version, checks bundled catalog matches fixture. - Wire field tests (
DiagnosticsWireContractTest.kt): compares field paths between Rust-produced manifests and Kotlin serialization.
Golden file support
GoldenContractSupport.kt provides assertJsonGolden() and
assertTextGolden(). Set RIPDPI_BLESS_GOLDENS=1 to regenerate. Diff
artifacts are written to core/diagnostics/build/golden-diffs/.
Key test classes
| Test | What it verifies |
|---|---|
DiagnosticsWireContractTest | Field-level compatibility between Rust and Kotlin wire types |
DiagnosticsContractGovernanceTest | Schema versions match, fixtures decode, catalog matches asset |
DiagnosticsScanWorkflowTest | Strategy probe enrichment, background eligibility, policy construction |
DiagnosticsScanControllerTest | Scan lifecycle: start, cancel, hidden probe conflict resolution |
DiagnosticsScanExecutionCoordinatorTest | Execution flow: polling, finalization, DNS-corrected re-probe |
DiagnosticsScanRequestFactoryTest | Wire request construction from profiles and settings |
DiagnosticsModelsCompatibilityTest | Model serialization round-trip stability |
Wire compatibility verification
After any wire type change:
- Run
cargo test --locked -p ripdpi-monitor-engine-- catches fixture decode failures and outcome token coverage gaps. - Run
:core:diagnostics:testDebugUnitTest-- catches field manifest and schema version mismatches. - If fields were added: bless goldens with
RIPDPI_BLESS_GOLDENS=1and commit updated fixtures.
13. PCAP Diagnostic Recording
On-device packet capture for DPI evasion debugging without external tools.
Rust components:
ripdpi-monitor-engine/ diagnostics archive support -- standard pcap export, retention, and archive bundling for captured sessionsripdpi-proxy-runtime/src/runtime/state/desync.rsplus the runtime desync context --PcapHookcallback invoked on outbound desync executionripdpi-android/src/ffi/proxy_bridge/pcap.rs-- JNI bridge:jniStartPcapRecording,jniStopPcapRecording,jniIsPcapRecording
Android components:
DiagnosticsViewModel--togglePcapRecording()action,pcapRecordingstate flowDiagnosticsArchiveFileStore--cleanupPcapFiles()(24h age-based),getRecentPcapFiles()for exportDiagnosticsArchiveRenderer-- includes up to 3 recent pcap files in diagnostic export bundles
Storage safety: 10 MB file cap, 50 connection auto-stop, cache directory storage, 24h cleanup on app start.
Signals
- GitHub stars
- 71
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
diagnostics-system- Source
- github.com/po4yka/ripdpi