diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go index 0be82e7..c0a34d8 100644 --- a/x/xnet/stack-async.go +++ b/x/xnet/stack-async.go @@ -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) diff --git a/x/xnet/xnet_test.go b/x/xnet/xnet_test.go index fc83a25..0b7b6b9 100644 --- a/x/xnet/xnet_test.go +++ b/x/xnet/xnet_test.go @@ -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) + } +}