Files
EvoFirewall/docs/agent-hardening-checkpoint.md
T
Denozordec 9fa6269174
quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
feat: harden firewall policies telemetry and operations
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
2026-10-26 13:10:58 +07:00

14 KiB

Agent hardening and telemetry v2 checkpoint

This checkpoint implements agent safety, report delivery and capability-gated dual-stack compilation. It is not a production or device certification. Linux nft compiles and collects both IPv4 and IPv6. The ipset backend supports IPv6 when all required ip6tables tools are available; otherwise requested IPv6 enforcement is explicitly unsupported. RouterOS renders both families and probes native IPv6 availability before mutation; native execution remains unverified.

Linux agent

  • The downloaded policy is validated as JSON and written to data files. CIDRs, hash, actions, protocols and port ranges never pass through shell eval.
  • Mixed CIDR arrays are canonicalized and collapsed independently by family. nft creates separate IPv4/IPv6 address and Port ACL source sets. An IPv4 any-source ACL is restricted to IPv4 and cannot bypass IPv6 restrictions.
  • Strict default-drop semantics apply to both families. Required IPv6 ND/RA/control and management traffic must be explicitly allowed; capability support does not configure network autoconfiguration or add hidden allow exceptions.
  • The policy hash contract is sha256:<64 lowercase hex characters>, matching GET /v1/agent/policy.
  • A process lock serializes runs. Private run directories, HTTP deadlines and bounded kernel command execution prevent shared temporary-file and concurrent-apply races.
  • nft enforcement is compiled into one batch, checked with nft --check --file, then submitted as one transaction. Optional detail-counter features may be omitted after a failed feature check; required enforcement is retained.
  • ipset candidates are populated in inactive sets for both families when IPv6 tooling is available. Both family rule candidates are checked before any swap. If the second rule-family transaction fails, the already-applied first-family owned rules/jump positions and all four sets are rolled back. Foreign rules are preserved. Port ACL remains explicitly unsupported for ipset.
  • ipset candidates are populated in inactive sets. Swap or iptables-restore failure triggers compensating rollback. The pair of swaps is not one atomic kernel operation.
  • The owned iptables INPUT jump is reinserted at position one through the checked restore transaction. Drift verification includes INPUT order and rejects a foreign rule preempting this jump. Foreign INPUT rules are preserved.
  • Unchanged policies skip apply only after structural kernel verification. Traffic counters, kernel handles and expiring detail elements are excluded from this fingerprint.
  • The last successful hash and policy are committed only after successful required apply operations.
  • The JSON policy limit is 8 MiB, with 200,000 raw CIDR memberships, 50,000 canonical unique memberships, and 1,000 serialized Port ACL rules. Effective memberships are counted per deny/allow/source array before optional network collapse. Repeated entries in distinct arrays or expanded TCP/UDP rules count again. Adjacent and overlapping unique CIDRs count separately. Source import capacity is separate from assignment capacity. Rejected candidates report POLICY_BUDGET_EXCEEDED and retain last good enforcement.
  • A strong ETag is sent only with its exact durable last-good JSON body. Both commit together after successful apply or verified no-op. A 304 still runs validation and structural drift verification/repair using that saved body. Failed candidates never replace the cache. A bounded upstream 422 error body propagates only known structured codes; authentication failure remains an authentication failure.

Reports

  • Reports use the shared telemetry v2 schema on POST /v1/agent/apply-report.
  • Linux persists sequence allocation and up to 32 immutable pending report payloads in one state file using file fsync, rename and directory fsync. Reports are retried with their original ID, sequence and contents.
  • An HTTP success is insufficient by itself: the response must acknowledge the same report ID and sequence as accepted, duplicate, or already superseded by a newer sequence.
  • A matching authenticated stale ACK supplies the durable server sequence_floor. A rolled-back client resumes at floor + 1 with a fresh counter epoch and explicit sequence_rollback_coverage_unknown; retries retain their original payloads. Invalid floors preserve pending bytes. A MAX_SAFE_INTEGER floor stops sync without discarding pending state and requires re-enrollment; sequence numbers never wrap.
  • Loss counts are per report interval, drained only in the same durable commit as the queued report. They are not lifetime totals repeated in every subsequent report.
  • A newer generation with the same hash is acknowledged after kernel verification without apply or epoch rotation; previously queued report payloads remain immutable.
  • A full or unwritable queue blocks counter-resetting apply. A final snapshot of the previous epoch is queued before apply. New policy installation rotates counter epochs; reboot and observed counter decreases also rotate the affected epochs.
  • nft traffic collection reads JSON and stable owned rule IDs. Prerouting accepts are excluded from accepted totals because the same packet subsequently traverses input or forward. Drops are terminal and counted at the rejecting hook.
  • ipset traffic collection reads owned iptables counters. Detail collection does not present prefix aggregates as measured individual-host traffic.
  • Expected active families are persisted. If any required iptables-family read fails, aggregate packet totals are omitted and quality is partial. The prior complete per-rule baselines survive the omission, so recovery detects a family reset even when the other family's counters grew.
  • Missing, unsupported and unsafe integer counters are omitted and labelled in quality metadata. They are not sent as observed zeroes. Detail truncation includes found/sent counts when known.
  • Host firewall snapshots are refreshed every five minutes or after apply. Failed observation is unknown and omitted. A successful report ACK does not trigger a separate heartbeat.

RouterOS

  • Target minimum version is RouterOS 7.21, with the required scripting, JSON, random-string, file-read and import commands. That minimum has not been executed on a device. Native IPv6 is probed before either family mutates; a policy requesting IPv6 is rejected with UNSUPPORTED_CAPABILITY when unavailable. An IPv4-only policy can still use the supported family.
  • The renderer stages unique address-list banks and complete disabled canonical rules for both families before activation. IPv4 and IPv6 are separate native transactions: failure attempts compensation for both. Original owned rules retain their full matchers and positions; the transaction changes only their comment/disabled fields, and rollback restores those snapshot values while removing newly created rules. Originally disabled rules remain disabled on rollback. Foreign rules are preserved.
  • Missing, duplicated, modified or reordered owned rules trigger canonical replacement. Successful verification checks chain, action, enabled state, source bank, unexpected matchers and static rule order. One prior disabled retired wave is retained for rollback and reaped on the next apply; only unreferenced owned banks are cleaned. An unexpectedly active retired rule requires manual recovery.
  • RouterOS enforcement is INPUT deny only, with remaining INPUT behavior controlled externally. FORWARD is deny, allow, then configured default; default accept leaves the default-drop rule disabled. This scope applies equally to both families. No hidden ND/RA or management exceptions are added.
  • The sync reads JSON and then imports the current RSC endpoint. It acknowledges the actual hash/revision published only after successful import verification, so a controller generation change between those requests cannot produce a false ACK. Verified no-op updates the applied revision without resetting counters.
  • The script retains one serialized complete v2 pending report and next sequence together, using flash/evofw-telemetry.json when a flash disk/directory is present and evofw-telemetry.json otherwise. Only a matching ACK clears it. Valid rolled-back state resynchronizes from the authenticated server floor, rotates the traffic epoch and labels unknown coverage. Corrupt or exhausted state stops sync instead of silently resetting numbering.
  • RouterOS file updates do not provide the Linux fsync-and-rename durability contract. This limitation is included in report quality metadata. Power-loss behavior requires device validation.
  • Address-list presence is not a packet measurement. RouterOS omits per-IP and per-port packet metrics and reports those capabilities as unavailable. It does not synthesize packets: 1 from presence.
  • Default FORWARD DROP verification requires an empty source address list in both families; unexpected narrowing matcher keys are rejected before accepting a no-op.
  • Rule counters are checked for safe integer range before aggregation across both active families. Complete observations retain separate stable family/rule baselines; missing-family reads leave those baselines intact. A decrease in any individual rule rotates the traffic epoch even when other counters grew, and both regular and pre-apply final snapshots label counter_reset_coverage_unknown. Successful apply clears the prior rule baselines. A missing required family omits aggregate totals. Traffic epochs rotate after detected resets; the final old-epoch snapshot must be saved and ACKed before an apply that resets counters. No separate post-ACK heartbeat is emitted.
  • Only successful probe and verified enforcement advertise policy-ipv6 and traffic-counters-ipv6. RouterOS Port ACL is explicitly unsupported; IP and port detail metrics remain omitted. Linux nft separately advertises implemented IPv6 Port ACL/detail capabilities; ipset advertises IPv6 enforcement without Port ACL.
  • Policy downloads use files and bounded chunk reads, avoiding the native output=user response truncation. Both JSON and generated RSC are checked against 8 MiB before parsing/import. RouterOS requests ?transport=routeros: only the controller's known budget rejection becomes a 200 error envelope with no hash/revision/cache validator. The installer recognizes that envelope before policy validation or import. Authentication and unexpected failures keep normal HTTP failure behavior. Native fetch has no documented hard byte quota: the cooperative Range header may be ignored, so download disk bounds rely on the controller's pre-transmission budget plus a one-minute deadline and local post-download check.

RouterOS primary sources and assumptions

  • Scripting manual documents runtime errors, conditional blocks, arrays, JSON conversion and import handling. The older official RouterOS scripting presentation, page 23, describes unknown commands failing at runtime. The missing-IPv6-path probe is based on those semantics; parsing/import behavior on a device without the IPv6 command remains a required test.
  • Fetch CLI documents the silent 63 KiB output=user response limit, file output, HTTP headers and duration. Pending report/ACK bodies are small; policy downloads use file output instead.
  • File read CLI documents chunked reads up to 32,768 bytes and returned data. The script checks file size before reading and detects missing progress.
  • The official Files guide documents persistent flash/ placement, including flash entries typed disk, and delayed NAND writes that can leave empty files after power loss. Corrupt state fails closed; this checkpoint does not claim crash-atomic native storage.
  • Clock manual documents date, time and GMT offset fields used for report timestamps. Version-specific timestamp formatting, large JSON/string allocation, import scope, matcher serialization and compensating rollback still need CHR/device execution.

Verification and remaining device work

Focused validation:

node apps/api/node_modules/vitest/vitest.mjs run src/services/agent-hardening.test.ts src/services/agent-hardening-telemetry.test.ts src/services/agent-hardening-dualstack.test.ts src/services/agent-hardening-budget.test.ts src/services/agent-hardening-cache.test.ts src/services/agent-hardening-mikrotik.test.ts src/services/policy/mikrotik-rsc.test.ts src/services/sync-script.test.ts src/services/install-links.test.ts --root apps/api
node apps/api/node_modules/typescript/bin/tsc --noEmit -p apps/api/tsconfig.json

Current checkpoint: 100 focused tests passed across the nine files above; API TypeScript and Bash syntax checks for the agent and installer passed. The suite executes embedded Python, real Git Bash control flow with mocked firewall commands, actual authenticated policy endpoints, the Bash parser, shared report schema validation, API ingestion, replay acknowledgement and queue removal. It covers queue overflow, mismatched ACK, timeout, failed persistence, final-snapshot ordering, mixed-family rollback, missing-family recovery, no-op revision ACK, canonical budget boundaries, 304 drift repair, failed-candidate cache retention and stale-sequence resynchronization. RouterOS checks are generated-code/static contracts plus actual API roundtrips; they do not emulate or execute native import.

Linux kernel nft/ipset/iptables commands were not executed in this Windows environment. nft syntax feature compatibility, kernel rollback behavior, Docker packet paths and counters require a Linux network-namespace or VM matrix. RouterOS scripts were rendered and checked statically; they were not executed on a RouterOS device or CHR. RouterOS syntax/version compatibility, clock offset formatting, file persistence, boot recovery and command-failure rollback remain device checks. No deployment is implied by these tests.

Follow-up RouterOS regression validation: 20 tests passed across agent-hardening-mikrotik.test.ts and policy/mikrotik-rsc.test.ts, including both-family default matcher and shared per-rule reset detector contracts. A broader 102-test run in the restricted Windows sandbox produced 67 passes and 35 environment failures (Git Bash startup exit 3221225794 and Python atomic replace WinError 5 in sandbox temp storage); this does not replace the prior successful checkpoint run. These new checks are static native-script contracts, not RouterOS execution.