bound MTU on Stack EgressIP (#151)

This commit is contained in:
Pat Whittingslow
2026-07-01 18:46:24 -03:00
committed by GitHub
parent cdc99fb9ff
commit 4d6363da00
2 changed files with 67 additions and 1 deletions
+6 -1
View File
@@ -165,9 +165,14 @@ func (s *StackAsync) IngressIP(ipFrame []byte) error {
func (s *StackAsync) EgressIP(dstIPFrame []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(dstIPFrame) < s.link.MTU() {
mtu := s.link.MTU()
if len(dstIPFrame) < mtu {
return 0, lneto.ErrShortBuffer
}
// Clip to MTU so downstream layers cannot emit an IP datagram larger than the
// link MTU (mirrors StackEthernet.Encapsulate). This also bounds the TCP frame
// budget, so the advertised MSS becomes MTU-ipHdr-20 instead of the buffer size.
dstIPFrame = dstIPFrame[:mtu]
n, err := s.ip4.Encapsulate(dstIPFrame, 0, 0)
if s.ipv6enabled && n == 0 {
n, err = s.stack6.EgressIPv6(dstIPFrame)
+61
View File
@@ -1135,3 +1135,64 @@ func getTCPFrame(etherFrame []byte) (tcp.Frame, bool) {
func backoffYield(consecutiveBackoffs uint) time.Duration {
return lneto.BackoffFlagGosched
}
// TestEgressIP_TCPMSSAdvertisesMTU guards against advertising a Maximum Segment Size
// derived from the (oversized) egress buffer instead of the link MTU. See the
// EgressIP clip in StackAsync: without it the SYN advertises ~65479 rather than
// MTU-ipHdr-20.
func TestEgressIP_TCPMSSAdvertisesMTU(t *testing.T) {
const mtu = 1280
const wantMSS = uint16(mtu - 20 - 20) // -IPv4 header -TCP header = 1240.
s1, s2, c1, _ := newTCPStacks(t, 4, mtu)
raddr := s2.Addr4()
err := s1.DialTCP4(c1, 12345, raddr, 80)
if err != nil {
t.Fatal(err)
}
// Emit the client SYN through the IP (TUN) egress path using a buffer far
// larger than the MTU. The advertised MSS must reflect the MTU, not len(buf).
buf := make([]byte, 65535)
n, err := s1.EgressIP(buf)
if err != nil {
t.Fatal(err)
} else if n == 0 {
t.Fatal("no SYN emitted")
} else if n > mtu {
t.Fatalf("emitted IP datagram %d exceeds MTU %d", n, mtu)
}
ifrm, err := ipv4.NewFrame(buf[:n])
if err != nil {
t.Fatal(err)
}
tfrm, err := tcp.NewFrame(ifrm.Payload())
if err != nil {
t.Fatal(err)
}
seg := tfrm.Segment(len(tfrm.Payload()))
if !seg.Flags.HasAny(tcp.FlagSYN) {
t.Fatalf("expected SYN, got flags %s", seg.Flags.String())
}
var op tcp.OptionCodec
gotMSS := uint16(0)
found := false
err = op.ForEachOption(tfrm.Options(), func(kind tcp.OptionKind, data []byte) error {
if kind == tcp.OptMaxSegmentSize && len(data) == 2 {
gotMSS = uint16(data[0])<<8 | uint16(data[1])
found = true
}
return nil
})
if err != nil {
t.Fatal(err)
}
if !found {
t.Fatal("no MSS option in SYN")
}
if gotMSS != wantMSS {
t.Errorf("advertised MSS = %d, want %d (MTU %d - 40)", gotMSS, wantMSS, mtu)
}
}