A deep technical dive into gov-pass: WinDivert and NFQUEUE internals, reassembly complexity, split/inject mechanics, Go implementation notes, and PCAP-based validation.
One-line summary: gov-pass reshapes the first TLS ClientHello record by splitting it into controlled TCP segments, while failing open on any ambiguity.
Unlike full TLS proxies, gov-pass never decrypts or mutates TLS payloads. It only changes packet boundaries for the first TLS record in a flow. Everything else is pass-through. This narrow scope makes the system predictable and easier to audit: if it cannot safely identify a ClientHello record, it returns the original packets unmodified.
Why split-only? The practical goal is to alter the shape of the first TLS record without touching its contents or the rest of the flow. That reduces risk compared to full proxying and makes experiments repeatable: either a split happens early, or the flow is released unchanged.
This project is an experimental prototype inspired by ideas from goodbye-dpi and other open-source traffic-splitting tools. The implementation is purpose-built for research and controlled testing.
flowchart LR
A[Capture: WinDivert / NFQUEUE / pf divert] --> B[Decode IPv4/TCP + port filter]
B --> C[Flow table + reassembly]
C --> D[TLS ClientHello detect]
D --> E[Split plan]
E --> F[Inject split segments]
C --> G[Fail-open: reinject originals]
F --> H[Pass-through remaining flow]
G --> H
Only outbound IPv4 TCP flows with destination port 443 enter the pipeline. Anything else is accepted immediately.
Flow state and reassembly (single pass explanation)#
Each flow is keyed by (src IP, dst IP, src port, dst port, protocol) and hashed to a worker shard. A worker owns the flow table and reassembly state for its shard, ensuring ordered handling without locks.
Collection begins on the first payload packet. The reassembler only needs a contiguous prefix from the base sequence number, so it:
Preserved; PSH/FIN only on the last injected segment
Checksums
Recalculated for every injected segment
Key defaults:
Setting
Default
split-mode
tls-hello
split-chunk
5
collect-timeout
250ms
max-buffer
64KB
max-held-pkts
32
max-seg-payload
1460
worker count
CPU cores
flow idle timeout
30s
Tuning guidance:
Increase max-buffer only if you see partial ClientHello records
Decrease collect-timeout if latency spikes are unacceptable
Reduce max-seg-payload if you need smaller injected segments
Windows (WinDivert) driver behavior and queue tuning#
On Windows, the adapter uses WinDivert at the network layer with a filter such as:
outbound and ip and tcp.DstPort == 443.
Key runtime knobs map directly to WinDivert queue parameters:
QueueLen: maximum packets held by the driver. If this is too small under bursty load, packets will be dropped and the flow will fail open.
QueueTime: maximum time (ms) a packet can be queued before being delivered to user-space. Lower values reduce latency but increase the chance of pressure under load.
QueueSize: total bytes allowed in the queue. This is the last line of defense against large bursts of oversized packets.
Tuning guidance:
Start with higher QueueLen and QueueSize if you expect high fan-out (many concurrent HTTPS flows).
Keep QueueTime low enough to avoid adding jitter to ClientHello timing, but not so low that the queue constantly flushes under load.
If you see frequent fail-open on Windows, check queue pressure first before adjusting reassembly parameters.
Symptom to action (Windows):
Symptom
Likely cause
Action
Frequent fail-open on busy traffic
QueueLen/QueueSize too small
Increase queue parameters before tuning reassembly
Noticeable handshake delay
QueueTime too high
Lower QueueTime and retest latency
No split observed
Offload or filtering issue
Verify filter and driver is running; confirm outbound only
WinDivert’s helper routine is used to recalculate IP/TCP checksums after packet rebuild, which keeps the injection path simple and avoids platform-specific checksum logic.
Linux (NFQUEUE + raw socket): edge cases and performance#
The Linux adapter uses NFQUEUE to receive packets and a raw socket with IP_HDRINCL for reinjection. A few details matter in practice:
Queue bypass and loop prevention: reinjected packets must be marked (SO_MARK) and bypass NFQUEUE rules, or the flow will loop indefinitely.
Queue pressure: queue-maxlen and kernel queue capacity determine how much backpressure is tolerated before packet loss or bypass occurs. Using --queue-bypass keeps the system fail-open during pressure.
Copy range: larger copy-range increases user-space work but is necessary to reconstruct the full TLS record. Default is full packet.
Offload effects: GRO/GSO/TSO can create large aggregated packets that hide true segment boundaries. Disabling offload on the egress interface stabilizes observed packet sizes during testing and validation.
NFQUEUE errors: netlink ENOBUFS indicates queue overflow; the adapter enables netlink.NoENOBUFS to avoid noisy error storms.
Performance considerations:
The system is latency-sensitive only during the collection window. After injection, flows are pass-through and no longer incur reassembly costs.
Worker sharding keeps flow-local state hot in CPU cache and reduces contention.
One or more follow-up segments that complete the ClientHello record
Sequence numbers increment by exactly the segment lengths
If you disable splitting (or force fail-open), you should see the ClientHello carried in a single larger TCP segment instead of the split pattern above.
Sharing hands-on cloud infrastructure and DevOps experience. Writing about Kubernetes, Terraform, and observability, and documenting lessons learned as a solo operator.