fix(core): let an impairment outlive the flow record that implements it - #53
Merged
Merged
Conversation
_reset_until and _flow_last retired a generation every FLOW_ROTATE_S (30 s), so a record survived 30-60 s - and both tables hold the state that IS an impairment. Two settings were capped by a constant nobody had connected them to: - rst_cooldown accepts up to 3600 s; measured at 120 s the same flow was reset again after 30.3, then 60.8, then 60.8 s. - NAT expiry was worse than capped: a retired record reads back as "never seen", so the next inbound packet reopens the mapping with nothing sent. A 30 s and a 120 s timeout dropped ZERO packets, because the record died just before the first inbound packet arrived. - core: _FlowTable.keep_for(seconds) raises the age window. set_rst passes the cooldown; set_nat switches ageing OFF for _flow_last while NAT is on, because matching the window to the timeout is not enough - the record must outlive the timeout AND the blackhole after it, which lasts until the application sends. - core: the size ceiling is untouched and still checked on every write, so a longer window makes the table older, never bigger. Measured through the real setter: 250k flows at nat_timeout=3600 peaked at 199,999 against the 200,000 ceiling. - core: corrected the prose this invalidates - the _FlowTable docstring claimed eviction "can lose an impairment, never invent one" as if that covered both paths (it covers SIZE; on AGE it was a silent cap), and the step-3 comment from the F1 fix described the blackhole ending at the first rotation. - tests: three in test_core.py; the two behaviour tests verified by mutation, which reproduces the audit numbers exactly (gaps=[30.3, 60.8, 60.8, ...] and dropped=0, passed=720). - both changelogs (convention 39). After: cooldowns of 60/120/300 s reset at exactly those intervals; a NAT blackhole held the full 900 s probe at 5, 30 and 120 s timeouts and ended on the first outbound packet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…escribe Audit F8. Both user-visible, in both languages, and neither catchable by a test - true-sounding prose next to correct code. - tips.stat_loss claimed "Dropped" also counted link outages. Those have had their own counter (drop_flap) since they stopped inflating it, and tips.stat_flap says "Counted separately from loss" in the next cell. The wrong tooltip was on the number a tester reads first. - tips.data_down promised "Hovering also shows how much the app tried to download". add_tooltip renders one static string; there is no dynamic path and never was. The offered figure lives only in the repro report as metrics.offered_mb, and even that is the SUM of both directions. Replaced with something true: dropped packets are not counted in this figure - which is exactly what separates it from the connections table's byte columns. - both changelogs (convention 39; a GUI-visible change goes to the user-facing one). 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.
What and why
Two audit findings: F2 (RST cooldown silently truncated) and F8 (two tooltips describing
counters they do not describe). F2 also closes the tail left over from the NAT fix in #51.
F2 - an impairment expired when its flow record did
_reset_untiland_flow_lastare_FlowTables that retired a generation everyFLOW_ROTATE_S(30 s), so a record survived 30-60 s. Both tables hold the state that is animpairment, so two settings were capped by a constant nobody had connected them to:
--rst-cooldown 60 / 120 / 300--nat-timeout 5--nat-timeout 30and120The NAT case was worse than capped: a retired record reads back as "never seen", so the next
inbound packet reopened the mapping with nothing sent - at a 30 s timeout the record died at the
rotation just before the first inbound packet arrived, so the impairment never fired at all.
_FlowTable.keep_for(seconds)raises the age window.set_rstpasses the cooldown (a fixedspan).
set_natswitches ageing OFF for_flow_lastwhile NAT is on: matching the window to thetimeout is not enough and was measured failing exactly as above, because the record has to
outlive the timeout AND the blackhole after it, and that blackhole is meant to last until the
application sends.
Why raising the window is safe: the SIZE ceiling is enforced on every write, independently of
the age window, so a longer window makes the table older and never bigger. Measured through the
real setter: 250k flows at
nat_timeout=3600peaked at 199,999 against the 200,000 ceiling.F8 - two tooltips that were not true
tips.stat_losssaid "Dropped" also counted link outages. Those have had their own countersince they stopped inflating it, and
tips.stat_flapsays "Counted separately from loss" in thenext cell - two tooltips contradicting each other, with the wrong one on the number a tester
reads first.
tips.data_downpromised "Hovering also shows how much the app tried to download".add_tooltiprenders one static string; there is no dynamic path and never was. The offeredfigure exists only as
metrics.offered_mbin the repro report, and even that is the sum of bothdirections. Replaced with something true and more useful: dropped packets are not counted in
this figure, which is exactly what separates it from the connections table's byte columns.
For the reviewer
keep_fora no-op reproduces theaudit's numbers exactly:
gaps=[30.3, 60.8, 60.8, ...]for the cooldown anddropped=0, passed=720for NAT.test_the_size_ceiling_holds_even_with_the_age_rotation_switched_offdrives the table throughset_naton purpose. The existing churn test assignsnat_timeout_sdirectly, so it neverexercises the ageing-off path - the safety property behind this change would have gone
unguarded.
_FlowTabledocstringclaimed eviction "can lose an impairment, never invent one" as though that covered both paths;
it covers the SIZE path, and on the AGE path it was not a trade but a silent cap. The step-3
comment added in fix(core): keep an expired NAT mapping expired until the app sends #51 described the blackhole ending at the first rotation - that is now what the
fix prevents.
memory bound is unchanged.
Checklist
python -m pytest testspasses locally (677 tests, 0 failures, elevated shell).python smoke_gui.pypasses.tests/test_core.py; two verified by mutation).lang/en.jsonandlang/pl.jsonupdated (465 keys, identical sets).CHANGELOG.md; technical ones and new tests inCHANGELOG-INTERNAL.md, under[Unreleased].type(scope): summary).VERSION.txt.🤖 Generated with Claude Code