Files
echolot/server/internal/selftest/mtu_linux.go
T
mrambossekandClaude Opus 5 4ae744aae5
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 27s
server-release / release (push) Successful in 28s
server: self-test — sysctl audit + egress-MTU self-proof ("server proven good")
A measurement server must prove its own host isn't distorting results:
- sysctl audit (/proc/sys): flags accept_ra on a static host, ICMP
  redirects, ICMP rate-limiting of the server's own errors, and disabled
  TCP options — each a measurement-fidelity hazard, with the "why".
- egress-MTU self-proof: DF PMTUD probe (IP_MTU_DISCOVER + getsockopt
  IP_MTU, no root — Linux-only, stub elsewhere) to external anchors. If the
  server's own uplink is below 1500, client MTU tests measure THIS server,
  so we say so.
Exposed at GET /admin/selftest (full report) and as server_selftest
{mtu_ok, sysctl_ok} in the profile so clients can trust or skip MTU tests.
Recommended deploy/99-echolot-sysctl.conf + README section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:44:38 +02:00

104 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build linux
package selftest
import (
"net"
"net/netip"
"syscall"
"time"
)
// Linux IP-level constants for PMTU discovery. Not all are exported by the
// stdlib syscall package across versions, so they are pinned here (stable
// kernel ABI) — same rationale as the prober's OsAbi.
const (
ipMTUDiscover = 10 // IP_MTU_DISCOVER
ipMTU = 14 // IP_MTU
ipPMTUDiscDo = 2 // IP_PMTUDISC_DO (set DF, honor PMTU)
ipv6MTUDiscover = 23 // IPV6_MTU_DISCOVER
ipv6MTU = 24 // IPV6_MTU
ipv6PMTUDiscDo = 2 // IPV6_PMTUDISC_DO
)
// probeEgressMTU sends a DF-flagged full-size UDP datagram toward target and
// reads back the kernel's discovered path MTU. A reduction below 1500 means
// the SERVER's own uplink can't carry full-size packets — so client MTU
// results would measure the server, not the client. No root, no raw socket:
// IP_MTU_DISCOVER + a getsockopt on IP_MTU, mirroring the prober's approach.
func probeEgressMTU(target string) MTUResult {
res := MTUResult{Target: target}
addr, err := netip.ParseAddr(target)
if err != nil {
// allow "host" that resolves
ips, e := net.LookupIP(target)
if e != nil || len(ips) == 0 {
res.Err = "resolve: " + errStr(err)
return res
}
addr, _ = netip.AddrFromSlice(ips[0])
}
addr = addr.Unmap()
is4 := addr.Is4()
fam := syscall.AF_INET6
if is4 {
fam = syscall.AF_INET
}
fd, err := syscall.Socket(fam, syscall.SOCK_DGRAM, 0)
if err != nil {
res.Err = "socket: " + errStr(err)
return res
}
defer syscall.Close(fd)
if is4 {
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, ipMTUDiscover, ipPMTUDiscDo)
} else {
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_IPV6, ipv6MTUDiscover, ipv6PMTUDiscDo)
}
// Full-size probe: 1500 total IP/UDP headers (28 v4, 48 v6).
payload := 1472
sa := sockaddr(addr, 33434)
if !is4 {
payload = 1452
}
// A DF send larger than the local MTU fails immediately with EMSGSIZE; a
// path reduction updates IP_MTU after the ICMP frag-needed returns, so we
// send, briefly wait, and read the discovered MTU.
_ = syscall.Sendto(fd, make([]byte, payload), 0, sa)
time.Sleep(700 * time.Millisecond)
_ = syscall.Sendto(fd, make([]byte, payload), 0, sa) // second send observes any reduction
level, opt := syscall.IPPROTO_IP, ipMTU
if !is4 {
level, opt = syscall.IPPROTO_IPV6, ipv6MTU
}
mtu, err := syscall.GetsockoptInt(fd, level, opt)
if err != nil || mtu <= 0 {
res.Err = "getsockopt IP_MTU: " + errStr(err)
return res
}
res.DiscoveredMTU = mtu
res.FullMTU = mtu >= 1500
return res
}
func sockaddr(a netip.Addr, port int) syscall.Sockaddr {
if a.Is4() {
return &syscall.SockaddrInet4{Port: port, Addr: a.As4()}
}
return &syscall.SockaddrInet6{Port: port, Addr: a.As16()}
}
func errStr(err error) string {
if err == nil {
return "nil"
}
return err.Error()
}