diff --git a/pkg/tcpip/checker/checker.go b/pkg/tcpip/checker/checker.go index 68b5a96ac..174b24d8a 100644 --- a/pkg/tcpip/checker/checker.go +++ b/pkg/tcpip/checker/checker.go @@ -1882,6 +1882,20 @@ func IPv6HopByHopExtensionHeader(checkers ...IPv6ExtHdrOptionChecker) IPv6ExtHdr } } +// IPv6ExperimentHeader checks the extension header is an Experiment extension +// header and validates the value. +func IPv6ExperimentHeader(want uint16) IPv6ExtHdrChecker { + return func(t *testing.T, payloadHeader header.IPv6PayloadHeader) { + h, ok := payloadHeader.(header.IPv6ExperimentExtHdr) + if !ok { + t.Errorf("got = %T, want = header.IPv6UnknownExtHdrOption", payloadHeader) + } + if h.Value != want { + t.Errorf("got = %d, want = %d", h.Value, want) + } + } +} + // IPv6RouterAlert validates that an extension header option is the RouterAlert // option and matches on its value. func IPv6RouterAlert(want header.IPv6RouterAlertValue) IPv6ExtHdrOptionChecker { diff --git a/pkg/tcpip/header/ipv6_extension_headers.go b/pkg/tcpip/header/ipv6_extension_headers.go index 7f75b82b6..110f5c63c 100644 --- a/pkg/tcpip/header/ipv6_extension_headers.go +++ b/pkg/tcpip/header/ipv6_extension_headers.go @@ -49,6 +49,10 @@ const ( // of an IPv6 payload, as per RFC 8200 section 4.7. IPv6NoNextHeaderIdentifier IPv6ExtensionHeaderIdentifier = 59 + // IPv6ExperimentExtHdrIdentifier is the header identifier of an Experiment + // extension header, as per RFC 4727 section 3.3. + IPv6ExperimentExtHdrIdentifier IPv6ExtensionHeaderIdentifier = 253 + // IPv6UnknownExtHdrIdentifier is reserved by IANA. // https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#extension-header // "254 Use for experimentation and testing [RFC3692][RFC4727]" @@ -411,6 +415,17 @@ type IPv6DestinationOptionsExtHdr struct { // isIPv6PayloadHeader implements IPv6PayloadHeader.isIPv6PayloadHeader. func (IPv6DestinationOptionsExtHdr) isIPv6PayloadHeader() {} +// IPv6ExperimentExtHdr is a buffer holding the Experiment extension header. +type IPv6ExperimentExtHdr struct { + Value uint16 +} + +// Release implements IPv6PayloadHeader.Release. +func (IPv6ExperimentExtHdr) Release() {} + +// isIPv6PayloadHeader implements IPv6PayloadHeader.isIPv6PayloadHeader. +func (IPv6ExperimentExtHdr) isIPv6PayloadHeader() {} + // IPv6RoutingExtHdr is a buffer holding the Routing extension header specific // data as outlined in RFC 8200 section 4.4. type IPv6RoutingExtHdr struct { @@ -625,6 +640,17 @@ func (i *IPv6PayloadIterator) Next() (IPv6PayloadHeader, bool, error) { i.nextHdrIdentifier = nextHdrIdentifier return IPv6DestinationOptionsExtHdr{ipv6OptionsExtHdr{view}}, false, nil + case IPv6ExperimentExtHdrIdentifier: + var data [IPv6ExperimentHdrLength - ipv6ExperimentHdrValueOffset]byte + nextHdrIdentifier, _, err := i.nextHeaderData(false /* fragmentHdr */, data[:]) + if err != nil { + return nil, true, err + } + i.nextHdrIdentifier = nextHdrIdentifier + hdr := IPv6ExperimentExtHdr{ + Value: binary.BigEndian.Uint16(data[:ipv6ExperimentHdrTagLength]), + } + return hdr, false, nil case IPv6NoNextHeaderIdentifier: // This indicates the end of the IPv6 payload. return nil, true, nil @@ -689,7 +715,7 @@ func (i *IPv6PayloadIterator) nextHeaderData(fragmentHdr bool, bytes []byte) (IP i.nextOffset += uint32((length + 1) * ipv6ExtHdrLenBytesPerUnit) bytesLen := int(length)*ipv6ExtHdrLenBytesPerUnit + ipv6ExtHdrLenBytesExcluded - if fragmentHdr { + if bytes != nil { if n := len(bytes); n < bytesLen { panic(fmt.Sprintf("bytes only has space for %d bytes but need space for %d bytes (length = %d) for extension header with id = %d", n, bytesLen, length, i.nextHdrIdentifier)) } @@ -735,6 +761,36 @@ type IPv6SerializableExtHdr interface { serializeInto(nextHeader uint8, b []byte) int } +// ipv6RouterAlertPayloadLength is the length of the Router Alert payload +// as defined in RFC 4727 section 3.3. +const ( + IPv6ExperimentHdrLength = 8 + ipv6ExperimentNextHeaderOffset = 0 + ipv6ExperimentLengthOffset = 1 + ipv6ExperimentHdrValueOffset = 2 + ipv6ExperimentHdrTagLength = 2 +) + +var _ IPv6SerializableExtHdr = (*IPv6ExperimentExtHdr)(nil) + +// identifier implements IPv6SerializableExtHdr. +func (h IPv6ExperimentExtHdr) identifier() IPv6ExtensionHeaderIdentifier { + return IPv6ExperimentExtHdrIdentifier +} + +// length implements IPv6SerializableExtHdr. +func (h IPv6ExperimentExtHdr) length() int { + return IPv6ExperimentHdrLength +} + +// serializeInto implements IPv6SerializableExtHdr. +func (h IPv6ExperimentExtHdr) serializeInto(nextHeader uint8, b []byte) int { + b[ipv6ExperimentNextHeaderOffset] = nextHeader + b[ipv6ExperimentLengthOffset] = (IPv6ExperimentHdrLength / ipv6ExtHdrLenBytesPerUnit) - 1 + binary.BigEndian.PutUint16(b[ipv6ExperimentHdrValueOffset:][:ipv6ExperimentHdrTagLength], uint16(h.Value)) + return IPv6ExperimentHdrLength +} + var _ IPv6SerializableExtHdr = (*IPv6SerializableHopByHopExtHdr)(nil) // IPv6SerializableHopByHopExtHdr implements serialization of the Hop by Hop diff --git a/pkg/tcpip/network/ipv6/ipv6.go b/pkg/tcpip/network/ipv6/ipv6.go index 969afcc86..aa1a2b3bf 100644 --- a/pkg/tcpip/network/ipv6/ipv6.go +++ b/pkg/tcpip/network/ipv6/ipv6.go @@ -738,6 +738,9 @@ func (e *endpoint) MaxHeaderLength() uint16 { } func addIPHeader(srcAddr, dstAddr tcpip.Address, pkt *stack.PacketBuffer, params stack.NetworkHeaderParams, extensionHeaders header.IPv6ExtHdrSerializer) tcpip.Error { + if params.ExperimentOptionValue != 0 { + extensionHeaders = append(extensionHeaders, &header.IPv6ExperimentExtHdr{Value: params.ExperimentOptionValue}) + } extHdrsLen := extensionHeaders.Length() length := pkt.Size() + extensionHeaders.Length() if length > math.MaxUint16 { @@ -1463,6 +1466,7 @@ func (e *endpoint) processExtensionHeader(it *header.IPv6PayloadIterator, pkt ** if err := e.processIPv6RawPayloadHeader(&extHdr, it, *pkt, *routerAlert, previousHeaderStart, *hasFragmentHeader); err != nil { return true, err } + case header.IPv6ExperimentExtHdr: default: // Since the iterator returns IPv6RawPayloadHeader for unknown Extension // Header IDs this should never happen unless we missed a supported type diff --git a/pkg/tcpip/stack/gro/gro.go b/pkg/tcpip/stack/gro/gro.go index 2276d2874..758e8241a 100644 --- a/pkg/tcpip/stack/gro/gro.go +++ b/pkg/tcpip/stack/gro/gro.go @@ -450,6 +450,7 @@ func (gd *GRO) dispatch6(pkt *stack.PacketBuffer) { case header.IPv6HopByHopOptionsExtHdr: case header.IPv6RoutingExtHdr: case header.IPv6DestinationOptionsExtHdr: + case header.IPv6ExperimentExtHdr: default: // This is either a TCP header or something we can't handle. ipHdrSize = int(it.HeaderOffset()) diff --git a/pkg/tcpip/transport/internal/network/endpoint.go b/pkg/tcpip/transport/internal/network/endpoint.go index 1de2d38c7..a72d57592 100644 --- a/pkg/tcpip/transport/internal/network/endpoint.go +++ b/pkg/tcpip/transport/internal/network/endpoint.go @@ -310,6 +310,13 @@ func (c *WriteContext) newPacketBufferLocked(reserveHdrBytes int, data buffer.Bu // This matches Linux behaviour: // https://github.com/torvalds/linux/blob/38d741cb70b/include/net/sock.h#L2519 // https://github.com/torvalds/linux/blob/38d741cb70b/net/core/sock.c#L2588 + var expOptVal uint16 + if nic, err := c.e.stack.GetNICByID(c.route.OutgoingNIC()); err == nil && nic.ExperimentIPOptionEnabled() { + expOptVal = c.e.ops.GetExperimentOptionValue() + } + if c.route.NetProto() == header.IPv6ProtocolNumber && expOptVal != 0 { + reserveHdrBytes += header.IPv6ExperimentHdrLength + } pktSize := int64(reserveHdrBytes) + int64(data.Size()) e.sendBufferSizeInUse += pktSize diff --git a/pkg/tcpip/transport/tcp/connect.go b/pkg/tcpip/transport/tcp/connect.go index 492afe24b..b7e2db465 100644 --- a/pkg/tcpip/transport/tcp/connect.go +++ b/pkg/tcpip/transport/tcp/connect.go @@ -820,7 +820,11 @@ type tcpFields struct { func (e *Endpoint) sendSynTCP(r *stack.Route, tf tcpFields, opts header.TCPSynOptions) tcpip.Error { tf.opts = makeSynOptions(opts) // We ignore SYN send errors and let the callers re-attempt send. - p := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: header.TCPMinimumSize + int(r.MaxHeaderLength()) + len(tf.opts)}) + hdrSize := header.TCPMinimumSize + int(r.MaxHeaderLength()) + len(tf.opts) + if r.NetProto() == header.IPv6ProtocolNumber && tf.expOptVal != 0 { + hdrSize += header.IPv6ExperimentHdrLength + } + p := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: hdrSize}) defer p.DecRef() if err := e.sendTCP(r, tf, p, stack.GSO{}); err != nil { e.stats.SendErrors.SynSendToNetworkFailed.Increment() @@ -892,6 +896,10 @@ func sendTCPBatch(r *stack.Route, tf tcpFields, pkt *stack.PacketBuffer, gso sta // packet already has the truncated data. shouldSplitPacket := i != n-1 if shouldSplitPacket { + if r.NetProto() == header.IPv6ProtocolNumber && tf.expOptVal != 0 { + // Reserve extra bytes for the experiment option. + hdrSize += header.IPv6ExperimentHdrLength + } splitPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: hdrSize}) splitPkt.Data().ReadFromPacketData(pkt.Data(), packetSize) pkt = splitPkt @@ -1018,7 +1026,11 @@ func (e *Endpoint) sendRaw(pkt *stack.PacketBuffer, flags header.TCPFlags, seq, } options := e.makeOptions(sackBlocks) defer putOptions(options) - pkt.ReserveHeaderBytes(header.TCPMinimumSize + int(e.route.MaxHeaderLength()) + len(options)) + hdrSize := header.TCPMinimumSize + int(e.route.MaxHeaderLength()) + len(options) + if e.route.NetProto() == header.IPv6ProtocolNumber && e.getExperimentOptionValue(e.route) != 0 { + hdrSize += header.IPv6ExperimentHdrLength + } + pkt.ReserveHeaderBytes(hdrSize) return e.sendTCP(e.route, tcpFields{ id: e.TransportEndpointInfo.ID, ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit), diff --git a/pkg/tcpip/transport/tcp/protocol.go b/pkg/tcpip/transport/tcp/protocol.go index 9e6872b0b..50e53baa4 100644 --- a/pkg/tcpip/transport/tcp/protocol.go +++ b/pkg/tcpip/transport/tcp/protocol.go @@ -233,12 +233,17 @@ func replyWithReset(st *stack.Stack, s *segment, tos, ipv4TTL uint8, ipv6HopLimi ack = s.sequenceNumber.Add(s.logicalLen()) } - p := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: header.TCPMinimumSize + int(route.MaxHeaderLength())}) - defer p.DecRef() var expOptVal uint16 if s.ep != nil { expOptVal = s.ep.getExperimentOptionValue(route) } + hdrSize := header.TCPMinimumSize + int(route.MaxHeaderLength()) + if route.NetProto() == header.IPv6ProtocolNumber && expOptVal != 0 { + hdrSize += header.IPv6ExperimentHdrLength + } + p := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: hdrSize}) + defer p.DecRef() + return sendTCP(route, tcpFields{ id: s.id, ttl: ttl, diff --git a/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go b/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go index 1dcb19a7c..37f6a9082 100644 --- a/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go +++ b/pkg/tcpip/transport/tcp/test/e2e/tcp_test.go @@ -9452,6 +9452,104 @@ func TestSetExperimentOption(t *testing.T) { checker.IPv4(t, v, checker.IPv4Options(want)) } +func TestSetExperimentOptionIPv6(t *testing.T) { + c := context.NewWithOpts(t, context.Options{ + EnableV4: false, + EnableV6: true, + MTU: e2e.DefaultMTU, + EnableExperimentIPOption: true, + }) + defer c.Cleanup() + + ep, err := c.Stack().NewEndpoint(tcp.ProtocolNumber, ipv6.ProtocolNumber, &c.WQ) + if err != nil { + t.Fatalf("NewEndpoint failed: %v", err) + } + c.EP = ep + + // Start connection attempt. + waitEntry, notifyCh := waiter.NewChannelEntry(waiter.WritableEvents) + c.WQ.EventRegister(&waitEntry) + defer c.WQ.EventUnregister(&waitEntry) + + err = c.EP.Connect(tcpip.FullAddress{Addr: context.TestV6Addr, Port: context.TestPort}) + if _, ok := err.(*tcpip.ErrConnectStarted); !ok { + t.Fatalf("Unexpected return value from Connect: %v", err) + } + + // Receive SYN packet. + b := c.GetV6Packet() + defer b.Release() + checker.IPv6(t, b, + checker.TCP( + checker.DstPort(context.TestPort), + checker.TCPFlags(header.TCPFlagSyn), + ), + ) + if got, want := tcp.EndpointState(c.EP.State()), tcp.StateSynSent; got != want { + t.Fatalf("Unexpected endpoint state: want %v, got %v", want, got) + } + + iss := seqnum.Value(context.TestInitialSequenceNumber) + rcvWnd := seqnum.Size(30000) + tcpHdr := header.TCP(header.IPv6(b.AsSlice()).Payload()) + synOpts := header.ParseSynOptions(tcpHdr.Options(), false /* isAck */) + c.IRS = seqnum.Value(tcpHdr.SequenceNumber()) + + c.SendV6Packet(nil, &context.Headers{ + SrcPort: tcpHdr.DestinationPort(), + DstPort: tcpHdr.SourcePort(), + Flags: header.TCPFlagSyn | header.TCPFlagAck, + SeqNum: iss, + AckNum: c.IRS.Add(1), + RcvWnd: rcvWnd, + TCPOpts: nil, + }) + + // Receive ACK packet. + b = c.GetV6Packet() + defer b.Release() + checker.IPv6(t, b, + checker.TCP( + checker.DstPort(context.TestPort), + checker.TCPFlags(header.TCPFlagAck), + checker.TCPSeqNum(uint32(c.IRS)+1), + checker.TCPAckNum(uint32(iss)+1), + ), + ) + + // Wait for connection to be established. + select { + case <-notifyCh: + if err := c.EP.LastError(); err != nil { + t.Fatalf("Unexpected error when connecting: %v", err) + } + case <-time.After(1 * time.Second): + t.Fatalf("Timed out waiting for connection") + } + if got, want := tcp.EndpointState(c.EP.State()), tcp.StateEstablished; got != want { + t.Fatalf("Unexpected endpoint state: want %v, got %v", want, got) + } + + c.RcvdWindowScale = uint8(synOpts.WS) + c.Port = tcpHdr.SourcePort() + + 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.GetV6Packet() + defer v.Release() + + checker.IPv6WithExtHdr(t, v, checker.IPv6ExtHdr(checker.IPv6ExperimentHeader(expval))) +} + func TestSetExperimentOptionWithOptionDisabled(t *testing.T) { c := context.NewWithOpts(t, context.Options{ EnableV4: true, diff --git a/pkg/tcpip/transport/udp/udp_test.go b/pkg/tcpip/transport/udp/udp_test.go index 448038d63..f32039502 100644 --- a/pkg/tcpip/transport/udp/udp_test.go +++ b/pkg/tcpip/transport/udp/udp_test.go @@ -2336,6 +2336,42 @@ func TestSetExperimentOption(t *testing.T) { checker.IPv4(t, v, checker.IPv4Options(want)) } +func TestSetExperimentOptionIPv6(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(ipv6.ProtocolNumber, udp.ProtocolNumber) + + if err := c.EP.Connect(tcpip.FullAddress{Addr: context.TestV6Addr, 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) + } + + 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.IPv6WithExtHdr(t, v, checker.IPv6ExtHdr(checker.IPv6ExperimentHeader(expval))) +} + func TestMain(m *testing.M) { refs.SetLeakMode(refs.LeaksPanic) code := m.Run()