hotfix(pmtud/tcp): respect MSS from server into account

This commit is contained in:
Quentin McGaw
2026-02-18 18:32:10 +00:00
parent 183d351b58
commit 224618337c
2 changed files with 233 additions and 40 deletions
+199 -25
View File
@@ -64,10 +64,14 @@ func tcpChecksum(ipHeader, tcpHeader, payload []byte) uint16 {
const (
tcpFlagsOffset = 13
rstFlag byte = 0x04
finFlag byte = 0x01
synFlag byte = 0x02
ackFlag byte = 0x10
rstFlag byte = 0x04
pshFlag byte = 0x08
ackFlag byte = 0x10
urgFlag byte = 0x20
eceFlag byte = 0x40
cwrFlag byte = 0x80
)
type packetType uint8
@@ -75,8 +79,12 @@ type packetType uint8
const (
packetTypeSYN packetType = iota + 1
packetTypeSYNACK
packetTypeACK
packetTypeFIN
packetTypeFINACK
packetTypeRST
packetTypeRSTACK
packetTypePSHACK
packetTypeACK
)
func (p packetType) String() string {
@@ -85,40 +93,206 @@ func (p packetType) String() string {
return "SYN"
case packetTypeSYNACK:
return "SYN-ACK"
case packetTypeACK:
return "ACK"
case packetTypeFIN:
return "FIN"
case packetTypeFINACK:
return "FIN-ACK"
case packetTypeRST:
return "RST"
case packetTypeRSTACK:
return "RST-ACK"
case packetTypePSHACK:
return "PSH-ACK"
case packetTypeACK:
return "ACK"
default:
panic("unknown packet type")
}
}
type tcpHeader struct {
typ packetType
srcPort uint16
dstPort uint16
seq uint32
ack uint32
dataOffset uint8
flags uint8
windowSize uint16
checksum uint16
urgentPtr uint16
options options
}
var (
errTCPHeaderTooShort = errors.New("TCP header is too short")
errTCPHeaderDataOffset = errors.New("TCP header data offset is invalid")
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)
// parseTCPHeader parses the TCP header from b.
// b should be the entire TCP packet bytes.
func parseTCPHeader(b []byte) (header tcpHeader, err error) {
if len(b) < int(constants.BaseTCPHeaderLength) {
return tcpHeader{}, fmt.Errorf("%w: %d bytes", errTCPHeaderTooShort, len(b))
}
seq = binary.BigEndian.Uint32(header[4:8])
ack = binary.BigEndian.Uint32(header[8:12])
return packetType, seq, ack, nil
header.srcPort = binary.BigEndian.Uint16(b[0:2])
header.dstPort = binary.BigEndian.Uint16(b[2:4])
header.seq = binary.BigEndian.Uint32(b[4:8])
header.ack = binary.BigEndian.Uint32(b[8:12])
// upper 4 bits of the 12th byte
header.dataOffset = (b[12] >> 4) * 4 //nolint:mnd
header.flags = b[13]
header.windowSize = binary.BigEndian.Uint16(b[14:16])
header.checksum = binary.BigEndian.Uint16(b[16:18])
header.urgentPtr = binary.BigEndian.Uint16(b[18:20])
switch {
case uint32(header.dataOffset) < constants.BaseTCPHeaderLength:
return tcpHeader{}, fmt.Errorf("%w: data offset is %d bytes, expected at least %d bytes",
errTCPHeaderDataOffset, header.dataOffset, constants.BaseTCPHeaderLength)
case int(header.dataOffset) > len(b):
return tcpHeader{}, fmt.Errorf("%w: data offset is %d bytes, but packet is only %d bytes",
errTCPHeaderDataOffset, header.dataOffset, len(b))
}
if uint32(header.dataOffset) > constants.BaseTCPHeaderLength {
optionsBytes := b[constants.BaseTCPHeaderLength:header.dataOffset]
header.options, err = parseTCPOptions(optionsBytes)
if err != nil {
return tcpHeader{}, fmt.Errorf("parsing TCP options: %w", err)
}
}
flags := header.flags
switch {
case flags&synFlag != 0:
if flags&ackFlag != 0 {
header.typ = packetTypeSYNACK
} else {
header.typ = packetTypeSYN
}
case flags&rstFlag != 0:
if flags&ackFlag != 0 {
header.typ = packetTypeRSTACK
} else {
header.typ = packetTypeRST
}
case flags&finFlag != 0:
if flags&ackFlag != 0 {
header.typ = packetTypeFINACK
} else {
header.typ = packetTypeFIN
}
case flags&pshFlag != 0:
header.typ = packetTypePSHACK
case flags&ackFlag != 0:
header.typ = packetTypeACK
default:
return tcpHeader{}, fmt.Errorf("%w: flags are 0x%02x", errTCPPacketTypeUnknown, flags)
}
header.seq = binary.BigEndian.Uint32(b[4:8])
header.ack = binary.BigEndian.Uint32(b[8:12])
return header, nil
}
type options struct {
mss uint16
windowScale *uint8 // Pointer to differentiate between 0 and "not present"
sackPermitted bool
timestamps *optionTimestamps
}
type optionTimestamps struct {
value uint32
echo uint32
}
var (
errTCPOptionLengthTruncated = errors.New("TCP option length is truncated")
ErrTCPOptionLengthInvalid = errors.New("TCP option length is invalid")
ErrTCPOptionMSSInvalid = errors.New("TCP option MSS value is invalid")
ErrTCPOptionWindowScaleInvalid = errors.New("TCP option Window Scale value is invalid")
ErrTCPOptionTimestampsInvalid = errors.New("TCP option Timestamps value is invalid")
errTCPOptionTypeUnknown = errors.New("TCP option type is unknown")
)
func parseTCPOptions(b []byte) (parsed options, err error) {
i := 0
for i < len(b) {
optionType := b[i]
// Handle single-byte options
if optionType == 0 { // End of List
break
}
if optionType == 1 { // No-Operation (Padding)
i++
continue
}
// Handle TLV (Type-Length-Value) options
if i+1 >= len(b) {
// This should not happen for DF packets.
return options{}, fmt.Errorf("%w: at offset %d", errTCPOptionLengthTruncated, i)
}
length := int(b[i+1])
const minLength = 2
maxLength := len(b) - i
switch {
case length < minLength:
return options{}, fmt.Errorf("%w: type %d at offset %d has length %d < %d",
ErrTCPOptionLengthInvalid, optionType, i, length, minLength)
case length > maxLength:
return options{}, fmt.Errorf("%w: type %d at offset %d has length %d > %d",
ErrTCPOptionLengthInvalid, optionType, i, length, maxLength)
}
data := b[i+2 : i+length]
const (
optionTypeMSS = 2
optionTypeWindowScale = 3
optionTypeSACKPermitted = 4
optionTypeTimestamps = 8
)
switch optionType {
case optionTypeMSS:
const expectedLength = 4
if length != expectedLength {
return options{}, fmt.Errorf("%w: MSS option at offset %d has length %d, expected %d",
ErrTCPOptionMSSInvalid, i, length, expectedLength)
}
parsed.mss = binary.BigEndian.Uint16(data)
case optionTypeWindowScale:
const expectedLength = 3
if length != expectedLength {
return options{}, fmt.Errorf("%w: window scale option at offset %d has length %d, expected %d",
ErrTCPOptionWindowScaleInvalid, i, length, expectedLength)
}
windowScale := data[0]
parsed.windowScale = &windowScale
case optionTypeSACKPermitted:
parsed.sackPermitted = true
case optionTypeTimestamps:
const expectedLength = 10
if length != expectedLength {
return options{}, fmt.Errorf("%w: timestamps option at offset %d has length %d, expected %d",
ErrTCPOptionTimestampsInvalid, i, length, expectedLength)
}
parsed.timestamps = &optionTimestamps{
value: binary.BigEndian.Uint32(data[:4]),
echo: binary.BigEndian.Uint32(data[4:]),
}
default:
return options{}, fmt.Errorf("%w: type %d", errTCPOptionTypeUnknown, optionType)
}
i += length
}
return parsed, nil
}