fix(engine): inject the RST the way loopback packets actually travel - #64
Merged
Merged
Conversation
"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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.1connections. It blackholed them for the length ofthe 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".
rst=1/1rst=5/1rst=1/1The counter moving from
5/1to1/1is independent confirmation: a connection that dies at oncehas no further packets left to swallow during the cooldown.
It also closes the audit's oldest unverified claim.
rst_sentwas said to prove only thatsend()did not raise. It proves more: Windows accepts the packet_build_rst_packetforges, andthe application's connection really dies.
Two hypotheses, one measurement each - and the first was wrong
pydivert.Packetstarts withLoopback=0, so the flag was carried over from theprovoking 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 baselineto the exchange. Falsified.
loopback and tcpprinted every packet of a real 127.0.0.1 conversation asoutbound=1, loopback=1, exactly once each - the server's replies included. Loopback has noinbound presentation at all, so an RST injected as
Direction.INBOUNDwas put on a path thestack 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 100catches the first packet of a flow,and a bare RST with
seq=0and no ACK is ignored in SYN_SENT (RFC 793). That run measured the SYNcase, 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.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.
🤖 Generated with Claude Code