package pmtud import ( "context" "errors" "fmt" "net/netip" "time" "github.com/qdm12/gluetun/internal/firewall" "github.com/qdm12/gluetun/internal/pmtud/constants" "github.com/qdm12/gluetun/internal/pmtud/icmp" "github.com/qdm12/gluetun/internal/pmtud/tcp" ) var ErrPMTUDFailICMPAndTCP = errors.New("PMTUD failed with both ICMP and TCP") // PathMTUDiscover discovers the maximum MTU using both ICMP and TCP. // Multiple ICMP addresses and TCP addresses can be specified for redundancy. // ICMP PMTUD is run first. If successful, the range of possible MTU values to // check for TCP PMTUD is reduced to [maxMTU-150, maxMTU] where maxMTU is the // maximum MTU found with ICMP PMTUD. Otherwise, TCP PMTUD is run with the // whole range of possible MTU values up to the physical link MTU to check. // If the physicalLinkMTU is zero, it defaults to 1500 which is the ethernet standard MTU. // If the pingTimeout is zero, it defaults to 1 second. // If the logger is nil, a no-op logger is used. // It returns [ErrMTUNotFound] if the MTU could not be determined. func PathMTUDiscover(ctx context.Context, icmpAddrs []netip.Addr, tcpAddrs []netip.AddrPort, physicalLinkMTU uint32, tryTimeout time.Duration, fw tcp.Firewall, logger Logger) ( mtu uint32, err error, ) { if physicalLinkMTU == 0 { const ethernetStandardMTU = 1500 physicalLinkMTU = ethernetStandardMTU } if tryTimeout == 0 { tryTimeout = time.Second } if logger == nil { logger = &noopLogger{} } // Try finding the MTU using ICMP maxPossibleMTU := physicalLinkMTU icmpSuccess := false for _, icmpIP := range icmpAddrs { mtu, err := icmp.PathMTUDiscover(ctx, icmpIP, physicalLinkMTU, tryTimeout, logger) switch { case err == nil: logger.Debugf("ICMP path MTU discovery against %s found maximum valid MTU %d", icmpIP, mtu) icmpSuccess = true maxPossibleMTU = mtu case errors.Is(err, icmp.ErrNotPermitted), errors.Is(err, icmp.ErrMTUNotFound): logger.Debugf("ICMP path MTU discovery failed: %s", err) default: return 0, fmt.Errorf("ICMP path MTU discovery: %w", err) } if icmpSuccess { break } } for _, addrPort := range tcpAddrs { minMTU := constants.MinIPv4MTU if addrPort.Addr().Is6() { minMTU = constants.MinIPv6MTU } if icmpSuccess { const mtuMargin = 150 minMTU = max(maxPossibleMTU-mtuMargin, minMTU) } mtu, err = tcp.PathMTUDiscover(ctx, addrPort, minMTU, maxPossibleMTU, fw, logger) if err != nil { if errors.Is(err, firewall.ErrMarkMatchModuleMissing) { logger.Debugf("aborting TCP path MTU discovery: %s", err) if icmpSuccess { return maxPossibleMTU, nil // only rely on ICMP PMTUD results } return 0, fmt.Errorf("%w", ErrPMTUDFailICMPAndTCP) } logger.Debugf("TCP path MTU discovery to %s failed: %s", addrPort, err) continue } logger.Debugf("TCP path MTU discovery to %s found maximum valid MTU %d", addrPort, mtu) return mtu, nil } // TCP PMTUD failed for all addresses for external reasons, // so do not take the risk and return an error. return 0, fmt.Errorf("TCP path MTU discovery: last error: %w", err) }