Skip to content

fix(engine): inject the RST the way loopback packets actually travel - #64

Merged
donislawdev merged 1 commit into
masterfrom
fix/rst-reaches-loopback-connections
Jul 28, 2026
Merged

fix(engine): inject the RST the way loopback packets actually travel#64
donislawdev merged 1 commit into
masterfrom
fix/rst-reaches-loopback-connections

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

F15 - found by measuring the one thing the audit had never checked: what the application sees,
rather than what a counter says.

The finding

"Reset connections" did nothing to 127.0.0.1 connections. It blackholed them for the length of
the cooldown while reporting an RST as sent, so an application talking to a local service - a dev
server, a database, a proxy - sat there until its own timeout expired instead of seeing a reset.
For a test that expects a broken connection, that is the difference between "failed as designed"
and "hung".

run result counters
DNS over TCP to 8.8.8.8:53 (not loopback) RESET at 6.6 s, WinError 10054 rst=1/1
own echo server on 127.0.0.1 TIMED OUT at 9.5 s, never reset rst=5/1
127.0.0.1, after this fix RESET at 6.5 s, WinError 10054 rst=1/1

The counter moving from 5/1 to 1/1 is independent confirmation: a connection that dies at once
has no further packets left to swallow during the cooldown.

It also closes the audit's oldest unverified claim. rst_sent was said to prove only that
send() did not raise. It proves more: Windows accepts the packet _build_rst_packet forges, and
the application's connection really dies.

Two hypotheses, one measurement each - and the first was wrong

  1. A fresh pydivert.Packet starts with Loopback=0, so the flag was carried over from the
    provoking packet. One line on purpose - changing the direction at the same time would have
    made a success ambiguous. Re-ran the probe: TIMED OUT at 9.5s after 26 exchanges, the baseline
    to the exchange. Falsified.
  2. Rather than guess a second time, the mechanism was measured. A sniff-only handle on
    loopback and tcp printed every packet of a real 127.0.0.1 conversation as
    outbound=1, loopback=1, exactly once each - the server's replies included. Loopback has no
    inbound presentation at all, so an RST injected as Direction.INBOUND was put on a path the
    stack never reads.

The RST is now built to look exactly like those captured rows: OUTBOUND with the loopback flag, for
loopback packets only. Ordinary traffic keeps INBOUND, which is measured to work and must not
change.

A measurement trap worth keeping

The first attempt fired on the SYN, because --rst-prob 100 catches the first packet of a flow,
and a bare RST with seq=0 and no ACK is ignored in SYN_SENT (RFC 793). That run measured the SYN
case, not the established one, and looked like "RST does not work". When testing behaviour on a
connection: establish it, pass some traffic, then switch the impairment on.

Verification

  • python -m pytest tests -> 709 passed, 0 failed (elevated shell). python smoke_gui.py -> OK.
  • New guard asserting direction and flag for both cases against a hand-built IPv4+TCP ACK,
    conditional on pydivert (win32-only) like the driver-queue ABI check. Three mutants, every one
    caught
    - including "every RST becomes a loopback one", which would have broken the ordinary path
    this fix must not touch.
  • No hot-path change: this is the RST construction path, which runs once per reset.

🤖 Generated with Claude Code

"Reset connections" did nothing to 127.0.0.1 connections. It blackholed them for
the cooldown while reporting an RST as sent, so an application talking to a local
service sat there until its own timeout instead of seeing a reset.

Found by measuring what the APPLICATION sees rather than what a counter says: own
echo server, connection established and exchanging, tool started afterwards ->
TIMED OUT after 26 exchanges, rst=5/1. The same shape against 8.8.8.8:53 - not
loopback - reset the connection at 6.6 s with WinError 10054, so the injection
itself was never the problem.

Two hypotheses, one measurement each, and the first was wrong. Carrying the
Loopback address flag over changed nothing (identical baseline, to the exchange).
Rather than guess again, the mechanism was measured: a sniff-only handle showed
every packet of a real loopback conversation presented as outbound=1, loopback=1,
exactly once each - the server's replies included. Loopback has no inbound
presentation, so an RST injected as INBOUND went onto a path the stack never
reads. The RST is now built to look exactly like those captured rows. Ordinary
traffic keeps INBOUND, which is measured to work and must not change.

Result: CONNECTION RESET at 6.5 s, and the tool's counters moved from rst=5/1 to
rst=1/1 - independent confirmation, since a connection that dies at once has no
further packets to swallow during the cooldown.

Also closes the audit's oldest unverified claim on the way: rst_sent proves more
than "send() did not raise" - Windows accepts the forged packet and the
application's connection really dies.

Three mutants, every one caught, including "every RST becomes a loopback one",
which would have broken the ordinary path this must not touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 19a41fd into master Jul 28, 2026
8 checks passed
@donislawdev
donislawdev deleted the fix/rst-reaches-loopback-connections branch July 28, 2026 14:31
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