From 078be6271220e904d45e220e2c0ed5693a821a1b Mon Sep 17 00:00:00 2001 From: Lucas Manning Date: Tue, 3 Dec 2024 15:16:31 -0800 Subject: [PATCH] Add support for setting the experiment option header in IPv4. PiperOrigin-RevId: 702486039 --- pkg/tcpip/header/ipv4.go | 65 +++++++++++++++++++ pkg/tcpip/network/ipv4/ipv4.go | 3 + pkg/tcpip/transport/tcp/connect.go | 19 +++--- pkg/tcpip/transport/tcp/test/e2e/tcp_test.go | 57 ++++++++++++++++ .../transport/tcp/testing/context/context.go | 8 ++- .../transport/testing/context/context.go | 6 +- pkg/tcpip/transport/udp/udp_test.go | 43 ++++++++++++ 7 files changed, 189 insertions(+), 12 deletions(-) diff --git a/pkg/tcpip/header/ipv4.go b/pkg/tcpip/header/ipv4.go index d6801199ab..a0f1cabbba 100644 --- a/pkg/tcpip/header/ipv4.go +++ b/pkg/tcpip/header/ipv4.go @@ -603,6 +603,9 @@ const ( // IPv4OptionTimestampType is the option type for the Timestamp option. IPv4OptionTimestampType IPv4OptionType = 68 + // IPv4OptionExperimentType is the option type for the Experiment option. + IPv4OptionExperimentType IPv4OptionType = 30 + // ipv4OptionTypeOffset is the offset in an option of its type field. ipv4OptionTypeOffset = 0 @@ -800,6 +803,17 @@ func (i *IPv4OptionIterator) Next() (IPv4Option, bool, *IPv4OptParameterProblem) } retval := IPv4OptionRouterAlert(optionBody) return &retval, false, nil + + case IPv4OptionExperimentType: + if optLen != IPv4OptionExperimentLength { + i.ErrCursor++ + return nil, false, &IPv4OptParameterProblem{ + Pointer: i.ErrCursor, + NeedICMP: true, + } + } + retval := IPv4OptionExperiment(optionBody) + return &retval, false, nil } retval := IPv4OptionGeneric(optionBody) return &retval, false, nil @@ -1074,6 +1088,35 @@ func (ra *IPv4OptionRouterAlert) Value() uint16 { return binary.BigEndian.Uint16(ra.Contents()[IPv4OptionRouterAlertValueOffset:]) } +// Experiment option specific related constants. +const ( + // IPv4OptionExperimentLength is the length of an Experiment option. + IPv4OptionExperimentLength = 4 + + // IPv4OptionExperimentValueOffset is the offset for the value of an + // Experiment option. + IPv4OptionExperimentValueOffset = 2 +) + +var _ IPv4Option = (*IPv4OptionExperiment)(nil) + +// IPv4OptionExperiment is an IPv4 option defined by RFC 4727. +type IPv4OptionExperiment []byte + +// Type implements IPv4Option. +func (*IPv4OptionExperiment) Type() IPv4OptionType { return IPv4OptionExperimentType } + +// Size implements IPv4Option. +func (*IPv4OptionExperiment) Size() uint8 { return uint8(IPv4OptionExperimentLength) } + +// Contents implements IPv4Option. +func (ex *IPv4OptionExperiment) Contents() []byte { return *ex } + +// Value returns the value of the IPv4OptionRouterAlert. +func (ex *IPv4OptionExperiment) Value() uint16 { + return binary.BigEndian.Uint16(ex.Contents()[IPv4OptionExperimentValueOffset:]) +} + // IPv4SerializableOption is an interface to represent serializable IPv4 option // types. type IPv4SerializableOption interface { @@ -1179,6 +1222,28 @@ func (o *IPv4SerializableRouterAlertOption) serializeInto(buffer []byte) uint8 { return o.length() } +var _ IPv4SerializableOptionPayload = (*IPv4SerializableExperimentOption)(nil) +var _ IPv4SerializableOption = (*IPv4SerializableExperimentOption)(nil) + +// IPv4SerializableExperimentOption provides serialization for the IPv4 +// Experiment option. +type IPv4SerializableExperimentOption struct { + Tag uint16 +} + +func (*IPv4SerializableExperimentOption) optionType() IPv4OptionType { + return IPv4OptionExperimentType +} + +func (*IPv4SerializableExperimentOption) length() uint8 { + return IPv4OptionExperimentLength - IPv4OptionExperimentValueOffset +} + +func (o *IPv4SerializableExperimentOption) serializeInto(buffer []byte) uint8 { + binary.BigEndian.PutUint16(buffer, o.Tag) + return o.length() +} + var _ IPv4SerializableOption = (*IPv4SerializableNOPOption)(nil) // IPv4SerializableNOPOption provides serialization for the IPv4 no-op option. diff --git a/pkg/tcpip/network/ipv4/ipv4.go b/pkg/tcpip/network/ipv4/ipv4.go index dbaaf532fe..bb4bcb6ed3 100644 --- a/pkg/tcpip/network/ipv4/ipv4.go +++ b/pkg/tcpip/network/ipv4/ipv4.go @@ -449,6 +449,9 @@ func (e *endpoint) getID() uint16 { } func (e *endpoint) addIPHeader(srcAddr, dstAddr tcpip.Address, pkt *stack.PacketBuffer, params stack.NetworkHeaderParams, options header.IPv4OptionsSerializer) tcpip.Error { + if expVal := params.ExperimentOptionValue; expVal != 0 { + options = append(options, &header.IPv4SerializableExperimentOption{Tag: expVal}) + } hdrLen := header.IPv4MinimumSize var optLen int if options != nil { diff --git a/pkg/tcpip/transport/tcp/connect.go b/pkg/tcpip/transport/tcp/connect.go index 8332bab184..492afe24bc 100644 --- a/pkg/tcpip/transport/tcp/connect.go +++ b/pkg/tcpip/transport/tcp/connect.go @@ -1020,15 +1020,16 @@ func (e *Endpoint) sendRaw(pkt *stack.PacketBuffer, flags header.TCPFlags, seq, defer putOptions(options) pkt.ReserveHeaderBytes(header.TCPMinimumSize + int(e.route.MaxHeaderLength()) + len(options)) return e.sendTCP(e.route, tcpFields{ - id: e.TransportEndpointInfo.ID, - ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit), - tos: e.sendTOS, - flags: flags, - seq: seq, - ack: ack, - rcvWnd: rcvWnd, - opts: options, - df: e.pmtud == tcpip.PMTUDiscoveryWant || e.pmtud == tcpip.PMTUDiscoveryDo, + id: e.TransportEndpointInfo.ID, + ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit), + tos: e.sendTOS, + flags: flags, + seq: seq, + ack: ack, + rcvWnd: rcvWnd, + opts: options, + df: e.pmtud == tcpip.PMTUDiscoveryWant || e.pmtud == tcpip.PMTUDiscoveryDo, + expOptVal: e.getExperimentOptionValue(e.route), }, pkt, e.gso) } diff --git a/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go b/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go index b6841b375a..1dcb19a7c2 100644 --- a/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go +++ b/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go @@ -9421,6 +9421,63 @@ func TestLateSynCookieAck(t *testing.T) { } } +func TestSetExperimentOption(t *testing.T) { + c := context.NewWithOpts(t, context.Options{ + EnableV4: true, + MTU: e2e.DefaultMTU, + EnableExperimentIPOption: true, + }) + defer c.Cleanup() + + c.CreateConnected(context.TestInitialSequenceNumber, 30000, -1 /* epRcvBuf */) + + var expval uint16 = 99 + c.EP.SocketOptions().SetExperimentOptionValue(expval) + + var r bytes.Reader + r.Reset(make([]byte, 1)) + _, err := c.EP.Write(&r, tcpip.WriteOptions{}) + if err != nil { + t.Fatalf("Write failed: %s", err) + } + + v := c.GetPacket() + defer v.Release() + want := header.IPv4Options{ + byte(header.IPv4OptionExperimentType), + byte(header.IPv4OptionExperimentLength), + 0, + byte(expval), + } + checker.IPv4(t, v, checker.IPv4Options(want)) +} + +func TestSetExperimentOptionWithOptionDisabled(t *testing.T) { + c := context.NewWithOpts(t, context.Options{ + EnableV4: true, + MTU: e2e.DefaultMTU, + EnableExperimentIPOption: false, + }) + defer c.Cleanup() + + c.CreateConnected(context.TestInitialSequenceNumber, 30000, -1 /* epRcvBuf */) + + var expval uint16 = 99 + c.EP.SocketOptions().SetExperimentOptionValue(expval) + + var r bytes.Reader + r.Reset(make([]byte, 1)) + _, err := c.EP.Write(&r, tcpip.WriteOptions{}) + if err != nil { + t.Fatalf("Write failed: %s", err) + } + + v := c.GetPacket() + defer v.Release() + want := header.IPv4Options{} + checker.IPv4(t, v, checker.IPv4Options(want)) +} + func TestMain(m *testing.M) { refs.SetLeakMode(refs.LeaksPanic) code := m.Run() diff --git a/pkg/tcpip/transport/tcp/testing/context/context.go b/pkg/tcpip/transport/tcp/testing/context/context.go index 50ac740fc6..5f38c1621b 100644 --- a/pkg/tcpip/transport/tcp/testing/context/context.go +++ b/pkg/tcpip/transport/tcp/testing/context/context.go @@ -132,6 +132,10 @@ type Options struct { // Probe is a probe function to attach to the stack. Probe tcp.TCPProbeFunc + + // EnableExperimentIPOption indicates whether the NIC is responsible for + // passing the experiment IP option. + EnableExperimentIPOption bool } // Context provides an initialized Network stack and a link layer endpoint @@ -247,7 +251,7 @@ func NewWithOpts(t *testing.T, opts Options) *Context { if testing.Verbose() { wep = sniffer.New(ep) } - nicOpts := stack.NICOptions{Name: "nic1"} + nicOpts := stack.NICOptions{Name: "nic1", EnableExperimentIPOption: opts.EnableExperimentIPOption} if err := s.CreateNICWithOptions(1, wep, nicOpts); err != nil { t.Fatalf("CreateNICWithOptions(_, _, %+v) failed: %v", opts, err) } @@ -255,7 +259,7 @@ func NewWithOpts(t *testing.T, opts Options) *Context { if testing.Verbose() { wep2 = sniffer.New(channel.New(1000, opts.MTU, "")) } - opts2 := stack.NICOptions{Name: "nic2"} + opts2 := stack.NICOptions{Name: "nic2", EnableExperimentIPOption: opts.EnableExperimentIPOption} if err := s.CreateNICWithOptions(2, wep2, opts2); err != nil { t.Fatalf("CreateNICWithOptions(_, _, %+v) failed: %v", opts2, err) } diff --git a/pkg/tcpip/transport/testing/context/context.go b/pkg/tcpip/transport/testing/context/context.go index 85e0689b0e..eae17e7120 100644 --- a/pkg/tcpip/transport/testing/context/context.go +++ b/pkg/tcpip/transport/testing/context/context.go @@ -75,6 +75,10 @@ type Options struct { // HandleLocal specifies if non-loopback interfaces are allowed to loop // packets. HandleLocal bool + + // EnableExperimentIPOption indicates whether the NIC is responsible for + // passing the experiment IP option. + EnableExperimentIPOption bool } // New allocates and initializes a test context containing a configured stack. @@ -112,7 +116,7 @@ func NewWithOptions(t *testing.T, transportProtocols []stack.TransportProtocolFa if testing.Verbose() { wep = sniffer.New(ep) } - if err := s.CreateNIC(NICID, wep); err != nil { + if err := s.CreateNICWithOptions(NICID, wep, stack.NICOptions{Name: "nic1", EnableExperimentIPOption: options.EnableExperimentIPOption}); err != nil { t.Fatalf("CreateNIC(%d, _): %s", NICID, err) } diff --git a/pkg/tcpip/transport/udp/udp_test.go b/pkg/tcpip/transport/udp/udp_test.go index 79eac3a61a..448038d63b 100644 --- a/pkg/tcpip/transport/udp/udp_test.go +++ b/pkg/tcpip/transport/udp/udp_test.go @@ -2293,6 +2293,49 @@ func TestWritePayloadSizeTooBig(t *testing.T) { } } +func TestSetExperimentOption(t *testing.T) { + opts := context.Options{ + EnableExperimentIPOption: true, + MTU: context.DefaultMTU, + HandleLocal: true, + } + c := context.NewWithOptions(t, []stack.TransportProtocolFactory{udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, opts) + defer c.Cleanup() + + c.CreateEndpoint(ipv4.ProtocolNumber, udp.ProtocolNumber) + + if err := c.EP.Connect(tcpip.FullAddress{Addr: context.TestAddr, Port: context.TestPort}); err != nil { + c.T.Fatalf("Connect failed: %s", err) + } + + var expval uint16 = 99 + c.EP.SocketOptions().SetExperimentOptionValue(expval) + + var r bytes.Reader + r.Reset(make([]byte, 1)) + _, err := c.EP.Write(&r, tcpip.WriteOptions{}) + if err != nil { + t.Fatalf("Write failed: %s", err) + } + + want := header.IPv4Options{ + byte(header.IPv4OptionExperimentType), + byte(header.IPv4OptionExperimentLength), + 0, + byte(expval), + } + + pkt := c.LinkEP.Read() + if pkt == nil { + t.Fatal("Packet wasn't written out") + } + defer pkt.DecRef() + + v := stack.PayloadSince(pkt.LinkHeader()) + defer v.Release() + checker.IPv4(t, v, checker.IPv4Options(want)) +} + func TestMain(m *testing.M) { refs.SetLeakMode(refs.LeaksPanic) code := m.Run()