fmr binds two IPv4 addresses. connFor picked whichever socket of the right family came first in the bind list, so a downtrain for a session established on .150 went out from .151 — and every packet was dropped by the client's NAT, which has no mapping for that pair. tcpdump on the server showed all 50 leaving; the client saw none. Read as "100% downstream loss", which is the worst kind of wrong: a confident measurement of something that never happened. Sessions now record which of our own bound addresses received their traffic, and granted sends (and delayed echo) go back out through that socket. The fallback to a family match is kept for the case where nothing has been received yet, and the test pins both paths — a single-homed lab can never reproduce this. Also: the client-side halves of the same work — anonymizer (core-privacy), local run archive with retention (core-archive), upload client, and the app's settings and history screens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
127 lines
4.0 KiB
Go
127 lines
4.0 KiB
Go
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// Package selftest lets the daemon prove its own host is a clean measurement
|
|
// target: the kernel isn't silently altering what clients measure, and the
|
|
// server's own egress reaches full MTU. If the server side is already broken,
|
|
// client-side results (especially MTU/PMTUD) measure the server, not the
|
|
// client — so the daemon says so.
|
|
package selftest
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Severity of a check result.
|
|
type Severity string
|
|
|
|
const (
|
|
OK Severity = "ok"
|
|
Warn Severity = "warn"
|
|
)
|
|
|
|
// Check is one sysctl (or derived) assertion.
|
|
type Check struct {
|
|
Name string `json:"name"`
|
|
Got string `json:"got"`
|
|
Want string `json:"want"`
|
|
Severity Severity `json:"severity"`
|
|
Why string `json:"why"`
|
|
}
|
|
|
|
// MTUResult is one egress path-MTU probe outcome.
|
|
type MTUResult struct {
|
|
Target string `json:"target"`
|
|
DiscoveredMTU int `json:"discovered_mtu"`
|
|
FullMTU bool `json:"full_mtu"` // >= 1500
|
|
Err string `json:"err,omitempty"`
|
|
}
|
|
|
|
// Report is the whole self-test.
|
|
type Report struct {
|
|
Sysctls []Check `json:"sysctls"`
|
|
EgressMTU []MTUResult `json:"egress_mtu"`
|
|
// SysctlOK / MTUOK are the compact "server proven good" signals; the
|
|
// profile surfaces these so a client can skip MTU tests the server can't
|
|
// support honestly.
|
|
SysctlOK bool `json:"sysctl_ok"`
|
|
MTUOK bool `json:"mtu_ok"`
|
|
}
|
|
|
|
// readSysctl reads /proc/sys/<dotted.name>. Empty string if unavailable.
|
|
func readSysctl(name string) string {
|
|
p := "/proc/sys/" + strings.ReplaceAll(name, ".", "/")
|
|
b, err := os.ReadFile(p)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
// sysctlChecks are the measurement-fidelity assertions. Each closure returns
|
|
// OK/Warn given the read value; a missing value (non-Linux / restricted) is
|
|
// reported as Warn "unreadable" but never fatal.
|
|
var sysctlChecks = []struct {
|
|
name string
|
|
want string
|
|
why string
|
|
ok func(v string) bool
|
|
}{
|
|
{"net.ipv6.conf.all.accept_ra", "0", "static v6 host must not let RAs mutate routing (the very thing Echolot detects)", eq("0")},
|
|
{"net.ipv4.conf.all.accept_redirects", "0", "ICMP redirects could alter routing mid-measurement", eq("0")},
|
|
{"net.ipv4.conf.all.send_redirects", "0", "an endpoint should not emit ICMP redirects", eq("0")},
|
|
{"net.ipv4.icmp_echo_ignore_all", "0", "server must answer ping so clients can measure to it", eq("0")},
|
|
{"net.ipv4.ip_no_pmtu_disc", "0", "server must honor path MTU on its own sends", eq("0")},
|
|
{"net.ipv4.tcp_sack", "1", "so a missing SACK in mss_observed is the path's fault, not the server's", eq("1")},
|
|
{"net.ipv4.tcp_timestamps", "1", "so TCP-timestamp absence reflects the path, not the server", eq("1")},
|
|
{"net.ipv4.tcp_window_scaling", "1", "so wscale absence reflects the path, not the server", eq("1")},
|
|
{"net.ipv4.icmp_ratelimit", "0", "nonzero throttles the server's ICMP errors → false loss/black-hole readings", eq("0")},
|
|
}
|
|
|
|
func eq(want string) func(string) bool { return func(v string) bool { return v == want } }
|
|
|
|
// Sysctls runs the sysctl audit.
|
|
func Sysctls() []Check {
|
|
out := make([]Check, 0, len(sysctlChecks))
|
|
for _, c := range sysctlChecks {
|
|
got := readSysctl(c.name)
|
|
sev := Warn
|
|
switch {
|
|
case got == "":
|
|
got = "(unreadable)"
|
|
case c.ok(got):
|
|
sev = OK
|
|
}
|
|
out = append(out, Check{Name: c.name, Got: got, Want: c.want, Severity: sev, Why: c.why})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Run performs the full self-test: sysctl audit + egress MTU probes to the
|
|
// given targets (each "host" — port is irrelevant for PMTUD).
|
|
func Run(mtuTargets []string) Report {
|
|
r := Report{Sysctls: Sysctls()}
|
|
r.SysctlOK = true
|
|
for _, c := range r.Sysctls {
|
|
if c.Severity == Warn {
|
|
r.SysctlOK = false
|
|
}
|
|
}
|
|
r.MTUOK = true
|
|
for _, t := range mtuTargets {
|
|
res := probeEgressMTU(t)
|
|
r.EgressMTU = append(r.EgressMTU, res)
|
|
if !res.FullMTU {
|
|
r.MTUOK = false
|
|
}
|
|
}
|
|
if len(r.EgressMTU) == 0 {
|
|
r.MTUOK = false // couldn't prove it
|
|
}
|
|
return r
|
|
}
|
|
|
|
var _ = strconv.Atoi
|