Skip to content

Mock driver - #12

Closed
vlabo wants to merge 16 commits into
mainfrom
feature/fuzzing
Closed

vlabo wants to merge 16 commits into
mainfrom
feature/fuzzing

Conversation

@vlabo

@vlabo vlabo commented Oct 21, 2026

Copy link
Copy Markdown
Owner

No description provided.

vlabo added 16 commits June 20, 2026 21:26
Introduce GitHub Actions for nightly fuzzing and PR validation.
Update Justfile to include driver tests and fuzzing commands.
Add mock_wdk dependency to enable host-based property testing.
Update CI workflows, Justfile, and Cargo.toml paths to reflect the
relocation of the fuzz directory from the root to driver/fuzz.
Adjust dependency paths and workspace configurations accordingly.
Introduce a new fuzz target that exercises the stateful callout pipeline,
decoding operations to pend flows, resolve verdicts, and verify packet-layer
actions against permanent verdicts. Add corpus seeds and a unit test to
ensure the harness links correctly and its oracles are non-vacuous.
Introduce the `callouts_mt` target to stress the shared device with multiple
worker threads driving concurrent callouts. This harness uses a barrier to
synchronize threads and includes a quiesce oracle to verify the packet cache
drains correctly after shutdown. Add a `Justfile` recipe to run these targets
under ThreadSanitizer for race detection.
Wrap FilterEngine in a R/W spinlock so Device::read, write,
shutdown, and inject_packet can take &self. Producer (callout)
and consumer (read) threads now share one &'static Device
without a global lock.

Remove tmp_ended_connections_buffer_* from ConnectionCache;
clean_ended_connections takes &self and returns freshly-owned
vecs, eliminating the &mut self requirement on Device.

Lift driver_entry out of its cfg(not(mock)) inner module so
the full-driver simulation can invoke the real DriverEntry over
the mocked WDK. get_device now returns &'static Device.
Add DEVICE_TEST_LOCK to serialize tests that touch the global
device slot. Add Device::new_with_blocking_reads to choose
blocking/non-blocking event queues at construction.

Fix port extraction in packet_util: resolve IPv4 option headers
(IHL > 5) and walk IPv6 extension header chains so packet-layer
keys match the ALE-layer flow keys.

Add simulation fuzz target: drives the full load → callout →
blocking read → verdict → unload lifecycle under real OS
threads, verifying the packet cache drains on shutdown.
Supports both ASAN and ThreadSanitizer.
A Rust daemon (`zf-sim`) loads the real driver over `mock_wdk`,
exposes it on a Windows named pipe, and bridges the pipe to
the driver's user-space channel. The real Go agent connects
exactly as it would to the kernel device, only the transport
differs.

Key pieces:
- `sim/`: new Cargo workspace (daemon + overlapped pipe server
  + fake network event producer)
- `kext_interface/pipe_connection.go`: `OpenPipe` opens the
  named pipe with synchronous I/O, returning a `*KextFile`
  the existing `RecvInfo`/verdict path can use unchanged
- `kext_interface/kext_file.go`: add `synchronous` mode so
  `ReadFile`/`WriteFile` pass a nil OVERLAPPED on pipe handles
- `kext_tester/main.go`: `ZF_SIM_PIPE` env var routes into
  `runSimulation`; verdict logic refactored into `handleInfo`
  shared with the production path
- `Justfile`: `sim-build`, `mock-run`, `sim-run` recipes
Replace the three-hardcoded-flow producer with a full concurrent
simulation harness. Workers drive semi-realistic ALE + packet-layer
flows (DNS, TLS, HTTP, ICMP, reauth, port-reuse) against the real
callout entry points; a reinject thread feeds driver-injected packets
back through the packet-layer callouts, mirroring kernel behaviour.
Everything is reproducible from a printed seed (ZF_SIM_SEED).

Supporting changes:
- fuzz_api: add TupleSpecV4/V6 and *_tuple callout entry points so
  the producer is not limited to the fixed pool; add drain_injected,
  injected_len, verdict_for_key_v4/v6, filter_engine_snapshot
- mock_wdk: record injected packets instead of no-op; expose drain /
  len and filter-engine registration snapshot
- sim: add SplitMix64 RNG (rng.rs) and scenario library (scenarios.rs)
- fuzz/targets: drain injected queue to prevent unbounded growth
1. **sim**: converted from a finite, config-driven run to an infinite loop with a fixed seed; removed teardown/watchdog code
2. **fuzz_api.rs**: removed legacy packet-level fuzz helpers (get_ports, redirect, key extraction, etc.)
3. **go.work**: new workspace file to build kext_tester against local kext_interface
4. **kext_tester/main.go**: log ConnectionEndV4/V6 events; panic on unexpected types
5. **Justfile**: update mock-run comment to reflect the forever-run semantics

```
refactor(sim): run forever with fixed seed

Convert the simulation harness from a finite, config-driven run
to an infinite loop. The seed is now hardcoded (SEED constant)
so any failure reproduces automatically on the next run; Ctrl+C
is the only intended stop condition. Remove the watchdog thread,
clean-teardown phase, and all ZF_SIM_* env-var config.

Remove the legacy packet-level fuzz helpers from fuzz_api.rs
(get_ports, redirect_outbound/inbound, key_from_bytes, owned_nbl,
fuzz_device_write) — these are superseded by the callout harness.

Add go.work to build kext_tester against the local kext_interface,
and log ConnectionEnd events (panic on unexpected types) to help
diagnose the running simulation.
```
The agent now loads and starts the mock driver itself via
`MockKextService` (mirroring how production loads the kernel
driver), so `just sim-run` runs the whole simulation with a
single command. A new scenario engine (`scenario.rs`) replaces
the previous random-only traffic generator with structured,
reproducible test scenarios selectable via `ZF_SIM_SCENARIO`.
Add a `dns` scenario op that performs a real DNS lookup at replay
time and expands into the callout sequence (ALE connect carrying
the wire query, query datagram out, response in, endpoint close,
resource release) plus an optional follow-on TCP session.  The
lookup uses the OS-configured resolver by default; a `resolver`
field overrides it.

Add `--loop` / `ZF_SIM_LOOP` / `sim-scenario-loop` (Justfile
recipe) to replay a scenario file indefinitely, resetting transient
driver state between passes with a new `reset_device_state` fuzz-
API function so each iteration sees fresh connection tables and the
injected-packet log does not grow without bound.

Generalise `dns_query_for` / `dns_response_for` in `scenarios.rs`
to accept any domain and IP, add `dns_answer_ip` for parsing live
responses, and add a new `sim/src/dns.rs` module that owns the
OS-resolver detection and raw-socket query logic.
# Conflicts:
#	.gitignore
#	driver/Cargo.toml
#	driver/src/device.rs
#	driver/src/entry.rs
#	driver/src/id_cache.rs
#	driver/src/lib.rs
#	driver/src/packet_callouts.rs
#	driver/src/packet_util.rs
#	kext_interface/go_command_test.bin
#	kext_interface/kext_file.go
#	protocol/rust_info_test.bin
Add docs/LLMs/, a set of auto-generated API references that list
every type, signature and doc comment with bodies stripped, so an
assistant can orient in the codebase without reading the whole
source tree. One file per area: driver, protocol, wdk, mock_wdk,
tools (sim + release) and Go user space.

Add the two generators that produce them:

- tools/rustdoc-gen — parses Rust sources with syn and emits one
  section per file (module docs, consts, types with their inherent
  impls, trait impl headers, free functions, extern blocks).
- tools/godoc-gen — walks the Go modules and emits one section per
  package; library packages show exported symbols only, package
  main shows all declarations.

Both skip test code, generated files and build scripts.

Wire them up as `just gen-llm-docs`, register godoc-gen in go.work,
and point CLAUDE.md at the new references so they are read before
exploring the source. docs/LLMs/README.md documents file sizes,
token estimates and which file to load for which task.
The fake-driver daemon and the Go agent were Windows-only: the daemon
exposed the driver on a named pipe and the agent had no other transport.
Add a Unix domain socket path on both sides so the whole end-to-end
simulation runs on Unix, and wire it into CI.

- sim: split pipe.rs into pipe/{windows,unix}.rs behind one PipeServer
  API; pipe::endpoint() maps the logical ZF_SIM_PIPE name to the OS
  address, and main.rs now reads that variable instead of hardcoding
  \\.\pipe\ZenithFence.
- Stream sockets have no message boundaries, but the driver needs one
  command per write, so commands are length-prefixed (u32 LE) on that
  transport. Events stay an unframed byte stream, as from the device.
  SOCK_SEQPACKET would truncate against the agent's 1 KiB read buffer.
- kext_interface: add SimConn (io.ReadWriteCloser) with per-OS
  openSimConn/SimEndpoint; the Unix framedConn mirrors the framing and
  keeps Read buffer-filling semantics. mock_service.go is no longer
  Windows-only. Move InterfaceVersion out of the Windows-only kext.go
  into version.go so the agent links off Windows.
- kext_tester: split main.go into main_windows.go (kernel service path)
  and main_unix.go (simulation only); the shared verdict policy moves to
  sim.go and takes io.ReadWriter.
- sim/dns.rs: read the default resolver from /etc/resolv.conf on Unix;
  scenario.rs installs a SIGINT handler that flags a watcher thread,
  since reporting isn't async-signal-safe.
- Justfile: [windows]/[unix] variants of the sim recipes; CI gains a
  sim-linux job running sim-test plus a mixed_v4v6_parallel replay,
  which has no `dns` op and so stays offline and deterministic.
The daemon used to load the driver once, serve a single agent and let
the producer run forever. Running the real agent against it meant
restarting the daemon for every agent run, and the producer failed the
run whenever the agent's own policy disagreed with kext_tester's fixed
expectations.

In random-traffic mode the daemon now loops: wait for an agent, run the
producer while it is connected, and on disconnect shut down the driver,
join the pumps, unload and load a fresh one for the next agent -- the
way the agent's Start/Stop load and unload the kernel driver. The
producer takes a stop flag so its workers, monitor and reinjector wind
down instead of running forever, and prints the session summary on the
way out. Scenario replay still serves one agent and exits.

--any-verdict / ZF_SIM_ANY_VERDICT selects VerdictPolicy::Any: the
agent may hand out whatever verdict it likes and the producer checks
only that the driver enforces the one it got (permit for accept, block
for block, block+absorb for drop/undeterminable/failed, anything for
temporary verdicts). A verdict that never arrives is counted, not
failed, and the flow closes as an application giving up would. Under
the default Expected policy nothing changes.

On the Go side the simulation pipe is now opened overlapped like the
device, so a pending Read no longer blocks writes from other goroutines
and Close can cancel it; the synchronous special case is gone. Broken-
pipe errors map to ERROR_HANDLE_EOF so a daemon going away looks like
the device's queue running down, and ReadVersion answers from
InterfaceVersion because the pipe has no IOCTL channel. Both sides
accept a full \\.\pipe\ path as the endpoint name, so an agent built
with a custom pipe path can point the daemon at it.

Adds the `just mock-serve` recipe that starts the daemon in this mode,
and regenerates the LLM reference docs.
@vlabo vlabo closed this Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant