mirror of
https://github.com/qdm12/gluetun.git
synced 2026-05-07 04:20:12 +02:00
be92aa2ac4
- Existing option `WIREGUARD_MTU` , if set, disables PMTUD and is used - New option `PMTUD_ICMP_ADDRESSES=1.1.1.1,8.8.8.8` and `PMTUD_TCP_ADDRESSES=1.1.1.1:443,8.8.8.8:443` - ICMP PMTUD now targets external-by-default IP addresses - New TCP PMTUD (binary search only) as a second MTU confirmation and fallback mechanism. - Force set TCP MSS to MTU - IP header - TCP base header - "magic 20 bytes" 🎆 - Fix #3108
164 lines
5.0 KiB
Go
164 lines
5.0 KiB
Go
package icmp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/qdm12/gluetun/internal/pmtud/constants"
|
|
"golang.org/x/net/icmp"
|
|
"golang.org/x/net/ipv4"
|
|
)
|
|
|
|
const (
|
|
icmpv4Protocol = 1
|
|
)
|
|
|
|
func listenICMPv4(ctx context.Context) (conn net.PacketConn, err error) {
|
|
var listenConfig net.ListenConfig
|
|
listenConfig.Control = func(_, _ string, rawConn syscall.RawConn) error {
|
|
var setDFErr error
|
|
err := rawConn.Control(func(fd uintptr) {
|
|
setDFErr = setDontFragment(fd) // runs when calling ListenPacket
|
|
})
|
|
if err == nil {
|
|
err = setDFErr
|
|
}
|
|
return err
|
|
}
|
|
|
|
const listenAddress = ""
|
|
packetConn, err := listenConfig.ListenPacket(ctx, "ip4:icmp", listenAddress)
|
|
if err != nil {
|
|
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
|
|
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrNotPermitted)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
|
packetConn = ipv4ToNetPacketConn(ipv4.NewPacketConn(packetConn))
|
|
}
|
|
|
|
return packetConn, nil
|
|
}
|
|
|
|
func findIPv4NextHopMTU(ctx context.Context, ip netip.Addr,
|
|
physicalLinkMTU uint32, pingTimeout time.Duration, logger Logger,
|
|
) (mtu uint32, err error) {
|
|
if ip.Is6() {
|
|
panic("IP address is not v4")
|
|
}
|
|
conn, err := listenICMPv4(ctx)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("listening for ICMP packets: %w", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
|
defer cancel()
|
|
go func() {
|
|
<-ctx.Done()
|
|
conn.Close()
|
|
}()
|
|
|
|
// First try to send a packet which is too big to get the maximum MTU
|
|
// directly.
|
|
outboundID, outboundMessage := buildMessageToSend("v4", physicalLinkMTU)
|
|
encodedMessage, err := outboundMessage.Marshal(nil)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("encoding ICMP message: %w", err)
|
|
}
|
|
|
|
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()})
|
|
if err != nil {
|
|
err = wrapConnErr(err, ctx, pingTimeout)
|
|
return 0, fmt.Errorf("writing ICMP message: %w", err)
|
|
}
|
|
|
|
buffer := make([]byte, physicalLinkMTU)
|
|
|
|
// for loop in case we read an ICMP message from another ICMP request
|
|
// or TCP/UDP traffic triggering an ICMP response.
|
|
for {
|
|
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
|
|
// must be large enough to read the entire reply packet. See:
|
|
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
|
|
bytesRead, _, err := conn.ReadFrom(buffer)
|
|
if err != nil {
|
|
err = wrapConnErr(err, ctx, pingTimeout)
|
|
return 0, fmt.Errorf("reading from ICMP connection: %w", err)
|
|
}
|
|
packetBytes := buffer[:bytesRead]
|
|
// Side note: echo reply should be at most the number of bytes
|
|
// sent, and can be lower, more precisely 576-ipHeader bytes,
|
|
// in case the next hop we are reaching replies with a destination
|
|
// unreachable and wants to ensure the response makes it way back
|
|
// by keeping a low packet size, see:
|
|
// https://datatracker.ietf.org/doc/html/rfc1122#page-59
|
|
|
|
inboundMessage, err := icmp.ParseMessage(icmpv4Protocol, packetBytes)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parsing message: %w", err)
|
|
}
|
|
|
|
switch typedBody := inboundMessage.Body.(type) {
|
|
case *icmp.DstUnreach:
|
|
const fragmentationRequiredAndDFFlagSetCode = 4
|
|
const portUnreachable = 3
|
|
const communicationAdministrativelyProhibitedCode = 13
|
|
switch inboundMessage.Code {
|
|
case fragmentationRequiredAndDFFlagSetCode:
|
|
case portUnreachable: // triggered by TCP or UDP from applications
|
|
continue // ignore and wait for the next message
|
|
case communicationAdministrativelyProhibitedCode:
|
|
return 0, fmt.Errorf("%w: %w (code %d)",
|
|
ErrDestinationUnreachable,
|
|
ErrCommunicationAdministrativelyProhibited,
|
|
inboundMessage.Code)
|
|
default:
|
|
return 0, fmt.Errorf("%w: code %d",
|
|
ErrDestinationUnreachable, inboundMessage.Code)
|
|
}
|
|
|
|
// See https://datatracker.ietf.org/doc/html/rfc1191#section-4
|
|
// Note: the go library does not handle this NextHopMTU section.
|
|
nextHopMTU := packetBytes[6:8]
|
|
mtu = uint32(binary.BigEndian.Uint16(nextHopMTU))
|
|
err = checkMTU(mtu, constants.MinIPv4MTU, physicalLinkMTU)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("checking next-hop-mtu found: %w", err)
|
|
}
|
|
|
|
// The code below is really for sanity checks
|
|
packetBytes = packetBytes[8:]
|
|
header, err := ipv4.ParseHeader(packetBytes)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parsing IPv4 header: %w", err)
|
|
}
|
|
packetBytes = packetBytes[header.Len:] // truncated original datagram
|
|
|
|
const truncated = true
|
|
err = checkEchoReply(icmpv4Protocol, packetBytes, outboundMessage, truncated)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("checking echo reply: %w", err)
|
|
}
|
|
return mtu, nil
|
|
case *icmp.Echo:
|
|
inboundID := uint16(typedBody.ID) //nolint:gosec
|
|
if inboundID == outboundID {
|
|
return physicalLinkMTU, nil
|
|
}
|
|
logger.Debugf("discarding received ICMP echo reply with id %d mismatching sent id %d",
|
|
inboundID, outboundID)
|
|
continue
|
|
default:
|
|
return 0, fmt.Errorf("%w: %T", ErrBodyUnsupported, typedBody)
|
|
}
|
|
}
|
|
}
|