// 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() }