IP_MTU getsockopt returns ENOTCONN on an unconnected socket; the v0.3.4 probe set IP_MTU_DISCOVER and Sendto but never Connect'd, so every probe errored. UDP-connect (no handshake) pins the route so IP_MTU reflects the path; switched to Write (two return values). Sysctl audit already flagged the four real fmr issues in v0.3.4; this makes the MTU proof report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
// 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)
|
||
}
|
||
|
||
// IP_MTU reflects the CONNECTED path's MTU, so the socket must be connected
|
||
// (an unconnected socket returns ENOTCONN). No handshake — UDP connect just
|
||
// pins the destination and resolves the route.
|
||
sa := sockaddr(addr, 33434)
|
||
if err := syscall.Connect(fd, sa); err != nil {
|
||
res.Err = "connect: " + errStr(err)
|
||
return res
|
||
}
|
||
|
||
// Full-size probe: 1500 total − IP/UDP headers (28 v4, 48 v6). 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.
|
||
payload := 1472
|
||
if !is4 {
|
||
payload = 1452
|
||
}
|
||
probe := make([]byte, payload)
|
||
_, _ = syscall.Write(fd, probe)
|
||
time.Sleep(700 * time.Millisecond)
|
||
_, _ = syscall.Write(fd, probe) // 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()
|
||
}
|