Path MTU discovery fixes and improvements (#3109)

- 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
This commit is contained in:
Quentin McGaw
2026-02-15 01:40:34 +01:00
committed by GitHub
parent 8f1fda7646
commit be92aa2ac4
59 changed files with 2050 additions and 376 deletions
+124
View File
@@ -0,0 +1,124 @@
package tcp
import (
"encoding/binary"
"errors"
"fmt"
"github.com/qdm12/gluetun/internal/pmtud/constants"
)
// For SYN, ack is 0.
// For SYN-ACK, ack is the sequence number + 1 of the SYN.
func makeTCPHeader(srcPort, dstPort uint16, seq, ack uint32, flags byte) []byte {
header := make([]byte, constants.BaseTCPHeaderLength)
binary.BigEndian.PutUint16(header[0:], srcPort)
binary.BigEndian.PutUint16(header[2:], dstPort)
binary.BigEndian.PutUint32(header[4:], seq)
binary.BigEndian.PutUint32(header[8:], ack)
//nolint:mnd
header[12] = byte(constants.BaseTCPHeaderLength) << 2 // data offset
header[13] = flags
// windowSize can be left to 5840 even for IPv6, it doesn't matter.
const windowSize = 5840
binary.BigEndian.PutUint16(header[14:], windowSize)
// header[16:17] is the checksum, set later
// header[18:19] is urgent pointer, not needed for our use case
return header
}
//nolint:mnd
func tcpChecksum(ipHeader, tcpHeader, payload []byte) uint16 {
var pseudoHeader []byte
isIPv6 := len(ipHeader) >= 40 && (ipHeader[0]>>4) == 6
if isIPv6 {
pseudoHeader = make([]byte, 40)
copy(pseudoHeader[0:16], ipHeader[8:24]) // Source Address
copy(pseudoHeader[16:32], ipHeader[24:40]) // Destination Address
totalLength := uint32(len(tcpHeader) + len(payload)) //nolint:gosec
binary.BigEndian.PutUint32(pseudoHeader[32:], totalLength)
pseudoHeader[39] = 6 // Next Header (TCP)
} else {
pseudoHeader = make([]byte, 12)
copy(pseudoHeader[0:4], ipHeader[12:16])
copy(pseudoHeader[4:8], ipHeader[16:20])
pseudoHeader[9] = 6
totalLength := uint16(len(tcpHeader) + len(payload)) //nolint:gosec
binary.BigEndian.PutUint16(pseudoHeader[10:], totalLength)
}
sum := uint32(0)
for _, slice := range [][]byte{pseudoHeader, tcpHeader, payload} {
for i := 0; i < len(slice)-1; i += 2 {
sum += uint32(binary.BigEndian.Uint16(slice[i : i+2]))
}
if len(slice)%2 != 0 {
sum += uint32(slice[len(slice)-1]) << 8
}
}
for (sum >> 16) > 0 {
sum = (sum & 0xFFFF) + (sum >> 16)
}
return ^uint16(sum) //nolint:gosec
}
const (
tcpFlagsOffset = 13
rstFlag byte = 0x04
synFlag byte = 0x02
ackFlag byte = 0x10
pshFlag byte = 0x08
)
type packetType uint8
const (
packetTypeSYN packetType = iota + 1
packetTypeSYNACK
packetTypeACK
packetTypeRST
)
func (p packetType) String() string {
switch p {
case packetTypeSYN:
return "SYN"
case packetTypeSYNACK:
return "SYN-ACK"
case packetTypeACK:
return "ACK"
case packetTypeRST:
return "RST"
default:
panic("unknown packet type")
}
}
var (
errTCPHeaderTooShort = errors.New("TCP header is too short")
errTCPPacketTypeUnknown = errors.New("TCP packet type is unknown")
)
// parseTCPHeader parses some elements from the TCP header.
func parseTCPHeader(header []byte) (packetType packetType, seq, ack uint32, err error) {
if len(header) < int(constants.BaseTCPHeaderLength) {
return 0, 0, 0, fmt.Errorf("%w: %d bytes", errTCPHeaderTooShort, len(header))
}
flags := header[tcpFlagsOffset]
switch {
case (flags&synFlag) != 0 && (flags&ackFlag) == 0:
packetType = packetTypeSYN
case (flags&synFlag) != 0 && (flags&ackFlag) != 0:
packetType = packetTypeSYNACK
case (flags & rstFlag) != 0:
packetType = packetTypeRST
case (flags & ackFlag) != 0:
packetType = packetTypeACK
default:
return 0, 0, 0, fmt.Errorf("%w: flags are 0x%02x", errTCPPacketTypeUnknown, flags)
}
seq = binary.BigEndian.Uint32(header[4:8])
ack = binary.BigEndian.Uint32(header[8:12])
return packetType, seq, ack, nil
}