Implementation notes / C11 and Windows kernel

A small Windows
packet filter,
explained.

A WFP callout driver that observes IPv4 TCP/UDP connection attempts and blocks selected endpoints. A C controller configures the rule and reads metadata across a secured user/kernel boundary.

Connection authorization, not raw packet capture.
Educational implementation. Kernel VM validation still pending.

02 IPv4 ALE layers04 TCP/UDP filters128 bounded event slots01 volatile block rule

01 / The problem

Observe a connection.
Make one deliberate decision.

A network application asks Windows to communicate with an endpoint. Where can a driver observe that request, reject a specific destination, and still cooperate with the rest of the firewall? This project explores that boundary using Windows Filtering Platform (WFP), the operating system’s network-filtering infrastructure.

The useful outcome is not a replacement for Windows Firewall. It is an implementation of the contracts behind a filter: registering callbacks, extracting typed metadata, handling concurrent requests, validating a control interface, and cleaning up every object the driver creates.

What “packet filter” means here

The driver works at the Application Layer Enforcement (ALE) authorization layers. It evaluates connection authorizations, not every packet. It does not inspect Ethernet frames, parse TCP payloads, decrypt traffic, or produce a PCAP file.

  1. 01Application requestA fresh TCP connection or UDP endpoint authorization
  2. 02Windows filteringWFP invokes a registered ALE callout
  3. 03Driver decisionCompare the endpoint against the current rule
  4. 04Policy arbitrationBlock a match, or continue through other WFP policy

02 / Architecture

Separate the control plane
from the classification path.

USER MODE

CONTROL

NetworkCtl.exe

Validate arguments, open the device, send a rule, read stats or events.

TEST

NetworkDriverTests.exe

Exercise shared policy functions and create repeatable TCP/UDP echo traffic.

DeviceIoControlsecured user / kernel boundaryMETHOD_BUFFERED

KERNEL MODE

CONTROL ENDPOINT

NetworkDriver.sys

Versioned IOCTLs update one rule and drain a bounded metadata ring under a spin lock.

CLASSIFICATION

WFP → NdClassify

Two ALE callouts receive typed endpoints, check action rights, evaluate policy, and record a decision.

Network traffic does not pass through the controller. WFP invokes the driver independently; IOCTLs carry only control data and recorded metadata.

WDM / LIFETIME

A control driver, not a NIC driver

WDM provides entry, dispatch, device creation and unload routines. There is no NDIS lightweight-filter attachment to a network adapter.

WFP / POLICY

Runtime and management objects

The runtime callout holds callback pointers. A management callout associates its GUID with a layer. Filters make WFP invoke that callout for TCP and UDP.

SHARED / CONTRACT

One header, two trust levels

The driver and controller compile the same pointer-free structures. Version, size, reserved fields and access bits define what crosses the boundary.

OUTBOUND

Authorize a connection

FWPM_LAYER_ALE_AUTH_CONNECT_V4

The service port is the remote port: the service the local application wants to reach.

INBOUND

Authorize a receive / accept

FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4

The service port is the local port: the service listening on this machine.

03 / Driver lifecycle

Registration is only half the job.

Initialization starts in monitor-only mode. WFP management changes are transactional, and the session is dynamic: its policy objects belong to the engine handle rather than becoming persistent machine policy.

  1. Initialize state.Set the versioned default rule, counters and spin lock. No enabled block rule survives a reload.
  2. Create the secure endpoint.Use IoCreateDeviceSecure for a named control device with a protected DACL.
  3. Register policy atomically.Open the BFE engine, begin a transaction, add the private sublayer, two callouts and four protocol filters, then commit.
  4. Expose the controller link.Publish the DOS symbolic link only after initialization succeeds.

04 / Follow a rule

Same endpoint. Different contracts.

WORKED EXAMPLES / NOT LIVE

A rule is a conjunction: direction and protocol and remote IPv4 and service port must match, except fields marked any. At least an IP or port is required. Expand a case to trace the reasoning.

EXAMPLE OUTBOUND RULENetworkCtl block out tcp 127.0.0.1 8080

These cases illustrate the implemented policy. This page neither loads the C driver nor receives network events.

AMatching outbound connectionBLOCK
Local
127.0.0.1:50000
Remote
127.0.0.1:8080
Protocol / direction
TCP / outbound

All four rule fields match. If WFP grants FWPS_RIGHT_ACTION_WRITE, the driver returns FWP_ACTION_BLOCK and clears the action-write right. The event ring records this callout’s block decision.

BSame IP, different service portCONTINUE
Local
127.0.0.1:50000
Remote
127.0.0.1:8081
Protocol / direction
TCP / outbound

Port 8081 does not match 8080, so this driver does not block. With action-write available it returns FWP_ACTION_CONTINUE, not a blanket permit. Windows Firewall or another provider can still reject the connection. UDP to 8080 would also miss this TCP-only rule.

CInbound: service means the local portBLOCK

Different rule for this case: NetworkCtl block in tcp 192.0.2.10 8080

Local
192.0.2.20:8080
Remote
192.0.2.10:50000
Protocol / direction
TCP / inbound

The remote address matches the peer, while service port 8080 matches the local listener. The peer’s ephemeral port 50000 is not the service port. With action-write available, this matches and blocks. These addresses are reserved documentation examples, not public test targets.

DThe callout cannot write the actionNO-RIGHT

Even if every rule field matches, the driver must not overwrite a decision when FWPS_RIGHT_ACTION_WRITE is absent. It leaves the classification output untouched and records ND_ACTION_NO_RIGHT. This label describes the callback’s authority, not a final allow or block result.

ERestore monitor-only modeCONTINUE*

NetworkCtl monitor replaces the rule with a disabled one. Subsequent authorizations are observed without blocking by this driver. *Action-write still matters: if it is absent, the existing action is preserved and the event is NO-RIGHT instead.

Closing the controller or pressing Ctrl+C in a watcher does not clear an enabled rule. Use monitor or unload the driver.

Rule changes affect future authorization callbacks. They do not explicitly request reauthorization or disconnect existing sockets; use fresh sockets when testing.

05 / Observable state

Metadata in. Bounded records out.

The classify callback can run at DISPATCH_LEVEL, concurrently on multiple CPUs. Its path uses nonpaged static storage and a short spin-lock critical section. It does not allocate memory, wait on user mode, write files, or print a log line for every event.

Oldest → newest

128 slots / fixed capacity
oldesteventevent···eventnewest

When full, the ring overwrites the oldest event and increments Overwritten. Each read drains up to 32 records. Two watchers compete for the same queue; they are not independent subscribers.

What an event contains

Identity & time
Sequence number, UTC timestamp, PID if WFP supplies it.
Endpoint
Direction, protocol, local/remote IPv4 and ports in a fixed-size structure.
Callout action
CONTINUE, BLOCK, or NO-RIGHT. No payload bytes or process path.

What the counters actually mean

Observed
Authorization callbacks seen, not a packet count or unique-connection count.
Blocked
Block decisions made by this callout, not measured dropped bytes.
Queued / Overwritten
Current buffered records / old records lost to capacity pressure.

06 / Permissions and trust

Privilege is not input validation.

01 / OPEN

Restricted device access

A protected DACL grants device access only to LocalSystem and elevated Builtin Administrators. FILE_DEVICE_SECURE_OPEN applies security to the device namespace; trailing file names are rejected.

02 / REQUEST

Read and write are separate

Rule changes require write access on the handle. Stats and events require read access. The controller runs asInvoker; it does not silently elevate or install a service.

03 / VALIDATE

Trust the contract, not the caller

METHOD_BUFFERED supplies a captured system buffer. The driver checks lengths, ABI version, rule fields and reserved values, then returns only initialized bytes.

DEVICE DACLD:P(A;;GA;;;SY)(A;;GA;;;BA)

D:P — protected discretionary ACL. SY — LocalSystem. BA — Builtin Administrators. The two allow entries grant generic-all access; an unelevated administrator token does not gain that grant.

No payload capture does not mean no sensitive data: IPs, ports, PIDs and timestamps can reveal activity. Test only on authorized systems, keep logs inside the lab, and redact them before publishing. An administrator-only sample is not a tamper-resistant security product.

07 / Design choices

Small enough to reason about.

Intentional choices and the capability each one gives up
ChoiceWhy it is hereTradeoff
ALE rather than raw packet layersTyped endpoints and connection-level policy without parsing network buffers.No per-packet inspection, payload capture, or byte counters.
One volatile ruleAtomic replacement and a simple matching contract; monitor-only after reload.No ordered rule list, persistence, CIDR matching or DNS resolution.
One lock, bounded storageRule, counters and ring remain consistent without allocation in classify.Shared-lock contention and intentional event loss under load; performance unmeasured.
Dynamic WFP sessionPolicy is tied to an engine handle, with transactional startup.No automatic recovery after BFE session loss; restart is required.
CONTINUE on nonmatchesCooperate with other WFP providers instead of broadly permitting traffic.The event alone cannot tell whether the application ultimately connected.

08 / Evidence, not assumptions

Built and unit-checked.
Not yet kernel-validated.

The repository records the following local development results with Visual Studio 2026 and WDK 10.0.28000.0. These are a documented validation snapshot, not a live CI badge and not evidence of successful filtering in a VM.

LOCAL / PASSED2

x64 configurations

Debug and Release solution builds, including WDK INF/catalog checks.

LOCAL / PASSED45

C checks per configuration

Shared rule validation, matching, ABI sizes and IOCTL access bits.

LOCAL / PASSED13

CLI validation cases

Help and invalid-input exit codes, without opening the driver.

LOCAL / PASSEDTCP + UDP

Loopback fixture

C echo listener/probe pairs without the driver loaded. Not a filtering test.

VM / not performed

Driver loading, live blocking, device ACL enforcement, concurrent kernel operation, unload/reload, BFE lifecycle and Driver Verifier still require an isolated VM. ARM64 configurations exist but have not been built or runtime-tested in the recorded validation.

Read the full validation matrix and recovery procedure

09 / Reproduce the work

Build locally. Load only in a lab.

1. Build the native projects

Install Visual Studio 2026 with MSVC v145, a compatible Windows SDK/WDK and WDK integration. Open NetworkDriver/NetworkDriver.slnx, or use the 64-bit MSBuild host from a VS Developer Command Prompt.

Developer Command Promptrepository root
cd NetworkDriver
"%VSINSTALLDIR%MSBuild\Current\Bin\amd64\MSBuild.exe" NetworkDriver.slnx /m /p:Configuration=Release /p:Platform=x64
bin\x64\Release\NetworkDriverTests.exe

The default tests run without a driver. Build outputs are unsigned by default. No certificate, service or boot setting is changed automatically.

2. Prepare the isolated VM

Use a disposable Windows VM with a snapshot, console access and an authorized private test network. Follow the full guide for test signing, certificate trust and the explicit demand-start service setup.

  • Use binaries matching the VM architecture.
  • Keep BFE and Windows Firewall enabled.
  • Use only narrow loopback or private-lab rules.
  • Do not change the development host’s boot or security settings.
Open the VM setup & rollback guide
3 / MONITOR → BLOCK → RECOVER

A deliberate lab sequence

Once the signed driver is running, use an elevated VM terminal. Start the C echo listener and a fresh probe for each trial; verify a matching BLOCK event rather than treating a timeout alone as evidence.

The controller commands below are instructions, not recorded output. For a UDP trial, change both the fixture and the rule protocol.

Elevated VM terminaldriver already loaded
NetworkCtl monitor
NetworkCtl block out tcp 127.0.0.1 8080
NetworkCtl events
NetworkCtl stats
NetworkCtl monitor

Finish by closing watchers and device handles, stopping the driver and removing the lab service. The guide covers rollback and certificate cleanup. Do not force-unload a driver.

10 / Scope and next steps

Know what this does not prove.

Intentionally outside this version

  • IPv6, ICMP, Ethernet inspection and raw packet capture.
  • Payload inspection, injection or TLS decryption.
  • Process-path policy, DNS/CIDR rules or persistent configuration.
  • Automatic BFE recovery and per-client event subscriptions.
  • Production readiness, tamper resistance or measured throughput claims.

The next useful engineering work

  1. Run and record the isolated-VM matrix, including negative and recovery paths.
  2. Exercise unload and concurrency with a debugger and targeted Driver Verifier.
  3. Only then extend scope: IPv6 layers, richer policy, or BFE-state handling.
  4. Measure callback cost and lock contention before making performance claims.

The lesson is not just how to return BLOCK. It is how to make a bounded, explainable decision without breaking the operating system’s ownership, lifetime, or permission rules.

11 / Read the implementation

A short path from design to source.