Skip to content

Commit 8d5c8ad

Browse files
cansofgreaseclaude
andcommitted
Speedtests that measure the line, and a dashboard search engines leave alone
An iperf3 speedtest ran on a single TCP stream by default, which measures that stream's congestion window rather than your connection. On a real ~350 Mbps path, 1 stream reported 116 Mbps, 4 reported 202 and 8 reported 268 - the same link, more than twice the answer. The default is now 8, suited to a 500 Mbps - 1 Gbps line, and the ceiling moves from 8 to 32 because that same path was still climbing at 12 with the server 93% idle. Past about twelve the differences are mostly run-to-run noise, which the tooltip and the docs now say. The number stays yours to pick: an instrument that retunes itself turns its own choices into apparent ISP changes. The tester enforces the same ceiling the settings do, so a raised count reaches the wire instead of being accepted, stored, displayed and then quietly dropped. The Legacy RSA padding toggle no longer promises something your own machine cannot do. Whether that option can be sent is decided by the iperf3 build on THIS host - it arrived in 3.17 and 3.20 reclassified it as server-only - so on a current build, ticking it made the run fail with a message that read like a server or credential problem. It is now locked outside that window, with a note saying which side of it this host falls on, and a build that cannot be identified locks nothing. Locked against being turned on, never against being turned off: a server saved with it already on - ticked under an older build, or from before this host's iperf3 moved past 3.20 - keeps sending the flag on every run, so that one stays clickable and says to turn it off, rather than leaving the only cure as deleting the server and its stored password. Unticking it locks the toggle again on the spot, so the escape hatch cannot be used to put the flag back. The warning about re-entering an iperf3 server's password stops outliving the edit that raised it. Nothing used to clear it - not opening another server, not closing and reopening the drawer - so it sat there accusing a server that was saved and authenticating fine. The message belongs to an open edit and now dies with it - on save, on Done, on Load defaults, on opening another server - and typing the original address back takes it away with the edit, instead of demanding a retype for a password the box still had. What survives is the durable signal: the password field itself says whether this address has one saved. A greyed-out setting no longer greys out the help tooltip inside it. Opacity applies to everything an element contains, so a dimmed label dimmed its own explanation to about 3.2:1 - the one thing an operator opens precisely because the control is unavailable. The rest of the settings list has carried an escape for this all along; the iperf3 server editor's fields now use the same one. A dashboard reachable from the internet is no longer something search engines will index. It is one person's view of one connection - ISP, exit city, speed history, outage log - and because every install renders identical titles, indexing also scattered duplicate branded pages. The server now sends X-Robots-Tag: noindex, nofollow, which covers /api and /metrics as well; the public demo is a separate build on its own host and is unaffected. Separately: the review-round labels are gone from the source. Forty-two comments cited a finding id - C-57, audit F-11 - from a review document no reader outside it can obtain, and three of them shipped inside the dashboard, where every install and the public demo serve them in View Source. Each one is now the reason the assertion exists, which is what the label was standing in for. Nine test files named after the round that produced them are named for what they test instead, and four tests that were only grouped by that round moved to the file their subject already lives in. No test was added, removed or changed: 966 before, 966 after. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c071c64 commit 8d5c8ad

29 files changed

Lines changed: 432 additions & 206 deletions

.github/workflows/iperf-integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: iperf integration
22

3-
# WHY THIS WORKFLOW EXISTS (audit finding F-12)
3+
# WHY THIS WORKFLOW EXISTS
44
# ------------------------------------------------------------------------------
55
# Every committed iperf3 test stubs the child process through the exec seam
66
# (internal/speedtest/iperf.go: iperfExec) and feeds back canned -J/--json bodies.

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ release:
182182
# rolling-commit amend, so the GitHub pre-release already exists with same-named
183183
# assets and the re-run would 422 on upload without this overwrite. A STABLE
184184
# release, by contrast, is cut once and must stay immutable: its checksums are
185-
# attested in release.yml (C-28), and silently overwriting a published stable
185+
# attested in release.yml, and silently overwriting a published stable
186186
# asset would invalidate that provenance. This field is a plain bool (not
187187
# templateable), so the prerelease-vs-stable split is enforced upstream in
188188
# release.yml - the "Forbid re-cutting a published stable release" guard fails the

internal/logbuf/logbuf_test.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,8 @@ func TestRingByteCeiling(t *testing.T) {
129129
// After eviction the backing array's tail must no longer reference the dropped
130130
// entries: trim compacts survivors to the front and zeros the vacated slots, so
131131
// the evicted strings become collectable instead of being pinned by the backing
132-
// array until a later Append overwrites the slot. Regression for C-57 (both the
133-
// one-line duplicate-survivor case and larger drops that retained evicted
134-
// strings).
132+
// array until a later Append overwrites the slot. Covers both the one-line
133+
// duplicate-survivor case and larger drops that retained evicted strings.
135134
func TestTrimReleasesEvictedSlots(t *testing.T) {
136135
r := New(3)
137136
for i := 1; i <= 6; i++ {
@@ -152,8 +151,7 @@ func TestTrimReleasesEvictedSlots(t *testing.T) {
152151
}
153152

154153
// Clear must release the retained entries, not merely reslice to length 0 (which
155-
// would leave the backing array pinning every cleared string). Regression for
156-
// C-57.
154+
// would leave the backing array pinning every cleared string).
157155
func TestClearReleasesReferences(t *testing.T) {
158156
r := New(4)
159157
for i := 1; i <= 4; i++ {
@@ -173,7 +171,7 @@ func TestClearReleasesReferences(t *testing.T) {
173171
}
174172

175173
// LoadFile must refuse a symlink rather than following it to an attacker-chosen
176-
// target, and must not seed the ring from it. Regression for C-23.
174+
// target, and must not seed the ring from it.
177175
func TestLoadFileRejectsSymlink(t *testing.T) {
178176
dir := t.TempDir()
179177
real := filepath.Join(dir, "real.txt")
@@ -195,7 +193,6 @@ func TestLoadFileRejectsSymlink(t *testing.T) {
195193

196194
// LoadFile must refuse a device file rather than reading from it forever (a
197195
// never-ending stream like /dev/zero would otherwise OOM/hang startup).
198-
// Regression for C-23.
199196
func TestLoadFileRejectsDevice(t *testing.T) {
200197
const dev = "/dev/zero"
201198
if _, err := os.Lstat(dev); err != nil {
@@ -213,7 +210,7 @@ func TestLoadFileRejectsDevice(t *testing.T) {
213210
// A file far larger than the ring's byte ceiling must not be read whole: only a
214211
// bounded tail is loaded (honouring the memory bound before allocation), the
215212
// newest entries are kept, and the partial line at the seek boundary is dropped
216-
// rather than fabricated into a torn entry. Regression for C-23.
213+
// rather than fabricated into a torn entry.
217214
func TestLoadFileReadsBoundedNewestTail(t *testing.T) {
218215
dir := t.TempDir()
219216
path := filepath.Join(dir, "logs.txt")

internal/monitor/monitor_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,7 @@ func TestPendingEventRetryLandsExactlyOnce(t *testing.T) {
11111111
// A DNS probe whose lookup fails because the monitor is shutting down (its round
11121112
// ctx is cancelled) must be neutral: no "dns down" warning and no dns.fail.* bump,
11131113
// so a normal stop can't log a phantom resolver outage or poison the recovered
1114-
// counters. Regression for C-64.
1114+
// counters.
11151115
func TestRoundDNSCancelledLookupIsNeutral(t *testing.T) {
11161116
stats.ResetForTest()
11171117
var buf bytes.Buffer
@@ -1148,7 +1148,6 @@ func TestRoundDNSCancelledLookupIsNeutral(t *testing.T) {
11481148

11491149
// Toggling the DNS sub-toggle OFF must clear the seed so re-enabling starts as
11501150
// "no reading yet" rather than resurfacing the pre-disable value as live.
1151-
// Regression for C-65.
11521151
func TestRoundDNSToggleOffClearsSeed(t *testing.T) {
11531152
stats.ResetForTest()
11541153
m, _ := newTestMonitor(t, 10, 1)
@@ -1181,7 +1180,7 @@ func TestRoundDNSToggleOffClearsSeed(t *testing.T) {
11811180
}
11821181

11831182
// A DNS probe already in flight when the DNS sub-toggle is switched OFF must
1184-
// DISCARD its now-disallowed result rather than publish it. Regression for C-65:
1183+
// DISCARD its now-disallowed result rather than publish it:
11851184
// without bumping the generation on the toggle-off edge, the late goroutine sets
11861185
// dnsSeen=true and inserts a sample under a feature the operator turned off.
11871186
func TestRoundDNSInflightDiscardedAfterToggleOff(t *testing.T) {

internal/netinfo/review_fixes_linux_test.go renamed to internal/netinfo/classify_linux_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package netinfo
44

55
import "testing"
66

7-
// C-53: a raw-socket packet whose IPv4 header claims IHL < 5 (fewer than 20
7+
// A raw-socket packet whose IPv4 header claims IHL < 5 (fewer than 20
88
// bytes) is malformed and must be rejected, not parsed - otherwise classifyRaw
99
// slices the ICMP payload from inside the header and a crafted short IHL can land
1010
// the "type" byte on header data and be misclassified. Mirrors the darwin twin's
Lines changed: 3 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import (
88
"sync"
99
"testing"
1010
"time"
11-
12-
"github.com/pingular/pingularity/internal/stats"
1311
)
1412

15-
// C-20: the refresh-ordering contract. A later-STARTED refresh (higher generation)
13+
// The refresh-ordering contract. A later-STARTED refresh (higher generation)
1614
// outranks an earlier one, so once it publishes, the older/slower one is refused -
1715
// it can't clobber the newer snapshot when it finally returns. A refresh's own
1816
// several effects (m.info, then the exit patch) still pass while it stays current.
@@ -34,68 +32,7 @@ func TestGenerationPublishOrdering(t *testing.T) {
3432
}
3533
}
3634

37-
// C-58: parseChaosTXT must validate the response against our fixed query - a
38-
// mismatched transaction id, a query masquerading as a response, the wrong
39-
// question count, a question that doesn't echo version.bind, or the wrong QCLASS
40-
// must all be ignored rather than trusted as the resolver's answer. A genuine
41-
// response still parses.
42-
func TestParseChaosTXTRejectsForeignResponse(t *testing.T) {
43-
build := func() []byte {
44-
// header: our id 0x1234, flags QR+RD+RA, QD=1 AN=1 NS=0 AR=0.
45-
b := []byte{0x12, 0x34, 0x81, 0x80, 0, 1, 0, 1, 0, 0, 0, 0}
46-
b = append(b, chaosVersionQuestion...)
47-
// answer: name pointer -> 0x0c, TYPE TXT, CLASS CHAOS, TTL 0, RDLENGTH, RDATA.
48-
b = append(b, 0xc0, 0x0c, 0x00, 0x10, 0x00, 0x03, 0, 0, 0, 0)
49-
txt := "dnsmasq"
50-
rd := append([]byte{byte(len(txt))}, txt...)
51-
b = append(b, byte(len(rd)>>8), byte(len(rd)))
52-
b = append(b, rd...)
53-
return b
54-
}
55-
if got := parseChaosTXT(build()); got != "dnsmasq" {
56-
t.Fatalf("valid response = %q, want dnsmasq (a legit answer was rejected)", got)
57-
}
58-
cases := []struct {
59-
name string
60-
mutate func([]byte)
61-
}{
62-
{"wrong transaction id", func(b []byte) { b[1] = 0x35 }},
63-
{"query not response (QR=0)", func(b []byte) { b[2] &^= 0x80 }},
64-
{"qdcount not one", func(b []byte) { b[5] = 2 }},
65-
{"question name mismatch", func(b []byte) { b[13] = 'x' }}, // corrupt the 'v' of "version"
66-
{"wrong qclass (IN not CHAOS)", func(b []byte) { b[12+len(chaosVersionQuestion)-1] = 0x01 }},
67-
}
68-
for _, c := range cases {
69-
b := build()
70-
c.mutate(b)
71-
if got := parseChaosTXT(b); got != "" {
72-
t.Errorf("%s: parseChaosTXT trusted a foreign response = %q, want \"\"", c.name, got)
73-
}
74-
}
75-
}
76-
77-
// C-55: a bare three-letter code is weak evidence (several collide with words -
78-
// "sea", "den", "van", "was"), so cityFromRDNS must raise a low-confidence marker
79-
// while still returning the city; an indexed code ("fra10") is stronger and must
80-
// not raise it. The city string is unchanged either way (display only, zero coords).
81-
func TestCityFromRDNSLowConfidenceMarker(t *testing.T) {
82-
stats.ResetForTest()
83-
if got := cityFromRDNS("core.sea.example.net"); got != "Seattle" {
84-
t.Fatalf("cityFromRDNS(bare sea) = %q, want Seattle", got)
85-
}
86-
if n := stats.Lifetime().Counters["netinfo.rdns_city_lowconf"]; n != 1 {
87-
t.Fatalf("bare three-letter token: lowconf counter = %d, want 1", n)
88-
}
89-
stats.ResetForTest()
90-
if got := cityFromRDNS("ae1-cr2.fra10.isp.net"); got != "Frankfurt" {
91-
t.Fatalf("cityFromRDNS(indexed fra10) = %q, want Frankfurt", got)
92-
}
93-
if n := stats.Lifetime().Counters["netinfo.rdns_city_lowconf"]; n != 0 {
94-
t.Fatalf("indexed token fra10: lowconf counter = %d, want 0 (stronger evidence)", n)
95-
}
96-
}
97-
98-
// C-21 (part two): a trace started on the OLD network must not overwrite the
35+
// Part two of the in-flight-trace contract: a trace started on the OLD network must not overwrite the
9936
// deliberate IP-change cache-bust when it finally lands. The bust bumps traceGen
10037
// mid-trace; at commit the trace sees the generation moved and drops its result,
10138
// leaving the cache cleared so the next caller re-traces the current network.
@@ -134,7 +71,7 @@ func TestCachedExitDropsOutOfGenerationTrace(t *testing.T) {
13471
}
13572
}
13673

137-
// C-21 (part one): while a trace toward target A is in flight, a caller wanting a
74+
// Part one of the in-flight-trace contract: while a trace toward target A is in flight, a caller wanting a
13875
// newly-selected target B must NOT be handed A's path by the single-flight waiter.
13976
// After waiting it re-validates the cache against its own target and traces B.
14077
func TestCachedExitWaiterRetracesForNewTarget(t *testing.T) {

internal/netinfo/geo_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"net/http"
88
"strings"
99
"testing"
10+
11+
"github.com/pingular/pingularity/internal/stats"
1012
)
1113

1214
// geoResp builds a fresh canned response (new body each call, so a host can be hit
@@ -64,3 +66,24 @@ func TestPublicIPGeo(t *testing.T) {
6466
})
6567
}
6668
}
69+
70+
// A bare three-letter code is weak evidence (several collide with words -
71+
// "sea", "den", "van", "was"), so cityFromRDNS must raise a low-confidence marker
72+
// while still returning the city; an indexed code ("fra10") is stronger and must
73+
// not raise it. The city string is unchanged either way (display only, zero coords).
74+
func TestCityFromRDNSLowConfidenceMarker(t *testing.T) {
75+
stats.ResetForTest()
76+
if got := cityFromRDNS("core.sea.example.net"); got != "Seattle" {
77+
t.Fatalf("cityFromRDNS(bare sea) = %q, want Seattle", got)
78+
}
79+
if n := stats.Lifetime().Counters["netinfo.rdns_city_lowconf"]; n != 1 {
80+
t.Fatalf("bare three-letter token: lowconf counter = %d, want 1", n)
81+
}
82+
stats.ResetForTest()
83+
if got := cityFromRDNS("ae1-cr2.fra10.isp.net"); got != "Frankfurt" {
84+
t.Fatalf("cityFromRDNS(indexed fra10) = %q, want Frankfurt", got)
85+
}
86+
if n := stats.Lifetime().Counters["netinfo.rdns_city_lowconf"]; n != 0 {
87+
t.Fatalf("indexed token fra10: lowconf counter = %d, want 0 (stronger evidence)", n)
88+
}
89+
}

internal/netinfo/resolver_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,43 @@ func TestFilterResolvers(t *testing.T) {
2727
}
2828
}
2929
}
30+
31+
// parseChaosTXT must validate the response against our fixed query - a
32+
// mismatched transaction id, a query masquerading as a response, the wrong
33+
// question count, a question that doesn't echo version.bind, or the wrong QCLASS
34+
// must all be ignored rather than trusted as the resolver's answer. A genuine
35+
// response still parses.
36+
func TestParseChaosTXTRejectsForeignResponse(t *testing.T) {
37+
build := func() []byte {
38+
// header: our id 0x1234, flags QR+RD+RA, QD=1 AN=1 NS=0 AR=0.
39+
b := []byte{0x12, 0x34, 0x81, 0x80, 0, 1, 0, 1, 0, 0, 0, 0}
40+
b = append(b, chaosVersionQuestion...)
41+
// answer: name pointer -> 0x0c, TYPE TXT, CLASS CHAOS, TTL 0, RDLENGTH, RDATA.
42+
b = append(b, 0xc0, 0x0c, 0x00, 0x10, 0x00, 0x03, 0, 0, 0, 0)
43+
txt := "dnsmasq"
44+
rd := append([]byte{byte(len(txt))}, txt...)
45+
b = append(b, byte(len(rd)>>8), byte(len(rd)))
46+
b = append(b, rd...)
47+
return b
48+
}
49+
if got := parseChaosTXT(build()); got != "dnsmasq" {
50+
t.Fatalf("valid response = %q, want dnsmasq (a legit answer was rejected)", got)
51+
}
52+
cases := []struct {
53+
name string
54+
mutate func([]byte)
55+
}{
56+
{"wrong transaction id", func(b []byte) { b[1] = 0x35 }},
57+
{"query not response (QR=0)", func(b []byte) { b[2] &^= 0x80 }},
58+
{"qdcount not one", func(b []byte) { b[5] = 2 }},
59+
{"question name mismatch", func(b []byte) { b[13] = 'x' }}, // corrupt the 'v' of "version"
60+
{"wrong qclass (IN not CHAOS)", func(b []byte) { b[12+len(chaosVersionQuestion)-1] = 0x01 }},
61+
}
62+
for _, c := range cases {
63+
b := build()
64+
c.mutate(b)
65+
if got := parseChaosTXT(b); got != "" {
66+
t.Errorf("%s: parseChaosTXT trusted a foreign response = %q, want \"\"", c.name, got)
67+
}
68+
}
69+
}

internal/settings/crypt_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ func (failSealCrypter) Unseal(s string) (string, error) { return s, nil }
431431
// If sealing a password fails, the save must ABORT - not fall through and write the
432432
// password in the clear. mutate returns the error before touching the store or the
433433
// live values, so nothing is persisted and the in-memory config is unchanged.
434-
// Regression for C-33 (sealServers used to `continue` past a Seal error).
434+
// sealServers used to `continue` past a Seal error, persisting the rest.
435435
func TestSealFailureAbortsSave(t *testing.T) {
436436
st, err := store.Open(":memory:")
437437
if err != nil {

internal/settings/settings.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const (
115115
MinIperfDur = 1
116116
MaxIperfDur = 30
117117
MinIperfStreams = 1
118-
MaxIperfStreams = 8
118+
MaxIperfStreams = 32
119119

120120
MaxOoklaConnections = 16 // ceiling for Ookla parallel connections; 0 = library default
121121
MaxIperfOmit = 5

0 commit comments

Comments
 (0)