// 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/. 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