From 006bbe78ca5680fafd68f0f21e91390e1244bed4 Mon Sep 17 00:00:00 2001 From: Nate Hurley Date: Wed, 25 May 2022 15:15:08 -0700 Subject: [PATCH] Implement IPv4 multicast forwarding. This change implements AddMulticastRoute and the requisite routing logic. Subsequent changes will still be needed to: 1. Emit events for missing route or unexpected input interface 2. Implement DelRoute 3. Implement GetRouteStats Updates #7338. PiperOrigin-RevId: 451026594 --- pkg/syserr/netstack.go | 60 +- pkg/tcpip/errors.go | 27 + pkg/tcpip/network/internal/ip/errors.go | 27 + pkg/tcpip/network/internal/ip/stats.go | 16 + pkg/tcpip/network/ipv4/BUILD | 2 + pkg/tcpip/network/ipv4/ipv4.go | 544 +++++++-- pkg/tcpip/network/ipv4/ipv4_test.go | 1076 ++++++++++++----- pkg/tcpip/stack/BUILD | 1 + pkg/tcpip/stack/registration.go | 12 + pkg/tcpip/stack/stack.go | 32 + pkg/tcpip/stack/stack_test.go | 78 ++ pkg/tcpip/tcpip.go | 15 +- pkg/tcpip/tests/integration/BUILD | 21 + pkg/tcpip/tests/integration/forward_test.go | 61 +- .../integration/multicast_forward_test.go | 585 +++++++++ 15 files changed, 2081 insertions(+), 476 deletions(-) create mode 100644 pkg/tcpip/tests/integration/multicast_forward_test.go diff --git a/pkg/syserr/netstack.go b/pkg/syserr/netstack.go index eb44f1254..ab5d9e4dd 100644 --- a/pkg/syserr/netstack.go +++ b/pkg/syserr/netstack.go @@ -25,33 +25,35 @@ import ( // Mapping for tcpip.Error types. var ( - ErrUnknownProtocol = New((&tcpip.ErrUnknownProtocol{}).String(), errno.EINVAL) - ErrUnknownNICID = New((&tcpip.ErrUnknownNICID{}).String(), errno.ENODEV) - ErrUnknownDevice = New((&tcpip.ErrUnknownDevice{}).String(), errno.ENODEV) - ErrUnknownProtocolOption = New((&tcpip.ErrUnknownProtocolOption{}).String(), errno.ENOPROTOOPT) - ErrDuplicateNICID = New((&tcpip.ErrDuplicateNICID{}).String(), errno.EEXIST) - ErrDuplicateAddress = New((&tcpip.ErrDuplicateAddress{}).String(), errno.EEXIST) - ErrAlreadyBound = New((&tcpip.ErrAlreadyBound{}).String(), errno.EINVAL) - ErrInvalidEndpointState = New((&tcpip.ErrInvalidEndpointState{}).String(), errno.EINVAL) - ErrAlreadyConnecting = New((&tcpip.ErrAlreadyConnecting{}).String(), errno.EALREADY) - ErrNoPortAvailable = New((&tcpip.ErrNoPortAvailable{}).String(), errno.EAGAIN) - ErrPortInUse = New((&tcpip.ErrPortInUse{}).String(), errno.EADDRINUSE) - ErrBadLocalAddress = New((&tcpip.ErrBadLocalAddress{}).String(), errno.EADDRNOTAVAIL) - ErrClosedForSend = New((&tcpip.ErrClosedForSend{}).String(), errno.EPIPE) - ErrClosedForReceive = New((&tcpip.ErrClosedForReceive{}).String(), errno.NOERRNO) - ErrTimeout = New((&tcpip.ErrTimeout{}).String(), errno.ETIMEDOUT) - ErrAborted = New((&tcpip.ErrAborted{}).String(), errno.EPIPE) - ErrConnectStarted = New((&tcpip.ErrConnectStarted{}).String(), errno.EINPROGRESS) - ErrDestinationRequired = New((&tcpip.ErrDestinationRequired{}).String(), errno.EDESTADDRREQ) - ErrNotSupported = New((&tcpip.ErrNotSupported{}).String(), errno.EOPNOTSUPP) - ErrQueueSizeNotSupported = New((&tcpip.ErrQueueSizeNotSupported{}).String(), errno.ENOTTY) - ErrNoSuchFile = New((&tcpip.ErrNoSuchFile{}).String(), errno.ENOENT) - ErrInvalidOptionValue = New((&tcpip.ErrInvalidOptionValue{}).String(), errno.EINVAL) - ErrBroadcastDisabled = New((&tcpip.ErrBroadcastDisabled{}).String(), errno.EACCES) - ErrNotPermittedNet = New((&tcpip.ErrNotPermitted{}).String(), errno.EPERM) - ErrBadBuffer = New((&tcpip.ErrBadBuffer{}).String(), errno.EFAULT) - ErrMalformedHeader = New((&tcpip.ErrMalformedHeader{}).String(), errno.EINVAL) - ErrInvalidPortRange = New((&tcpip.ErrInvalidPortRange{}).String(), errno.EINVAL) + ErrUnknownProtocol = New((&tcpip.ErrUnknownProtocol{}).String(), errno.EINVAL) + ErrUnknownNICID = New((&tcpip.ErrUnknownNICID{}).String(), errno.ENODEV) + ErrUnknownDevice = New((&tcpip.ErrUnknownDevice{}).String(), errno.ENODEV) + ErrUnknownProtocolOption = New((&tcpip.ErrUnknownProtocolOption{}).String(), errno.ENOPROTOOPT) + ErrDuplicateNICID = New((&tcpip.ErrDuplicateNICID{}).String(), errno.EEXIST) + ErrDuplicateAddress = New((&tcpip.ErrDuplicateAddress{}).String(), errno.EEXIST) + ErrAlreadyBound = New((&tcpip.ErrAlreadyBound{}).String(), errno.EINVAL) + ErrInvalidEndpointState = New((&tcpip.ErrInvalidEndpointState{}).String(), errno.EINVAL) + ErrAlreadyConnecting = New((&tcpip.ErrAlreadyConnecting{}).String(), errno.EALREADY) + ErrNoPortAvailable = New((&tcpip.ErrNoPortAvailable{}).String(), errno.EAGAIN) + ErrPortInUse = New((&tcpip.ErrPortInUse{}).String(), errno.EADDRINUSE) + ErrBadLocalAddress = New((&tcpip.ErrBadLocalAddress{}).String(), errno.EADDRNOTAVAIL) + ErrClosedForSend = New((&tcpip.ErrClosedForSend{}).String(), errno.EPIPE) + ErrClosedForReceive = New((&tcpip.ErrClosedForReceive{}).String(), errno.NOERRNO) + ErrTimeout = New((&tcpip.ErrTimeout{}).String(), errno.ETIMEDOUT) + ErrAborted = New((&tcpip.ErrAborted{}).String(), errno.EPIPE) + ErrConnectStarted = New((&tcpip.ErrConnectStarted{}).String(), errno.EINPROGRESS) + ErrDestinationRequired = New((&tcpip.ErrDestinationRequired{}).String(), errno.EDESTADDRREQ) + ErrNotSupported = New((&tcpip.ErrNotSupported{}).String(), errno.EOPNOTSUPP) + ErrQueueSizeNotSupported = New((&tcpip.ErrQueueSizeNotSupported{}).String(), errno.ENOTTY) + ErrNoSuchFile = New((&tcpip.ErrNoSuchFile{}).String(), errno.ENOENT) + ErrInvalidOptionValue = New((&tcpip.ErrInvalidOptionValue{}).String(), errno.EINVAL) + ErrBroadcastDisabled = New((&tcpip.ErrBroadcastDisabled{}).String(), errno.EACCES) + ErrNotPermittedNet = New((&tcpip.ErrNotPermitted{}).String(), errno.EPERM) + ErrBadBuffer = New((&tcpip.ErrBadBuffer{}).String(), errno.EFAULT) + ErrMalformedHeader = New((&tcpip.ErrMalformedHeader{}).String(), errno.EINVAL) + ErrInvalidPortRange = New((&tcpip.ErrInvalidPortRange{}).String(), errno.EINVAL) + ErrMulticastInputCannotBeOutput = New((&tcpip.ErrMulticastInputCannotBeOutput{}).String(), errno.EINVAL) + ErrMissingRequiredFields = New((&tcpip.ErrMissingRequiredFields{}).String(), errno.EINVAL) ) // TranslateNetstackError converts an error from the tcpip package to a sentry @@ -138,6 +140,10 @@ func TranslateNetstackError(err tcpip.Error) *Error { return ErrMalformedHeader case *tcpip.ErrInvalidPortRange: return ErrInvalidPortRange + case *tcpip.ErrMulticastInputCannotBeOutput: + return ErrMulticastInputCannotBeOutput + case *tcpip.ErrMissingRequiredFields: + return ErrMissingRequiredFields default: panic(fmt.Sprintf("unknown error %T", err)) } diff --git a/pkg/tcpip/errors.go b/pkg/tcpip/errors.go index 5d478ac32..2eb822822 100644 --- a/pkg/tcpip/errors.go +++ b/pkg/tcpip/errors.go @@ -552,4 +552,31 @@ func (*ErrWouldBlock) IgnoreStats() bool { } func (*ErrWouldBlock) String() string { return "operation would block" } +// ErrMissingRequiredFields indicates that a required field is missing. +// +// +stateify savable +type ErrMissingRequiredFields struct{} + +func (*ErrMissingRequiredFields) isError() {} + +// IgnoreStats implements Error. +func (*ErrMissingRequiredFields) IgnoreStats() bool { + return true +} +func (*ErrMissingRequiredFields) String() string { return "mising required fields" } + +// ErrMulticastInputCannotBeOutput indicates that an input interface matches an +// output interface in the same multicast route. +// +// +stateify savable +type ErrMulticastInputCannotBeOutput struct{} + +func (*ErrMulticastInputCannotBeOutput) isError() {} + +// IgnoreStats implements Error. +func (*ErrMulticastInputCannotBeOutput) IgnoreStats() bool { + return true +} +func (*ErrMulticastInputCannotBeOutput) String() string { return "output cannot contain input" } + // LINT.ThenChange(../syserr/netstack.go) diff --git a/pkg/tcpip/network/internal/ip/errors.go b/pkg/tcpip/network/internal/ip/errors.go index 94f1cd1cb..62f111750 100644 --- a/pkg/tcpip/network/internal/ip/errors.go +++ b/pkg/tcpip/network/internal/ip/errors.go @@ -74,6 +74,33 @@ func (*ErrMessageTooLong) isForwardingError() {} func (*ErrMessageTooLong) String() string { return "message too long" } +// ErrNoMulticastPendingQueueBufferSpace indicates that a multicast packet +// could not be added to the pending packet queue due to insufficient buffer +// space. +// +// +stateify savable +type ErrNoMulticastPendingQueueBufferSpace struct{} + +func (*ErrNoMulticastPendingQueueBufferSpace) isForwardingError() {} + +func (*ErrNoMulticastPendingQueueBufferSpace) String() string { return "no buffer space" } + +// ErrUnexpectedMulticastInputInterface indicates that the interface that the +// packet arrived on did not match the routes expected input interface. +type ErrUnexpectedMulticastInputInterface struct{} + +func (*ErrUnexpectedMulticastInputInterface) isForwardingError() {} + +func (*ErrUnexpectedMulticastInputInterface) String() string { return "unexpected input interface" } + +// ErrUnknownOutputEndpoint indicates that the output endpoint associated with +// a route could not be found. +type ErrUnknownOutputEndpoint struct{} + +func (*ErrUnknownOutputEndpoint) isForwardingError() {} + +func (*ErrUnknownOutputEndpoint) String() string { return "unknown endpoint" } + // ErrOther indicates the packet coould not be forwarded for a reason // captured by the contained error. type ErrOther struct { diff --git a/pkg/tcpip/network/internal/ip/stats.go b/pkg/tcpip/network/internal/ip/stats.go index 40ab21cb6..83b0b63fb 100644 --- a/pkg/tcpip/network/internal/ip/stats.go +++ b/pkg/tcpip/network/internal/ip/stats.go @@ -51,6 +51,19 @@ type MultiCounterIPForwardingStats struct { // header. ExtensionHeaderProblem tcpip.MultiCounterStat + // UnexpectedMulticastInputInterface is the number of multicast packets that + // were received on an interface that did not match the corresponding route's + // expected input interface. + UnexpectedMulticastInputInterface tcpip.MultiCounterStat + + // UnknownOutputEndpoint is the number of packets that could not be forwarded + // because the output endpoint could not be found. + UnknownOutputEndpoint tcpip.MultiCounterStat + + // NoMulticastPendingQueueBufferSpace is the number of multicast packets that + // were dropped due to insufficent buffer space in the pending packet queue. + NoMulticastPendingQueueBufferSpace tcpip.MultiCounterStat + // Errors is the number of IP packets received which could not be // successfully forwarded. Errors tcpip.MultiCounterStat @@ -66,6 +79,9 @@ func (m *MultiCounterIPForwardingStats) Init(a, b *tcpip.IPForwardingStats) { m.PacketTooBig.Init(a.PacketTooBig, b.PacketTooBig) m.ExhaustedTTL.Init(a.ExhaustedTTL, b.ExhaustedTTL) m.HostUnreachable.Init(a.HostUnreachable, b.HostUnreachable) + m.UnexpectedMulticastInputInterface.Init(a.UnexpectedMulticastInputInterface, b.UnexpectedMulticastInputInterface) + m.UnknownOutputEndpoint.Init(a.UnknownOutputEndpoint, b.UnknownOutputEndpoint) + m.NoMulticastPendingQueueBufferSpace.Init(a.NoMulticastPendingQueueBufferSpace, b.NoMulticastPendingQueueBufferSpace) } // LINT.ThenChange(:MultiCounterIPForwardingStats, ../../../tcpip.go:IPForwardingStats) diff --git a/pkg/tcpip/network/ipv4/BUILD b/pkg/tcpip/network/ipv4/BUILD index 53c24f4c8..97eeb782c 100644 --- a/pkg/tcpip/network/ipv4/BUILD +++ b/pkg/tcpip/network/ipv4/BUILD @@ -21,6 +21,7 @@ go_library( "//pkg/tcpip/network/hash", "//pkg/tcpip/network/internal/fragmentation", "//pkg/tcpip/network/internal/ip", + "//pkg/tcpip/network/internal/multicast", "//pkg/tcpip/stack", ], ) @@ -55,6 +56,7 @@ go_test( "//pkg/tcpip/transport/udp", "//pkg/waiter", "@com_github_google_go_cmp//cmp:go_default_library", + "@com_github_google_go_cmp//cmp/cmpopts:go_default_library", ], ) diff --git a/pkg/tcpip/network/ipv4/ipv4.go b/pkg/tcpip/network/ipv4/ipv4.go index e221557f2..eb7016415 100644 --- a/pkg/tcpip/network/ipv4/ipv4.go +++ b/pkg/tcpip/network/ipv4/ipv4.go @@ -30,6 +30,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/network/hash" "gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation" "gvisor.dev/gvisor/pkg/tcpip/network/internal/ip" + "gvisor.dev/gvisor/pkg/tcpip/network/internal/multicast" "gvisor.dev/gvisor/pkg/tcpip/stack" ) @@ -603,26 +604,88 @@ func (e *endpoint) WriteHeaderIncludedPacket(r *stack.Route, pkt *stack.PacketBu return e.writePacketPostRouting(r, pkt, true /* headerIncluded */) } -// forwardPacket attempts to forward a packet to its final destination. -func (e *endpoint) forwardPacket(pkt *stack.PacketBuffer) ip.ForwardingError { +// forwardPacketWithRoute emits the pkt using the provided route. +// +// If updateOptions is true, then the IP options will be updated in the copied +// pkt using the outgoing endpoint. Otherwise, the caller is responsible for +// updating the options. +// +// This method should be invoked by the endpoint that received the pkt. +func (e *endpoint) forwardPacketWithRoute(route *stack.Route, pkt *stack.PacketBuffer, updateOptions bool) ip.ForwardingError { + h := header.IPv4(pkt.NetworkHeader().View()) + stk := e.protocol.stack + + inNicName := stk.FindNICNameFromID(e.nic.ID()) + outNicName := stk.FindNICNameFromID(route.NICID()) + if ok := stk.IPTables().CheckForward(pkt, inNicName, outNicName); !ok { + // iptables is telling us to drop the packet. + e.stats.ip.IPTablesForwardDropped.Increment() + return nil + } + + // We need to do a deep copy of the IP packet because + // WriteHeaderIncludedPacket may modify the packet buffer, but we do + // not own it. + // + // TODO(https://gvisor.dev/issue/7473): For multicast, only create one deep + // copy and then clone. + newPkt := pkt.DeepCopyForForwarding(int(route.MaxHeaderLength())) + newHdr := header.IPv4(newPkt.NetworkHeader().View()) + defer newPkt.DecRef() + + forwardToEp, ok := e.protocol.getEndpointForNIC(route.NICID()) + if !ok { + return &ip.ErrUnknownOutputEndpoint{} + } + + if updateOptions { + if err := forwardToEp.updateOptionsForForwarding(newPkt); err != nil { + return err + } + } + + ttl := h.TTL() + // As per RFC 791 page 30, Time to Live, + // + // This field must be decreased at each point that the internet header + // is processed to reflect the time spent processing the datagram. + // Even if no local information is available on the time actually + // spent, the field must be decremented by 1. + newHdr.SetTTL(ttl - 1) + // We perform a full checksum as we may have updated options above. The IP + // header is relatively small so this is not expected to be an expensive + // operation. + newHdr.SetChecksum(0) + newHdr.SetChecksum(^newHdr.CalculateChecksum()) + + switch err := forwardToEp.writePacketPostRouting(route, newPkt, true /* headerIncluded */); err.(type) { + case nil: + return nil + case *tcpip.ErrMessageTooLong: + // As per RFC 792, page 4, Destination Unreachable: + // + // Another case is when a datagram must be fragmented to be forwarded by a + // gateway yet the Don't Fragment flag is on. In this case the gateway must + // discard the datagram and may return a destination unreachable message. + // + // WriteHeaderIncludedPacket checks for the presence of the Don't Fragment bit + // while sending the packet and returns this error iff fragmentation is + // necessary and the bit is also set. + _ = e.protocol.returnError(&icmpReasonFragmentationNeeded{}, pkt, false /* deliveredLocally */) + return &ip.ErrMessageTooLong{} + default: + return &ip.ErrOther{Err: err} + } +} + +// forwardUnicastPacket attempts to forward a packet to its final destination. +func (e *endpoint) forwardUnicastPacket(pkt *stack.PacketBuffer) ip.ForwardingError { h := header.IPv4(pkt.NetworkHeader().View()) dstAddr := h.DestinationAddress() - // As per RFC 3927 section 7, - // - // A router MUST NOT forward a packet with an IPv4 Link-Local source or - // destination address, irrespective of the router's default route - // configuration or routes obtained from dynamic routing protocols. - // - // A router which receives a packet with an IPv4 Link-Local source or - // destination address MUST NOT forward the packet. This prevents - // forwarding of packets back onto the network segment from which they - // originated, or to any other segment. - if header.IsV4LinkLocalUnicastAddress(h.SourceAddress()) { - return &ip.ErrLinkLocalSourceAddress{} - } - if header.IsV4LinkLocalUnicastAddress(dstAddr) || header.IsV4LinkLocalMulticastAddress(dstAddr) { - return &ip.ErrLinkLocalDestinationAddress{} + + if err := validateAddressesForForwarding(h); err != nil { + return err } ttl := h.TTL() @@ -640,29 +703,8 @@ func (e *endpoint) forwardPacket(pkt *stack.PacketBuffer) ip.ForwardingError { return &ip.ErrTTLExceeded{} } - if opts := h.Options(); len(opts) != 0 { - newOpts, _, optProblem := e.processIPOptions(pkt, opts, &optionUsageForward{}) - if optProblem != nil { - if optProblem.NeedICMP { - _ = e.protocol.returnError(&icmpReasonParamProblem{ - pointer: optProblem.Pointer, - }, pkt, false /* deliveredLocally */) - } - return &ip.ErrParameterProblem{} - } - copied := copy(opts, newOpts) - if copied != len(newOpts) { - panic(fmt.Sprintf("copied %d bytes of new options, expected %d bytes", copied, len(newOpts))) - } - // Since in forwarding we handle all options, including copying those we - // do not recognise, the options region should remain the same size which - // simplifies processing. As we MAY receive a packet with a lot of padded - // bytes after the "end of options list" byte, make sure we copy - // them as the legal padding value (0). - for i := copied; i < len(opts); i++ { - // Pad with 0 (EOL). RFC 791 page 23 says "The padding is zero". - opts[i] = byte(header.IPv4OptionListEndType) - } + if err := e.updateOptionsForForwarding(pkt); err != nil { + return err } stk := e.protocol.stack @@ -696,58 +738,17 @@ func (e *endpoint) forwardPacket(pkt *stack.PacketBuffer) ip.ForwardingError { } defer r.Release() - inNicName := stk.FindNICNameFromID(e.nic.ID()) - outNicName := stk.FindNICNameFromID(r.NICID()) - if ok := stk.IPTables().CheckForward(pkt, inNicName, outNicName); !ok { - // iptables is telling us to drop the packet. - e.stats.ip.IPTablesForwardDropped.Increment() - return nil - } - - // We need to do a deep copy of the IP packet because - // WriteHeaderIncludedPacket may modify the packet buffer, but we do - // not own it. - newPkt := pkt.DeepCopyForForwarding(int(r.MaxHeaderLength())) - newHdr := header.IPv4(newPkt.NetworkHeader().View()) - defer newPkt.DecRef() - - // As per RFC 791 page 30, Time to Live, + // TODO(https://gvisor.dev/issue/7472): Unicast IP options should be updated + // using the output endpoint (instead of the input endpoint). In particular, + // RFC 1812 section 5.2.1 states the following: // - // This field must be decreased at each point that the internet header - // is processed to reflect the time spent processing the datagram. - // Even if no local information is available on the time actually - // spent, the field must be decremented by 1. - newHdr.SetTTL(ttl - 1) - // We perform a full checksum as we may have updated options above. The IP - // header is relatively small so this is not expected to be an expensive - // operation. - newHdr.SetChecksum(0) - newHdr.SetChecksum(^newHdr.CalculateChecksum()) - - forwardToEp, ok := e.protocol.getEndpointForNIC(r.NICID()) - if !ok { - // The interface was removed after we obtained the route. - return &ip.ErrOther{Err: &tcpip.ErrUnknownDevice{}} - } - - switch err := forwardToEp.writePacketPostRouting(r, newPkt, true /* headerIncluded */); err.(type) { - case nil: - return nil - case *tcpip.ErrMessageTooLong: - // As per RFC 792, page 4, Destination Unreachable: - // - // Another case is when a datagram must be fragmented to be forwarded by a - // gateway yet the Don't Fragment flag is on. In this case the gateway must - // discard the datagram and may return a destination unreachable message. - // - // WriteHeaderIncludedPacket checks for the presence of the Don't Fragment bit - // while sending the packet and returns this error iff fragmentation is - // necessary and the bit is also set. - _ = e.protocol.returnError(&icmpReasonFragmentationNeeded{}, pkt, false /* deliveredLocally */) - return &ip.ErrMessageTooLong{} - default: - return &ip.ErrOther{Err: err} - } + // Processing of certain IP options requires that the router insert its IP + // address into the option. As noted in Section [5.2.4], the address + // inserted MUST be the address of the logical interface on which the + // packet is sent or the router's router-id if the packet is sent over an + // unnumbered interface. Thus, processing of these options cannot be + // completed until after the output interface is chosen. + return e.forwardPacketWithRoute(r, pkt, false /* updateOptions */) } // HandlePacket is called by the link layer when new ipv4 packets arrive for @@ -826,6 +827,163 @@ func (e *endpoint) handleLocalPacket(pkt *stack.PacketBuffer, canSkipRXChecksum e.handleValidatedPacket(h, pkt, e.nic.Name() /* inNICName */) } +func validateAddressesForForwarding(h header.IPv4) ip.ForwardingError { + // As per RFC 3927 section 7, + // + // A router MUST NOT forward a packet with an IPv4 Link-Local source or + // destination address, irrespective of the router's default route + // configuration or routes obtained from dynamic routing protocols. + // + // A router which receives a packet with an IPv4 Link-Local source or + // destination address MUST NOT forward the packet. This prevents + // forwarding of packets back onto the network segment from which they + // originated, or to any other segment. + if header.IsV4LinkLocalUnicastAddress(h.SourceAddress()) { + return &ip.ErrLinkLocalSourceAddress{} + } + if header.IsV4LinkLocalUnicastAddress(h.DestinationAddress()) || header.IsV4LinkLocalMulticastAddress(h.DestinationAddress()) { + return &ip.ErrLinkLocalDestinationAddress{} + } + return nil +} + +// forwardMulticastPacket validates a multicast pkt and attempts to forward it. +// +// This method should be invoked for incoming multicast packets using the +// endpoint that received the packet. +func (e *endpoint) forwardMulticastPacket(h header.IPv4, pkt *stack.PacketBuffer) ip.ForwardingError { + if err := validateAddressesForForwarding(h); err != nil { + return err + } + + if opts := h.Options(); len(opts) != 0 { + // Check if the options are valid, but don't mutate them. This corresponds + // to step 3 of RFC 1812 section 5.2.1.1. + if _, _, optProblem := e.processIPOptions(pkt, opts, &optionUsageVerify{}); optProblem != nil { + // Per RFC 1812 section 4.3.2.7, an ICMP error message should not be + // sent for: + // + // A packet destined to an IP broadcast or IP multicast address. + // + // Note that protocol.returnError also enforces this requirement. + // However, we intentionally omit it here since this path is multicast + // only. + return &ip.ErrParameterProblem{} + } + } + + routeKey := stack.UnicastSourceAndMulticastDestination{ + Source: h.SourceAddress(), + Destination: h.DestinationAddress(), + } + + // The pkt has been validated. Consequently, if a route is not found, then + // the pkt can safely be queued. + result, hasBufferSpace := e.protocol.multicastRouteTable.GetRouteOrInsertPending(routeKey, pkt) + + if !hasBufferSpace { + // Unable to queue the pkt. Silently drop it. + return &ip.ErrNoMulticastPendingQueueBufferSpace{} + } + + // TODO(https://gvisor.dev/issue/7338): Emit an event for a missing route. + if result.GetRouteResultState == multicast.InstalledRouteFound { + // Attempt to forward the pkt using an existing route. + return e.forwardValidatedMulticastPacket(pkt, result.InstalledRoute) + } + return &ip.ErrNoRoute{} +} + +func (e *endpoint) updateOptionsForForwarding(pkt *stack.PacketBuffer) ip.ForwardingError { + h := header.IPv4(pkt.NetworkHeader().View()) + if opts := h.Options(); len(opts) != 0 { + newOpts, _, optProblem := e.processIPOptions(pkt, opts, &optionUsageForward{}) + if optProblem != nil { + if optProblem.NeedICMP { + // Note that this will not emit an ICMP error if the destination is + // multicast. + _ = e.protocol.returnError(&icmpReasonParamProblem{ + pointer: optProblem.Pointer, + }, pkt, false /* deliveredLocally */) + } + return &ip.ErrParameterProblem{} + } + copied := copy(opts, newOpts) + if copied != len(newOpts) { + panic(fmt.Sprintf("copied %d bytes of new options, expected %d bytes", copied, len(newOpts))) + } + // Since in forwarding we handle all options, including copying those we + // do not recognise, the options region should remain the same size which + // simplifies processing. As we MAY receive a packet with a lot of padded + // bytes after the "end of options list" byte, make sure we copy + // them as the legal padding value (0). + for i := copied; i < len(opts); i++ { + // Pad with 0 (EOL). RFC 791 page 23 says "The padding is zero". + opts[i] = byte(header.IPv4OptionListEndType) + } + } + return nil +} + +// forwardValidatedMulticastPacket attempts to forward the pkt using the +// provided installedRoute. +// +// This method should be invoked by the endpoint that received the pkt. +func (e *endpoint) forwardValidatedMulticastPacket(pkt *stack.PacketBuffer, installedRoute *multicast.InstalledRoute) ip.ForwardingError { + // Per RFC 1812 section 5.2.1.3, + // + // Based on the IP source and destination addresses found in the datagram + // header, the router determines whether the datagram has been received + // on the proper interface for forwarding. If not, the datagram is + // dropped silently. + if e.nic.ID() != installedRoute.ExpectedInputInterface { + // TODO(https://gvisor.dev/issue/7338): Emit an event for an unexpected + // input interface. + return &ip.ErrUnexpectedMulticastInputInterface{} + } + + for _, outgoingInterface := range installedRoute.OutgoingInterfaces { + if err := e.forwardMulticastPacketForOutgoingInterface(pkt, outgoingInterface); err != nil { + e.handleForwardingError(err) + continue + } + // The pkt was successfully forwarded. Mark the route as used. + installedRoute.SetLastUsedTimestamp(e.protocol.stack.Clock().NowMonotonic()) + } + return nil +} + +// forwardMulticastPacketForOutgoingInterface attempts to forward the pkt out +// of the provided outgoingInterface. +// +// This method should be invoked by the endpoint that received the pkt. +func (e *endpoint) forwardMulticastPacketForOutgoingInterface(pkt *stack.PacketBuffer, outgoingInterface stack.MulticastRouteOutgoingInterface) ip.ForwardingError { + h := header.IPv4(pkt.NetworkHeader().View()) + + // Per RFC 1812 section 5.2.1.3, + // + // A copy of the multicast datagram is forwarded out each outgoing + // interface whose minimum TTL value is less than or equal to the TTL + // value in the datagram header. + // + // Copying of the packet is deferred to forwardPacketWithRoute since unicast + // and multicast both require a copy. + if outgoingInterface.MinTTL > h.TTL() { + return &ip.ErrTTLExceeded{} + } + + route := e.protocol.stack.NewRouteForMulticast(outgoingInterface.ID, h.DestinationAddress(), e.NetworkProtocolNumber()) + + if route == nil { + // Failed to convert to a stack.Route. This likely means that the outgoing + // endpoint no longer exists. + return &ip.ErrNoRoute{} + } + defer route.Release() + + return e.forwardPacketWithRoute(route, pkt, true /* updateOptions */) +} + func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) { pkt.NICID = e.nic.ID() @@ -860,40 +1018,81 @@ func (e *endpoint) handleValidatedPacket(h header.IPv4, pkt *stack.PacketBuffer, } } - // Before we do any processing, note if the packet was received as some - // sort of broadcast. The destination address should be an address we own - // or a group we joined. + if header.IsV4MulticastAddress(dstAddr) { + // Handle all packets destined to a multicast address separately. Unlike + // unicast, these packets can be both delivered locally and forwarded. See + // RFC 1812 section 5.2.3 for details regarding the forwarding/local + // delivery decision. + + multicastForwarding := e.MulticastForwarding() + + if multicastForwarding { + e.handleForwardingError(e.forwardMulticastPacket(h, pkt)) + } + + if e.IsInGroup(dstAddr) { + e.deliverPacketLocally(h, pkt, inNICName) + return + } + + if !multicastForwarding { + // Only consider the destination address invalid if we didn't attempt to + // forward the pkt and it was not delivered locally. + stats.ip.InvalidDestinationAddressesReceived.Increment() + } + return + } + + // Before we do any processing, check if the packet was received as some + // sort of broadcast. + // + // If the packet is destined for this device, then it should be delivered + // locally. Otherwise, if forwarding is enabled, it should be forwarded. if addressEndpoint := e.AcquireAssignedAddress(dstAddr, e.nic.Promiscuous(), stack.CanBePrimaryEndpoint); addressEndpoint != nil { subnet := addressEndpoint.AddressWithPrefix().Subnet() addressEndpoint.DecRef() pkt.NetworkPacketInfo.LocalAddressBroadcast = subnet.IsBroadcast(dstAddr) || dstAddr == header.IPv4Broadcast - } else if !e.IsInGroup(dstAddr) { - if !e.Forwarding() { - stats.ip.InvalidDestinationAddressesReceived.Increment() - return - } - switch err := e.forwardPacket(pkt); err.(type) { - case nil: - return - case *ip.ErrLinkLocalSourceAddress: - stats.ip.Forwarding.LinkLocalSource.Increment() - case *ip.ErrLinkLocalDestinationAddress: - stats.ip.Forwarding.LinkLocalDestination.Increment() - case *ip.ErrTTLExceeded: - stats.ip.Forwarding.ExhaustedTTL.Increment() - case *ip.ErrNoRoute: - stats.ip.Forwarding.Unrouteable.Increment() - case *ip.ErrParameterProblem: - stats.ip.MalformedPacketsReceived.Increment() - case *ip.ErrMessageTooLong: - stats.ip.Forwarding.PacketTooBig.Increment() - default: - panic(fmt.Sprintf("unexpected error %s while trying to forward packet: %#v", err, pkt)) - } - stats.ip.Forwarding.Errors.Increment() - return + e.deliverPacketLocally(h, pkt, inNICName) + } else if e.Forwarding() { + e.handleForwardingError(e.forwardUnicastPacket(pkt)) + } else { + stats.ip.InvalidDestinationAddressesReceived.Increment() } +} +// handleForwardingError processes the provided err and increments any relevant +// counters. +func (e *endpoint) handleForwardingError(err ip.ForwardingError) { + stats := e.stats.ip + switch err.(type) { + case nil: + return + case *ip.ErrLinkLocalSourceAddress: + stats.Forwarding.LinkLocalSource.Increment() + case *ip.ErrLinkLocalDestinationAddress: + stats.Forwarding.LinkLocalDestination.Increment() + case *ip.ErrTTLExceeded: + stats.Forwarding.ExhaustedTTL.Increment() + case *ip.ErrNoRoute: + stats.Forwarding.Unrouteable.Increment() + case *ip.ErrParameterProblem: + stats.MalformedPacketsReceived.Increment() + case *ip.ErrMessageTooLong: + stats.Forwarding.PacketTooBig.Increment() + case *ip.ErrNoMulticastPendingQueueBufferSpace: + stats.Forwarding.NoMulticastPendingQueueBufferSpace.Increment() + case *ip.ErrUnexpectedMulticastInputInterface: + stats.Forwarding.UnexpectedMulticastInputInterface.Increment() + case *ip.ErrUnknownOutputEndpoint: + stats.Forwarding.UnknownOutputEndpoint.Increment() + default: + panic(fmt.Sprintf("unrecognized forwarding error: %s", err)) + } + stats.Forwarding.Errors.Increment() +} + +func (e *endpoint) deliverPacketLocally(h header.IPv4, pkt *stack.PacketBuffer, inNICName string) { + stats := e.stats // iptables filtering. All packets that reach here are intended for // this machine and will not be forwarded. if ok := e.protocol.stack.IPTables().CheckInput(pkt, inNICName); !ok { @@ -1180,6 +1379,7 @@ func (e *endpoint) Stats() stack.NetworkEndpointStats { } var _ stack.NetworkProtocol = (*protocol)(nil) +var _ stack.MulticastForwardingNetworkProtocol = (*protocol)(nil) var _ stack.RejectIPv4WithHandler = (*protocol)(nil) var _ fragmentation.TimeoutHandler = (*protocol)(nil) @@ -1208,6 +1408,8 @@ type protocol struct { fragmentation *fragmentation.Fragmentation options Options + + multicastRouteTable multicast.RouteTable } // Number returns the ipv4 protocol number. @@ -1261,11 +1463,112 @@ func (p *protocol) DefaultTTL() uint8 { // Close implements stack.TransportProtocol. func (p *protocol) Close() { p.fragmentation.Release() + p.multicastRouteTable.Close() } // Wait implements stack.TransportProtocol. func (*protocol) Wait() {} +func (p *protocol) validateUnicastSourceAndMulticastDestination(addresses stack.UnicastSourceAndMulticastDestination) tcpip.Error { + if !p.isUnicastAddress(addresses.Source) || header.IsV4LinkLocalUnicastAddress(addresses.Source) { + return &tcpip.ErrBadAddress{} + } + + if !header.IsV4MulticastAddress(addresses.Destination) || header.IsV4LinkLocalMulticastAddress(addresses.Destination) { + return &tcpip.ErrBadAddress{} + } + + return nil +} + +func (p *protocol) newInstalledRoute(route stack.MulticastRoute) (*multicast.InstalledRoute, tcpip.Error) { + if len(route.OutgoingInterfaces) == 0 { + return nil, &tcpip.ErrMissingRequiredFields{} + } + + if !p.stack.HasNIC(route.ExpectedInputInterface) { + return nil, &tcpip.ErrUnknownNICID{} + } + + for _, outgoingInterface := range route.OutgoingInterfaces { + if route.ExpectedInputInterface == outgoingInterface.ID { + return nil, &tcpip.ErrMulticastInputCannotBeOutput{} + } + + if !p.stack.HasNIC(outgoingInterface.ID) { + return nil, &tcpip.ErrUnknownNICID{} + } + } + return p.multicastRouteTable.NewInstalledRoute(route), nil +} + +// AddMulticastRoute implements stack.MulticastForwardingNetworkProtocol. +func (p *protocol) AddMulticastRoute(addresses stack.UnicastSourceAndMulticastDestination, route stack.MulticastRoute) tcpip.Error { + if err := p.validateUnicastSourceAndMulticastDestination(addresses); err != nil { + return err + } + + installedRoute, err := p.newInstalledRoute(route) + if err != nil { + return err + } + + pendingPackets := p.multicastRouteTable.AddInstalledRoute(addresses, installedRoute) + + for _, pkt := range pendingPackets { + p.forwardPendingMulticastPacket(pkt, installedRoute) + } + return nil +} + +func (p *protocol) forwardPendingMulticastPacket(pkt *stack.PacketBuffer, installedRoute *multicast.InstalledRoute) { + defer pkt.DecRef() + + // Attempt to forward the packet using the endpoint that it originally + // arrived on. This ensures that the packet is only forwarded if it + // matches the route's expected input interface (see 5a of RFC 1812 section + // 5.2.1.3). + ep, ok := p.getEndpointForNIC(pkt.NICID) + + if !ok { + // The endpoint that the packet arrived on no longer exists. Silently + // drop the pkt. + return + } + ep.handleForwardingError(ep.forwardValidatedMulticastPacket(pkt, installedRoute)) +} + +func (p *protocol) isUnicastAddress(addr tcpip.Address) bool { + if len(addr) != header.IPv4AddressSize { + return false + } + + if addr == header.IPv4Any || addr == header.IPv4Broadcast { + return false + } + + if p.isSubnetLocalBroadcastAddress(addr) { + return false + } + return !header.IsV4MulticastAddress(addr) +} + +func (p *protocol) isSubnetLocalBroadcastAddress(addr tcpip.Address) bool { + p.mu.RLock() + defer p.mu.RUnlock() + + for _, e := range p.eps { + if addressEndpoint := e.AcquireAssignedAddress(addr, false /* createTemp */, stack.NeverPrimaryEndpoint); addressEndpoint != nil { + subnet := addressEndpoint.Subnet() + addressEndpoint.DecRef() + if subnet.IsBroadcast(addr) { + return true + } + } + } + return false +} + // parseAndValidate parses the packet (including its transport layer header) and // returns the parsed IP header. // @@ -1445,6 +1748,9 @@ func NewProtocolWithOptions(opts Options) stack.NetworkProtocolFactory { header.ICMPv4TimeExceeded: {}, header.ICMPv4ParamProblem: {}, } + if err := p.multicastRouteTable.Init(multicast.DefaultConfig(s.Clock())); err != nil { + panic(fmt.Sprintf("p.multicastRouteTable.Init(_): %s", err)) + } return p } } diff --git a/pkg/tcpip/network/ipv4/ipv4_test.go b/pkg/tcpip/network/ipv4/ipv4_test.go index 55aa1a9e2..282802fb0 100644 --- a/pkg/tcpip/network/ipv4/ipv4_test.go +++ b/pkg/tcpip/network/ipv4/ipv4_test.go @@ -25,6 +25,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "gvisor.dev/gvisor/pkg/refsvfs2" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/tcpip" @@ -138,114 +139,277 @@ func TestExcludeBroadcast(t *testing.T) { }) } -func TestForwarding(t *testing.T) { +const ( + incomingNICID = 1 + outgoingNICID = 2 +) + +var ( + incomingIPv4Addr = tcpip.AddressWithPrefix{ + Address: testutil.MustParse4("10.0.0.1"), + PrefixLen: 8, + } + outgoingIPv4Addr = tcpip.AddressWithPrefix{ + Address: testutil.MustParse4("11.0.0.1"), + PrefixLen: 8, + } + defaultEndpointConfigs = map[tcpip.NICID]tcpip.AddressWithPrefix{ + incomingNICID: incomingIPv4Addr, + outgoingNICID: outgoingIPv4Addr, + } + multicastIPv4Addr = testutil.MustParse4("225.0.0.0") + remoteIPv4Addr1 = testutil.MustParse4("10.0.0.2") + remoteIPv4Addr2 = testutil.MustParse4("11.0.0.2") +) + +func TestAddMulticastRouteIPv4Errors(t *testing.T) { + incomingEpSubnet := incomingIPv4Addr.Subnet() + wantErr := &tcpip.ErrBadAddress{} + + tests := []struct { + name string + srcAddr tcpip.Address + }{ + { + name: "subnet-local broadcast source", + srcAddr: incomingEpSubnet.Broadcast().To4(), + }, + { + name: "broadcast source", + srcAddr: header.IPv4Broadcast, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := newTestContext() + defer ctx.cleanup() + s := ctx.s + + for nicID, addr := range defaultEndpointConfigs { + ep := channel.New(1, ipv4.MaxTotalSize, "") + defer ep.Close() + + if err := s.CreateNIC(nicID, ep); err != nil { + t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err) + } + addr := tcpip.ProtocolAddress{ + Protocol: header.IPv4ProtocolNumber, + AddressWithPrefix: addr, + } + if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err) + } + } + + outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{{ID: outgoingNICID, MinTTL: 1}} + + addresses := stack.UnicastSourceAndMulticastDestination{ + Source: test.srcAddr, + Destination: multicastIPv4Addr, + } + + route := stack.MulticastRoute{ + ExpectedInputInterface: outgoingNICID, + OutgoingInterfaces: outgoingInterfaces, + } + + err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route) + + if !cmp.Equal(err, wantErr, cmpopts.EquateErrors()) { + t.Errorf("got s.AddMulticastRoute(%d, %#v, %#v) = %s, want %s", ipv4.ProtocolNumber, addresses, route, err, wantErr) + } + }) + } +} + +type icmpError struct { + icmpType header.ICMPv4Type + icmpCode header.ICMPv4Code +} + +type packetOptions struct { + ipFlags uint8 + payloadLength int + options header.IPv4Options +} + +func newICMPEchoPacket(t *testing.T, srcAddr, dstAddr tcpip.Address, ttl uint8, options packetOptions) (*stack.PacketBuffer, []byte) { const ( - incomingNICID = 1 - outgoingNICID = 2 - randomSequence = 123 - randomIdent = 42 - randomTimeOffset = 0x10203040 + arbitraryICMPHeaderSequence = 123 + randomIdent = 42 ) - incomingIPv4Addr := tcpip.AddressWithPrefix{ - Address: tcpip.Address(net.ParseIP("10.0.0.1").To4()), - PrefixLen: 8, + t.Helper() + ipHeaderLength := header.IPv4MinimumSize + len(options.options) + if ipHeaderLength > header.IPv4MaximumHeaderSize { + t.Fatalf("ipHeaderLength = %d, want <= %d ", ipHeaderLength, header.IPv4MaximumHeaderSize) } - outgoingIPv4Addr := tcpip.AddressWithPrefix{ - Address: tcpip.Address(net.ParseIP("11.0.0.1").To4()), - PrefixLen: 8, + totalLength := ipHeaderLength + header.ICMPv4MinimumSize + options.payloadLength + hdr := buffer.NewPrependable(totalLength) + hdr.Prepend(options.payloadLength) + icmpH := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize)) + icmpH.SetIdent(randomIdent) + icmpH.SetSequence(arbitraryICMPHeaderSequence) + icmpH.SetType(header.ICMPv4Echo) + icmpH.SetCode(header.ICMPv4UnusedCode) + icmpH.SetChecksum(0) + icmpH.SetChecksum(^header.Checksum(icmpH, 0)) + ip := header.IPv4(hdr.Prepend(ipHeaderLength)) + ip.Encode(&header.IPv4Fields{ + TotalLength: uint16(totalLength), + Protocol: uint8(header.ICMPv4ProtocolNumber), + TTL: ttl, + SrcAddr: srcAddr, + DstAddr: dstAddr, + Flags: options.ipFlags, + }) + if len(options.options) != 0 { + ip.SetHeaderLength(uint8(ipHeaderLength)) + // Copy options manually. We do not use Encode for options so we can + // verify malformed options with handcrafted payloads. + if want, got := copy(ip.Options(), options.options), len(options.options); want != got { + t.Fatalf("got copy(ip.Options(), test.options) = %d, want = %d", got, want) + } } - outgoingLinkAddr := tcpip.LinkAddress("\x02\x03\x03\x04\x05\x06") - remoteIPv4Addr1 := testutil.MustParse4("10.0.0.2") - remoteIPv4Addr2 := testutil.MustParse4("11.0.0.2") + ip.SetChecksum(0) + ip.SetChecksum(^ip.CalculateChecksum()) + + expectedICMPPayloadLength := func() int { + maxICMPPacketLength := header.IPv4MinimumProcessableDatagramSize + maxICMPPayloadLength := maxICMPPacketLength - header.ICMPv4MinimumSize - header.IPv4MinimumSize + if len(hdr.View()) > maxICMPPayloadLength { + return maxICMPPayloadLength + } + return len(hdr.View()) + } + + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Data: hdr.View().ToVectorisedView(), + }) + pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + + return pkt, hdr.View()[:expectedICMPPayloadLength()] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func checkFragements(t *testing.T, ep *channel.Endpoint, expectedFragments []fragmentInfo, requestPkt *stack.PacketBuffer) { + t.Helper() + var fragmentedPackets []*stack.PacketBuffer + for i := 0; i < len(expectedFragments); i++ { + reply := ep.Read() + if reply == nil { + t.Fatal("Expected ICMP Echo fragment through outgoing NIC") + } + fragmentedPackets = append(fragmentedPackets, reply) + } + + // The forwarded packet's TTL will have been decremented. + ipHeader := header.IPv4(requestPkt.NetworkHeader().View()) + ipHeader.SetTTL(ipHeader.TTL() - 1) + + // Forwarded packets have available header bytes equalling the sum of the + // maximum IP header size and the maximum size allocated for link layer + // headers. In this case, no size is allocated for link layer headers. + expectedAvailableHeaderBytes := header.IPv4MaximumHeaderSize + if err := compareFragments(fragmentedPackets, requestPkt, defaultMTU, expectedFragments, header.ICMPv4ProtocolNumber, true /* withIPHeader */, expectedAvailableHeaderBytes); err != nil { + t.Error(err) + } + for _, pkt := range fragmentedPackets { + pkt.DecRef() + } +} + +func TestForwarding(t *testing.T) { + const randomTimeOffset = 0x10203040 + unreachableIPv4Addr := testutil.MustParse4("12.0.0.2") - multicastIPv4Addr := testutil.MustParse4("225.0.0.0") linkLocalIPv4Addr := testutil.MustParse4("169.254.0.0") tests := []struct { - name string - TTL uint8 - sourceAddr tcpip.Address - destAddr tcpip.Address - expectErrorICMP bool - ipFlags uint8 - mtu uint32 - payloadLength int - options header.IPv4Options - forwardedOptions header.IPv4Options - icmpType header.ICMPv4Type - icmpCode header.ICMPv4Code - expectPacketUnrouteableError bool - expectLinkLocalSourceError bool - expectLinkLocalDestError bool - expectPacketForwarded bool - expectedFragmentsForwarded []fragmentInfo + name string + TTL uint8 + srcAddr tcpip.Address + dstAddr tcpip.Address + options header.IPv4Options + forwardedOptions header.IPv4Options + icmpError *icmpError + expectedPacketUnrouteableErrors uint64 + expectedLinkLocalSourceErrors uint64 + expectedLinkLocalDestErrors uint64 + expectedMalformedPacketErrors uint64 + expectedExhaustedTTLErrors uint64 + expectPacketForwarded bool }{ { - name: "TTL of zero", - TTL: 0, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, - expectErrorICMP: true, - icmpType: header.ICMPv4TimeExceeded, - icmpCode: header.ICMPv4TTLExceeded, - mtu: ipv4.MaxTotalSize, + name: "TTL of zero", + TTL: 0, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, + icmpError: &icmpError{ + icmpType: header.ICMPv4TimeExceeded, + icmpCode: header.ICMPv4TTLExceeded, + }, + expectedExhaustedTTLErrors: 1, + expectPacketForwarded: false, }, { name: "TTL of one", TTL: 1, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, expectPacketForwarded: true, - mtu: ipv4.MaxTotalSize, }, { name: "TTL of two", TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, expectPacketForwarded: true, - mtu: ipv4.MaxTotalSize, }, { name: "Max TTL", TTL: math.MaxUint8, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, expectPacketForwarded: true, - mtu: ipv4.MaxTotalSize, }, { name: "four EOL options", TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, - expectPacketForwarded: true, - mtu: ipv4.MaxTotalSize, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, options: header.IPv4Options{0, 0, 0, 0}, forwardedOptions: header.IPv4Options{0, 0, 0, 0}, + expectPacketForwarded: true, }, { - name: "TS type 1 full", - TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, - mtu: ipv4.MaxTotalSize, + name: "TS type 1 full", + TTL: 2, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, options: header.IPv4Options{ 68, 12, 13, 0xF1, 192, 168, 1, 12, 1, 2, 3, 4, }, - expectErrorICMP: true, - icmpType: header.ICMPv4ParamProblem, - icmpCode: header.ICMPv4UnusedCode, + icmpError: &icmpError{ + icmpType: header.ICMPv4ParamProblem, + icmpCode: header.ICMPv4UnusedCode, + }, + expectedMalformedPacketErrors: 1, }, { - name: "TS type 0", - TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, - mtu: ipv4.MaxTotalSize, + name: "TS type 0", + TTL: 2, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, options: header.IPv4Options{ 68, 24, 21, 0x00, 1, 2, 3, 4, @@ -265,11 +429,10 @@ func TestForwarding(t *testing.T) { expectPacketForwarded: true, }, { - name: "end of options list", - TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, - mtu: ipv4.MaxTotalSize, + name: "end of options list", + TTL: 2, + srcAddr: remoteIPv4Addr1, + dstAddr: remoteIPv4Addr2, options: header.IPv4Options{ 68, 12, 13, 0x11, 192, 168, 1, 12, @@ -288,80 +451,32 @@ func TestForwarding(t *testing.T) { expectPacketForwarded: true, }, { - name: "Network unreachable", - TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: unreachableIPv4Addr, - expectErrorICMP: true, - mtu: ipv4.MaxTotalSize, - icmpType: header.ICMPv4DstUnreachable, - icmpCode: header.ICMPv4NetUnreachable, - expectPacketUnrouteableError: true, - }, - { - name: "Multicast destination", - TTL: 2, - destAddr: multicastIPv4Addr, - expectPacketUnrouteableError: true, - }, - { - name: "Link local destination", - TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: linkLocalIPv4Addr, - expectLinkLocalDestError: true, - }, - { - name: "Link local source", - TTL: 2, - sourceAddr: linkLocalIPv4Addr, - destAddr: remoteIPv4Addr2, - expectLinkLocalSourceError: true, - }, - { - name: "Fragmentation needed and DF set", - TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, - ipFlags: header.IPv4FlagDontFragment, - // We've picked this MTU because it is: - // - // 1) Greater than the minimum MTU that IPv4 hosts are required to process - // (576 bytes). As per RFC 1812, Section 4.3.2.3: - // - // The ICMP datagram SHOULD contain as much of the original datagram as - // possible without the length of the ICMP datagram exceeding 576 bytes. - // - // Therefore, setting an MTU greater than 576 bytes ensures that we can fit a - // complete ICMP packet on the incoming endpoint (and make assertions about - // it). - // - // 2) Less than `ipv4.MaxTotalSize`, which lets us build an IPv4 packet whose - // size exceeds the MTU. - mtu: 1000, - payloadLength: 1004, - expectErrorICMP: true, - icmpType: header.ICMPv4DstUnreachable, - icmpCode: header.ICMPv4FragmentationNeeded, - }, - { - name: "Fragmentation needed and DF not set", - TTL: 2, - sourceAddr: remoteIPv4Addr1, - destAddr: remoteIPv4Addr2, - mtu: 1000, - payloadLength: 1004, - expectPacketForwarded: true, - // Combined, these fragments have length of 1012 octets, which is equal to - // the length of the payload (1004 octets), plus the length of the ICMP - // header (8 octets). - expectedFragmentsForwarded: []fragmentInfo{ - // The first fragment has a length of the greatest multiple of 8 which is - // less than or equal to to `mtu - header.IPv4MinimumSize`. - {offset: 0, payloadSize: uint16(976), more: true}, - // The next fragment holds the rest of the packet. - {offset: uint16(976), payloadSize: 36, more: false}, + name: "Network unreachable", + TTL: 2, + srcAddr: remoteIPv4Addr1, + dstAddr: unreachableIPv4Addr, + icmpError: &icmpError{ + icmpType: header.ICMPv4DstUnreachable, + icmpCode: header.ICMPv4NetUnreachable, }, + expectedPacketUnrouteableErrors: 1, + expectPacketForwarded: false, + }, + { + name: "Link local destination", + TTL: 2, + srcAddr: remoteIPv4Addr1, + dstAddr: linkLocalIPv4Addr, + expectedLinkLocalDestErrors: 1, + expectPacketForwarded: false, + }, + { + name: "Link local source", + TTL: 2, + srcAddr: linkLocalIPv4Addr, + dstAddr: remoteIPv4Addr2, + expectedLinkLocalSourceErrors: 1, + expectPacketForwarded: false, }, } for _, test := range tests { @@ -375,29 +490,19 @@ func TestForwarding(t *testing.T) { // it give a more recognisable signature than 00,00,00,00. clock.Advance(time.Millisecond * randomTimeOffset) - // We expect at most a single packet in response to our ICMP Echo Request. - incomingEndpoint := channel.New(1, test.mtu, "") - defer incomingEndpoint.Close() - if err := s.CreateNIC(incomingNICID, incomingEndpoint); err != nil { - t.Fatalf("CreateNIC(%d, _): %s", incomingNICID, err) - } - incomingIPv4ProtoAddr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: incomingIPv4Addr} - if err := s.AddProtocolAddress(incomingNICID, incomingIPv4ProtoAddr, stack.AddressProperties{}); err != nil { - t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", incomingNICID, incomingIPv4ProtoAddr, err) - } + endpoints := make(map[tcpip.NICID]*channel.Endpoint) + for nicID, addr := range defaultEndpointConfigs { + ep := channel.New(1, ipv4.MaxTotalSize, "") + defer ep.Close() - expectedEmittedPacketCount := 1 - if len(test.expectedFragmentsForwarded) > expectedEmittedPacketCount { - expectedEmittedPacketCount = len(test.expectedFragmentsForwarded) - } - outgoingEndpoint := channel.New(expectedEmittedPacketCount, test.mtu, outgoingLinkAddr) - defer outgoingEndpoint.Close() - if err := s.CreateNIC(outgoingNICID, outgoingEndpoint); err != nil { - t.Fatalf("CreateNIC(%d, _): %s", outgoingNICID, err) - } - outgoingIPv4ProtoAddr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: outgoingIPv4Addr} - if err := s.AddProtocolAddress(outgoingNICID, outgoingIPv4ProtoAddr, stack.AddressProperties{}); err != nil { - t.Fatalf("AddProtocolAddress(%d, %+v, {}): %s", outgoingNICID, outgoingIPv4ProtoAddr, err) + if err := s.CreateNIC(nicID, ep); err != nil { + t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err) + } + addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr} + if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err) + } + endpoints[nicID] = ep } s.SetRouteTable([]tcpip.Route{ @@ -412,167 +517,564 @@ func TestForwarding(t *testing.T) { }) if err := s.SetForwardingDefaultAndAllNICs(header.IPv4ProtocolNumber, true); err != nil { - t.Fatalf("SetForwardingDefaultAndAllNICs(%d, true): %s", header.IPv4ProtocolNumber, err) + t.Fatalf("s.SetForwardingDefaultAndAllNICs(%d, true): %s", header.IPv4ProtocolNumber, err) } - ipHeaderLength := header.IPv4MinimumSize + len(test.options) - if ipHeaderLength > header.IPv4MaximumHeaderSize { - t.Fatalf("got ipHeaderLength = %d, want <= %d ", ipHeaderLength, header.IPv4MaximumHeaderSize) - } - icmpHeaderLength := header.ICMPv4MinimumSize - totalLength := ipHeaderLength + icmpHeaderLength + test.payloadLength - hdr := buffer.NewPrependable(totalLength) - hdr.Prepend(test.payloadLength) - icmpH := header.ICMPv4(hdr.Prepend(icmpHeaderLength)) - icmpH.SetIdent(randomIdent) - icmpH.SetSequence(randomSequence) - icmpH.SetType(header.ICMPv4Echo) - icmpH.SetCode(header.ICMPv4UnusedCode) - icmpH.SetChecksum(0) - icmpH.SetChecksum(^header.Checksum(icmpH, 0)) - ip := header.IPv4(hdr.Prepend(ipHeaderLength)) - ip.Encode(&header.IPv4Fields{ - TotalLength: uint16(totalLength), - Protocol: uint8(header.ICMPv4ProtocolNumber), - TTL: test.TTL, - SrcAddr: test.sourceAddr, - DstAddr: test.destAddr, - Flags: test.ipFlags, - }) - if len(test.options) != 0 { - ip.SetHeaderLength(uint8(ipHeaderLength)) - // Copy options manually. We do not use Encode for options so we can - // verify malformed options with handcrafted payloads. - if want, got := copy(ip.Options(), test.options), len(test.options); want != got { - t.Fatalf("got copy(ip.Options(), test.options) = %d, want = %d", got, want) - } - } - ip.SetChecksum(0) - ip.SetChecksum(^ip.CalculateChecksum()) - requestPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ - Data: hdr.View().ToVectorisedView(), - }) + requestPkt, expectedICMPErrorPayload := newICMPEchoPacket(t, test.srcAddr, test.dstAddr, test.TTL, packetOptions{options: test.options}) defer requestPkt.DecRef() - requestPkt.NetworkProtocolNumber = header.IPv4ProtocolNumber + + incomingEndpoint, ok := endpoints[incomingNICID] + if !ok { + t.Fatalf("endpoints[%d] = (_, false), want (_, true)", incomingNICID) + } incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt) reply := incomingEndpoint.Read() - if test.expectErrorICMP { + if test.icmpError != nil { if reply == nil { - t.Fatalf("expected ICMP packet type %d through incoming NIC", test.icmpType) - } - - // We expect the ICMP packet to contain as much of the original packet as - // possible up to a limit of 576 bytes, split between payload, IP header, - // and ICMP header. - expectedICMPPayloadLength := func() int { - maxICMPPacketLength := header.IPv4MinimumProcessableDatagramSize - maxICMPPayloadLength := maxICMPPacketLength - icmpHeaderLength - ipHeaderLength - if len(hdr.View()) > maxICMPPayloadLength { - return maxICMPPayloadLength - } - return len(hdr.View()) + t.Fatalf("Expected ICMP packet type %d through incoming NIC", test.icmpError.icmpType) } checker.IPv4(t, stack.PayloadSince(reply.NetworkHeader()), checker.SrcAddr(incomingIPv4Addr.Address), - checker.DstAddr(test.sourceAddr), + checker.DstAddr(test.srcAddr), checker.TTL(ipv4.DefaultTTL), checker.ICMPv4( checker.ICMPv4Checksum(), - checker.ICMPv4Type(test.icmpType), - checker.ICMPv4Code(test.icmpCode), - checker.ICMPv4Payload(hdr.View()[:expectedICMPPayloadLength()]), + checker.ICMPv4Type(test.icmpError.icmpType), + checker.ICMPv4Code(test.icmpError.icmpCode), + checker.ICMPv4Payload(expectedICMPErrorPayload), ), ) reply.DecRef() } else if reply != nil { - t.Fatalf("expected no ICMP packet through incoming NIC, instead found: %#v", reply) + t.Fatalf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply) + } + + outgoingEndpoint, ok := endpoints[outgoingNICID] + if !ok { + t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID) } if test.expectPacketForwarded { - if len(test.expectedFragmentsForwarded) != 0 { - var fragmentedPackets []*stack.PacketBuffer - for i := 0; i < len(test.expectedFragmentsForwarded); i++ { - reply := outgoingEndpoint.Read() - if reply == nil { - t.Fatal("expected ICMP Echo fragment through outgoing NIC") - } - fragmentedPackets = append(fragmentedPackets, reply) - } - - // The forwarded packet's TTL will have been decremented. - ipHeader := header.IPv4(requestPkt.NetworkHeader().View()) - ipHeader.SetTTL(ipHeader.TTL() - 1) - - // Forwarded packets have available header bytes equalling the sum of the - // maximum IP header size and the maximum size allocated for link layer - // headers. In this case, no size is allocated for link layer headers. - expectedAvailableHeaderBytes := header.IPv4MaximumHeaderSize - if err := compareFragments(fragmentedPackets, requestPkt, test.mtu, test.expectedFragmentsForwarded, header.ICMPv4ProtocolNumber, true /* withIPHeader */, expectedAvailableHeaderBytes); err != nil { - t.Error(err) - } - for _, pkt := range fragmentedPackets { - pkt.DecRef() - } - } else { - reply := outgoingEndpoint.Read() - if reply == nil { - t.Fatal("expected ICMP Echo packet through outgoing NIC") - } - - checker.IPv4(t, stack.PayloadSince(reply.NetworkHeader()), - checker.SrcAddr(test.sourceAddr), - checker.DstAddr(test.destAddr), - checker.TTL(test.TTL-1), - checker.IPv4Options(test.forwardedOptions), - checker.ICMPv4( - checker.ICMPv4Checksum(), - checker.ICMPv4Type(header.ICMPv4Echo), - checker.ICMPv4Code(header.ICMPv4UnusedCode), - checker.ICMPv4Payload(nil), - ), - ) - reply.DecRef() + reply := outgoingEndpoint.Read() + if reply == nil { + t.Fatal("Expected ICMP Echo packet through outgoing NIC") } + + checker.IPv4(t, stack.PayloadSince(reply.NetworkHeader()), + checker.SrcAddr(test.srcAddr), + checker.DstAddr(test.dstAddr), + checker.TTL(test.TTL-1), + checker.IPv4Options(test.forwardedOptions), + checker.ICMPv4( + checker.ICMPv4Checksum(), + checker.ICMPv4Type(header.ICMPv4Echo), + checker.ICMPv4Code(header.ICMPv4UnusedCode), + checker.ICMPv4Payload(nil), + ), + ) + reply.DecRef() } else { if reply := outgoingEndpoint.Read(); reply != nil { - t.Fatalf("expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply) + t.Fatalf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply) } } - boolToInt := func(val bool) uint64 { - if val { - return 1 + + if got, want := s.Stats().IP.Forwarding.LinkLocalSource.Value(), test.expectedLinkLocalSourceErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.LinkLocalSource.Value() = %d, want = %d", got, want) + } + + if got, want := s.Stats().IP.Forwarding.LinkLocalDestination.Value(), test.expectedLinkLocalDestErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.LinkLocalDestination.Value() = %d, want = %d", got, want) + } + + if got, want := s.Stats().IP.MalformedPacketsReceived.Value(), test.expectedMalformedPacketErrors; got != want { + t.Errorf("s.Stats().IP.MalformedPacketsReceived.Value() = %d, want = %d", got, want) + } + + if got, want := s.Stats().IP.Forwarding.ExhaustedTTL.Value(), test.expectedExhaustedTTLErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.ExhaustedTTL.Value() = %d, want = %d", got, want) + } + + if got, want := s.Stats().IP.Forwarding.Unrouteable.Value(), test.expectedPacketUnrouteableErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.Unrouteable.Value() = %d, want = %d", got, want) + } + + expectedTotalErrors := test.expectedLinkLocalSourceErrors + test.expectedLinkLocalDestErrors + test.expectedMalformedPacketErrors + test.expectedExhaustedTTLErrors + test.expectedPacketUnrouteableErrors + if got, want := s.Stats().IP.Forwarding.Errors.Value(), expectedTotalErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want) + } + }) + } +} + +func TestFragmentForwarding(t *testing.T) { + const ( + defaultMTU = 1000 + defaultPayloadLength = defaultMTU + 4 + packetTTL = 2 + ) + + tests := []struct { + name string + ipFlags uint8 + icmpError *icmpError + expectedPacketTooBigErrors uint64 + expectedFragmentsForwarded []fragmentInfo + }{ + { + name: "Fragmentation needed and DF set", + ipFlags: header.IPv4FlagDontFragment, + // We've picked this MTU because it is: + // + // 1) Greater than the minimum MTU that IPv4 hosts are required to process + // (576 bytes). As per RFC 1812, Section 4.3.2.3: + // + // The ICMP datagram SHOULD contain as much of the original datagram as + // possible without the length of the ICMP datagram exceeding 576 bytes. + // + // Therefore, setting an MTU greater than 576 bytes ensures that we can fit a + // complete ICMP packet on the incoming endpoint (and make assertions about + // it). + // + // 2) Less than `ipv4.MaxTotalSize`, which lets us build an IPv4 packet whose + // size exceeds the MTU. + icmpError: &icmpError{ + icmpType: header.ICMPv4DstUnreachable, + icmpCode: header.ICMPv4FragmentationNeeded, + }, + expectedFragmentsForwarded: []fragmentInfo{}, + expectedPacketTooBigErrors: 1, + }, + { + name: "Fragmentation needed and DF not set", + expectedFragmentsForwarded: []fragmentInfo{ + // The first fragment has a length of the greatest multiple of 8 which is + // less than or equal to to `mtu - header.IPv4MinimumSize`. + {offset: 0, payloadSize: uint16(976), more: true}, + // The next fragment holds the rest of the packet. + {offset: uint16(976), payloadSize: 36, more: false}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := newTestContext() + defer ctx.cleanup() + s := ctx.s + + endpoints := make(map[tcpip.NICID]*channel.Endpoint) + for nicID, addr := range defaultEndpointConfigs { + // For the input interface, we expect at most a single packet in + // response to our ICMP Echo Request. + expectedEmittedPacketCount := 1 + + if nicID == outgoingNICID { + expectedEmittedPacketCount = max(1, len(test.expectedFragmentsForwarded)) } - return 0 + + ep := channel.New(expectedEmittedPacketCount, defaultMTU, "") + defer ep.Close() + + if err := s.CreateNIC(nicID, ep); err != nil { + t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err) + } + addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr} + if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err) + } + endpoints[nicID] = ep } - if got, want := s.Stats().IP.Forwarding.LinkLocalSource.Value(), boolToInt(test.expectLinkLocalSourceError); got != want { - t.Errorf("got s.Stats().IP.Forwarding.LinkLocalSource.Value() = %d, want = %d", got, want) + s.SetRouteTable([]tcpip.Route{ + { + Destination: incomingIPv4Addr.Subnet(), + NIC: incomingNICID, + }, + { + Destination: outgoingIPv4Addr.Subnet(), + NIC: outgoingNICID, + }, + }) + + if err := s.SetForwardingDefaultAndAllNICs(header.IPv4ProtocolNumber, true); err != nil { + t.Fatalf("s.SetForwardingDefaultAndAllNICs(%d, true): %s", header.IPv4ProtocolNumber, err) } - if got, want := s.Stats().IP.Forwarding.LinkLocalDestination.Value(), boolToInt(test.expectLinkLocalDestError); got != want { - t.Errorf("got s.Stats().IP.Forwarding.LinkLocalDestination.Value() = %d, want = %d", got, want) + requestPkt, expectedICMPErrorPayload := newICMPEchoPacket(t, remoteIPv4Addr1, remoteIPv4Addr2, packetTTL, packetOptions{ipFlags: test.ipFlags, payloadLength: defaultPayloadLength}) + defer requestPkt.DecRef() + + incomingEndpoint, ok := endpoints[incomingNICID] + if !ok { + t.Fatalf("endpoints[%d] = (_, false), want (_, true)", incomingNICID) + } + incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt) + reply := incomingEndpoint.Read() + + if test.icmpError != nil { + if reply == nil { + t.Fatalf("Expected ICMP packet type %d through incoming NIC", test.icmpError.icmpType) + } + + checker.IPv4(t, stack.PayloadSince(reply.NetworkHeader()), + checker.SrcAddr(incomingIPv4Addr.Address), + checker.DstAddr(remoteIPv4Addr1), + checker.TTL(ipv4.DefaultTTL), + checker.ICMPv4( + checker.ICMPv4Checksum(), + checker.ICMPv4Type(test.icmpError.icmpType), + checker.ICMPv4Code(test.icmpError.icmpCode), + checker.ICMPv4Payload(expectedICMPErrorPayload), + ), + ) + reply.DecRef() + } else if reply != nil { + t.Fatalf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply) } - if got, want := s.Stats().IP.MalformedPacketsReceived.Value(), boolToInt(test.icmpType == header.ICMPv4ParamProblem); got != want { - t.Errorf("got s.Stats().IP.MalformedPacketsReceived.Value() = %d, want = %d", got, want) + outgoingEndpoint, ok := endpoints[outgoingNICID] + if !ok { + t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID) } - if got, want := s.Stats().IP.Forwarding.ExhaustedTTL.Value(), boolToInt(test.TTL <= 0); got != want { - t.Errorf("got s.Stats().IP.Forwarding.ExhaustedTTL.Value() = %d, want = %d", got, want) + if len(test.expectedFragmentsForwarded) > 0 { + checkFragements(t, outgoingEndpoint, test.expectedFragmentsForwarded, requestPkt) + } else { + if reply := outgoingEndpoint.Read(); reply != nil { + t.Errorf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply) + } } - if got, want := s.Stats().IP.Forwarding.Unrouteable.Value(), boolToInt(test.expectPacketUnrouteableError); got != want { - t.Errorf("got s.Stats().IP.Forwarding.Unrouteable.Value() = %d, want = %d", got, want) + if got, want := s.Stats().IP.Forwarding.PacketTooBig.Value(), test.expectedPacketTooBigErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.PacketTooBig.Value() = %d, want = %d", got, want) } - if got, want := s.Stats().IP.Forwarding.Errors.Value(), boolToInt(!test.expectPacketForwarded); got != want { - t.Errorf("got s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want) + if got, want := s.Stats().IP.Forwarding.Errors.Value(), test.expectedPacketTooBigErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want) + } + }) + } +} + +func TestMulticastFragmentForwarding(t *testing.T) { + const ( + defaultMTU = 1000 + defaultPayloadLength = defaultMTU + 4 + packetTTL = 2 + multicastRouteMinTTL = 2 + ) + + tests := []struct { + name string + ipFlags uint8 + icmpError *icmpError + expectedPacketTooBigErrors uint64 + expectedFragmentsForwarded []fragmentInfo + }{ + { + name: "Fragmentation needed and DF set", + ipFlags: header.IPv4FlagDontFragment, + // We've picked this MTU because it is: + // + // 1) Greater than the minimum MTU that IPv4 hosts are required to process + // (576 bytes). As per RFC 1812, Section 4.3.2.3: + // + // The ICMP datagram SHOULD contain as much of the original datagram as + // possible without the length of the ICMP datagram exceeding 576 bytes. + // + // Therefore, setting an MTU greater than 576 bytes ensures that we can fit a + // complete ICMP packet on the incoming endpoint (and make assertions about + // it). + // + // 2) Less than `ipv4.MaxTotalSize`, which lets us build an IPv4 packet whose + // size exceeds the MTU. + icmpError: &icmpError{ + icmpType: header.ICMPv4DstUnreachable, + icmpCode: header.ICMPv4FragmentationNeeded, + }, + expectedFragmentsForwarded: []fragmentInfo{}, + expectedPacketTooBigErrors: 1, + }, + { + name: "Fragmentation needed and DF not set", + expectedFragmentsForwarded: []fragmentInfo{ + // The first fragment has a length of the greatest multiple of 8 which is + // less than or equal to to `mtu - header.IPv4MinimumSize`. + {offset: 0, payloadSize: uint16(976), more: true}, + // The next fragment holds the rest of the packet. + {offset: uint16(976), payloadSize: 36, more: false}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := newTestContext() + defer ctx.cleanup() + s := ctx.s + + endpoints := make(map[tcpip.NICID]*channel.Endpoint) + for nicID, addr := range defaultEndpointConfigs { + // For the input interface, we expect at most a single packet in + // response to our ICMP Echo Request. + expectedEmittedPacketCount := 1 + + if nicID == outgoingNICID { + expectedEmittedPacketCount = max(1, len(test.expectedFragmentsForwarded)) + } + + ep := channel.New(expectedEmittedPacketCount, defaultMTU, "") + defer ep.Close() + + if err := s.CreateNIC(nicID, ep); err != nil { + t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err) + } + addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr} + if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err) + } + s.SetNICMulticastForwarding(nicID, ipv4.ProtocolNumber, true /* enabled */) + endpoints[nicID] = ep } - if got, want := s.Stats().IP.Forwarding.PacketTooBig.Value(), boolToInt(test.icmpCode == header.ICMPv4FragmentationNeeded); got != want { - t.Errorf("got s.Stats().IP.Forwarding.PacketTooBig.Value() = %d, want = %d", got, want) + // Add a route that could theoretically be used to send an ICMP error. + // Note that such an error should never be sent for multicast. + s.SetRouteTable([]tcpip.Route{ + { + Destination: header.IPv4EmptySubnet, + NIC: outgoingNICID, + }, + }) + + outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{ + {ID: outgoingNICID, MinTTL: multicastRouteMinTTL}, + } + addresses := stack.UnicastSourceAndMulticastDestination{ + Source: remoteIPv4Addr1, + Destination: multicastIPv4Addr, + } + + route := stack.MulticastRoute{ + ExpectedInputInterface: incomingNICID, + OutgoingInterfaces: outgoingInterfaces, + } + + if err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route); err != nil { + t.Fatalf("s.AddMulticastRoute(%d, %#v, %#v): %s", ipv4.ProtocolNumber, addresses, route, err) + } + + requestPkt, _ := newICMPEchoPacket(t, remoteIPv4Addr1, multicastIPv4Addr, packetTTL, packetOptions{ipFlags: test.ipFlags, payloadLength: defaultPayloadLength}) + defer requestPkt.DecRef() + + incomingEndpoint, ok := endpoints[incomingNICID] + if !ok { + t.Fatalf("got endpoints[%d] = (_, false), want (_, true)", incomingNICID) + } + incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt) + reply := incomingEndpoint.Read() + + if reply != nil { + // An ICMP error should never be sent in response to a multicast packet. + t.Errorf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply) + } + + outgoingEndpoint, ok := endpoints[outgoingNICID] + if !ok { + t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID) + } + + if len(test.expectedFragmentsForwarded) > 0 { + checkFragements(t, outgoingEndpoint, test.expectedFragmentsForwarded, requestPkt) + } else { + if reply := outgoingEndpoint.Read(); reply != nil { + t.Errorf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply) + } + } + + if got, want := s.Stats().IP.Forwarding.PacketTooBig.Value(), test.expectedPacketTooBigErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.PacketTooBig.Value() = %d, want = %d", got, want) + } + + if got, want := s.Stats().IP.Forwarding.Errors.Value(), test.expectedPacketTooBigErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want) + } + }) + } +} + +func TestMulticastForwardingOptions(t *testing.T) { + const ( + randomTimeOffset = 0x10203040 + packetTTL = 2 + multicastRouteMinTTL = 2 + ) + + tests := []struct { + name string + options header.IPv4Options + forwardedOptions header.IPv4Options + expectedMalformedPacketErrors uint64 + expectPacketForwarded bool + }{ + { + name: "four EOL options", + options: header.IPv4Options{0, 0, 0, 0}, + forwardedOptions: header.IPv4Options{0, 0, 0, 0}, + expectPacketForwarded: true, + }, + { + name: "TS type 1 full", + options: header.IPv4Options{ + 68, 12, 13, 0xF1, + 192, 168, 1, 12, + 1, 2, 3, 4, + }, + expectedMalformedPacketErrors: 1, + }, + { + name: "TS type 0", + options: header.IPv4Options{ + 68, 24, 21, 0x00, + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16, + 0, 0, 0, 0, + }, + forwardedOptions: header.IPv4Options{ + 68, 24, 25, 0x00, + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16, + 0x00, 0xad, 0x1c, 0x40, // time we expect from fakeclock + }, + expectPacketForwarded: true, + }, + { + name: "end of options list", + options: header.IPv4Options{ + 68, 12, 13, 0x11, + 192, 168, 1, 12, + 1, 2, 3, 4, + 0, 10, 3, 99, // EOL followed by junk + 1, 2, 3, 4, + }, + forwardedOptions: header.IPv4Options{ + 68, 12, 13, 0x21, + 192, 168, 1, 12, + 1, 2, 3, 4, + 0, // End of Options hides following bytes. + 0, 0, 0, // 7 bytes unknown option removed. + 0, 0, 0, 0, + }, + expectPacketForwarded: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := newTestContext() + defer ctx.cleanup() + s := ctx.s + clock := ctx.clock + + // Advance the clock by some unimportant amount to make + // it give a more recognisable signature than 00,00,00,00. + clock.Advance(time.Millisecond * randomTimeOffset) + + endpoints := make(map[tcpip.NICID]*channel.Endpoint) + for nicID, addr := range defaultEndpointConfigs { + ep := channel.New(1, ipv4.MaxTotalSize, "") + defer ep.Close() + + if err := s.CreateNIC(nicID, ep); err != nil { + t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err) + } + addr := tcpip.ProtocolAddress{Protocol: header.IPv4ProtocolNumber, AddressWithPrefix: addr} + if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err) + } + s.SetNICMulticastForwarding(nicID, ipv4.ProtocolNumber, true /* enabled */) + endpoints[nicID] = ep + } + + // Add a route that could theoretically be used to send an ICMP error. + // Note that such an error should never be sent for multicast. + s.SetRouteTable([]tcpip.Route{ + { + Destination: header.IPv4EmptySubnet, + NIC: outgoingNICID, + }, + }) + + outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{ + {ID: outgoingNICID, MinTTL: multicastRouteMinTTL}, + } + addresses := stack.UnicastSourceAndMulticastDestination{ + Source: remoteIPv4Addr1, + Destination: multicastIPv4Addr, + } + + route := stack.MulticastRoute{ + ExpectedInputInterface: incomingNICID, + OutgoingInterfaces: outgoingInterfaces, + } + + if err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route); err != nil { + t.Fatalf("s.AddMulticastRoute(%d, %#v, %#v): %s", ipv4.ProtocolNumber, addresses, route, err) + } + + requestPkt, _ := newICMPEchoPacket(t, remoteIPv4Addr1, multicastIPv4Addr, packetTTL, packetOptions{options: test.options}) + defer requestPkt.DecRef() + + incomingEndpoint, ok := endpoints[incomingNICID] + if !ok { + t.Fatalf("endpoints[%d] = (_, false), want (_, true)", incomingNICID) + } + incomingEndpoint.InjectInbound(header.IPv4ProtocolNumber, requestPkt) + reply := incomingEndpoint.Read() + + if reply != nil { + // An ICMP error should never be sent in response to a multicast packet. + t.Errorf("Expected no ICMP packet through incoming NIC, instead found: %#v", reply) + } + + outgoingEndpoint, ok := endpoints[outgoingNICID] + if !ok { + t.Fatalf("endpoints[%d] = (_, false), want (_, true)", outgoingNICID) + } + + if test.expectPacketForwarded { + reply := outgoingEndpoint.Read() + if reply == nil { + t.Fatal("Expected ICMP Echo packet through outgoing NIC") + } + + checker.IPv4(t, stack.PayloadSince(reply.NetworkHeader()), + checker.SrcAddr(remoteIPv4Addr1), + checker.DstAddr(multicastIPv4Addr), + checker.TTL(packetTTL-1), + checker.IPv4Options(test.forwardedOptions), + checker.ICMPv4( + checker.ICMPv4Checksum(), + checker.ICMPv4Type(header.ICMPv4Echo), + checker.ICMPv4Code(header.ICMPv4UnusedCode), + checker.ICMPv4Payload(nil), + ), + ) + reply.DecRef() + } else { + if reply := outgoingEndpoint.Read(); reply != nil { + t.Fatalf("Expected no ICMP Echo packet through outgoing NIC, instead found: %#v", reply) + } + } + + if got, want := s.Stats().IP.MalformedPacketsReceived.Value(), test.expectedMalformedPacketErrors; got != want { + t.Errorf("s.Stats().IP.MalformedPacketsReceived.Value() = %d, want = %d", got, want) + } + + if got, want := s.Stats().IP.Forwarding.Errors.Value(), test.expectedMalformedPacketErrors; got != want { + t.Errorf("s.Stats().IP.Forwarding.Errors.Value() = %d, want = %d", got, want) } }) } diff --git a/pkg/tcpip/stack/BUILD b/pkg/tcpip/stack/BUILD index 060e644e4..0af5bca0c 100644 --- a/pkg/tcpip/stack/BUILD +++ b/pkg/tcpip/stack/BUILD @@ -127,6 +127,7 @@ go_test( "//pkg/tcpip/transport/udp", "//pkg/waiter", "@com_github_google_go_cmp//cmp:go_default_library", + "@com_github_google_go_cmp//cmp/cmpopts:go_default_library", ], ) diff --git a/pkg/tcpip/stack/registration.go b/pkg/tcpip/stack/registration.go index ce69972c2..0282983be 100644 --- a/pkg/tcpip/stack/registration.go +++ b/pkg/tcpip/stack/registration.go @@ -792,6 +792,18 @@ type MulticastRoute struct { OutgoingInterfaces []MulticastRouteOutgoingInterface } +// MulticastForwardingNetworkProtocol is the interface that needs to be +// implemented by the network protocols that support multicast forwarding. +type MulticastForwardingNetworkProtocol interface { + NetworkProtocol + + // AddMulticastRoute adds a route to the multicast routing table such that + // packets matching the addresses will be forwarded using the provided route. + // + // Returns an error if the addresses or route is invalid. + AddMulticastRoute(UnicastSourceAndMulticastDestination, MulticastRoute) tcpip.Error +} + // NetworkDispatcher contains the methods used by the network stack to deliver // inbound/outbound packets to the appropriate network/packet(if any) endpoints. type NetworkDispatcher interface { diff --git a/pkg/tcpip/stack/stack.go b/pkg/tcpip/stack/stack.go index 413864df4..9abe56a1a 100644 --- a/pkg/tcpip/stack/stack.go +++ b/pkg/tcpip/stack/stack.go @@ -564,6 +564,22 @@ func (s *Stack) SetForwardingDefaultAndAllNICs(protocol tcpip.NetworkProtocolNum return nil } +// AddMulticastRoute adds a multicast route to be used for the specified +// addresses and protocol. +func (s *Stack) AddMulticastRoute(protocol tcpip.NetworkProtocolNumber, addresses UnicastSourceAndMulticastDestination, route MulticastRoute) tcpip.Error { + netProto, ok := s.networkProtocols[protocol] + if !ok { + return &tcpip.ErrUnknownProtocol{} + } + + forwardingNetProto, ok := netProto.(MulticastForwardingNetworkProtocol) + if !ok { + return &tcpip.ErrNotSupported{} + } + + return forwardingNetProto.AddMulticastRoute(addresses, route) +} + // SetNICMulticastForwarding enables or disables multicast packet forwarding on // the specified NIC for the passed protocol. // @@ -1045,6 +1061,22 @@ func (s *Stack) getAddressEP(nic *nic, localAddr, remoteAddr tcpip.Address, netP return nic.findEndpoint(netProto, localAddr, CanBePrimaryEndpoint) } +// NewRouteForMulticast returns a Route that may be used to forward multicast +// packets. +// +// Returns nil if validation fails. +func (s *Stack) NewRouteForMulticast(nicID tcpip.NICID, remoteAddr tcpip.Address, netProto tcpip.NetworkProtocolNumber) *Route { + nic, ok := s.nics[nicID] + if !ok || !nic.Enabled() { + return nil + } + + if addressEndpoint := s.getAddressEP(nic, "" /* localAddr */, remoteAddr, netProto); addressEndpoint != nil { + return constructAndValidateRoute(netProto, addressEndpoint, nic, nic, "" /* gateway */, "" /* localAddr */, remoteAddr, s.handleLocal, false /* multicastLoop */) + } + return nil +} + // findLocalRouteFromNICRLocked is like findLocalRouteRLocked but finds a route // from the specified NIC. // diff --git a/pkg/tcpip/stack/stack_test.go b/pkg/tcpip/stack/stack_test.go index 4ef23c7b2..1da9054e0 100644 --- a/pkg/tcpip/stack/stack_test.go +++ b/pkg/tcpip/stack/stack_test.go @@ -27,6 +27,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "gvisor.dev/gvisor/pkg/rand" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/tcpip" @@ -225,6 +226,11 @@ type fakeNetworkEndpointStats struct{} // IsNetworkEndpointStats implements stack.NetworkEndpointStats. func (*fakeNetworkEndpointStats) IsNetworkEndpointStats() {} +type addMulticastRouteData struct { + addresses stack.UnicastSourceAndMulticastDestination + route stack.MulticastRoute +} + // fakeNetworkProtocol is a network-layer protocol descriptor. It aggregates the // number of packets sent and received via endpoints of this protocol. The index // where packets are added is given by the packet's destination address MOD 10. @@ -234,6 +240,8 @@ type fakeNetworkProtocol struct { packetCount [10]int sendPacketCount [10]int defaultTTL uint8 + + addMulticastRouteData addMulticastRouteData } func (*fakeNetworkProtocol) Number() tcpip.NetworkProtocolNumber { @@ -298,6 +306,13 @@ func (*fakeNetworkProtocol) Parse(pkt *stack.PacketBuffer) (tcpip.TransportProto return tcpip.TransportProtocolNumber(hdr[protocolNumberOffset]), true, true } +// AddMulticastRoute implements +// MulticastForwardingNetworkProtocol.AddMulticastRoute. +func (f *fakeNetworkProtocol) AddMulticastRoute(addresses stack.UnicastSourceAndMulticastDestination, route stack.MulticastRoute) tcpip.Error { + f.addMulticastRouteData = addMulticastRouteData{addresses, route} + return nil +} + // Forwarding implements stack.ForwardingNetworkEndpoint. func (f *fakeNetworkEndpoint) Forwarding() bool { f.mu.RLock() @@ -4662,6 +4677,69 @@ func TestFindRouteWithForwarding(t *testing.T) { } } +func TestAddMulticastRoute(t *testing.T) { + const ( + incomingNICID = 1 + outgoingNICID = 2 + ) + address := testutil.MustParse4("192.168.1.1") + outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{{ID: outgoingNICID, MinTTL: 3}} + addresses := stack.UnicastSourceAndMulticastDestination{Source: address, Destination: address} + + tests := []struct { + name string + netProto tcpip.NetworkProtocolNumber + factory stack.NetworkProtocolFactory + wantErr tcpip.Error + }{ + { + name: "valid", + netProto: fakeNetNumber, + factory: fakeNetFactory, + wantErr: nil, + }, + { + name: "unknown protocol", + factory: fakeNetFactory, + netProto: arp.ProtocolNumber, + wantErr: &tcpip.ErrUnknownProtocol{}, + }, + { + name: "not supported", + factory: arp.NewProtocol, + netProto: arp.ProtocolNumber, + wantErr: &tcpip.ErrNotSupported{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{test.factory}, + }) + + route := stack.MulticastRoute{ + ExpectedInputInterface: incomingNICID, + OutgoingInterfaces: outgoingInterfaces, + } + + err := s.AddMulticastRoute(test.netProto, addresses, route) + + if !cmp.Equal(err, test.wantErr, cmpopts.EquateErrors()) { + t.Errorf("s.AddMulticastRoute(%d, %#v, %#v) = %s, want %s", test.netProto, addresses, route, err, test.wantErr) + } + + if test.wantErr == nil { + fakeNet := s.NetworkProtocolInstance(fakeNetNumber).(*fakeNetworkProtocol) + + expectedAddMulticastRouteData := addMulticastRouteData{addresses, route} + if !cmp.Equal(fakeNet.addMulticastRouteData, expectedAddMulticastRouteData, cmp.AllowUnexported(addMulticastRouteData{}, stack.MulticastRoute{})) { + t.Errorf("fakeNet.addMulticastRouteData = %#v, want = %#v", fakeNet.addMulticastRouteData, expectedAddMulticastRouteData) + } + } + }) + } +} + func TestNICForwarding(t *testing.T) { const nicID = 1 diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go index f125c7fd3..81013e522 100644 --- a/pkg/tcpip/tcpip.go +++ b/pkg/tcpip/tcpip.go @@ -1698,11 +1698,24 @@ type IPForwardingStats struct { // header. ExtensionHeaderProblem *StatCounter + // UnexpectedMulticastInputInterface is the number of multicast packets that + // were received on an interface that did not match the corresponding route's + // expected input interface. + UnexpectedMulticastInputInterface *StatCounter + + // UnknownOutputEndpoint is the number of packets that could not be forwarded + // because the output endpoint could not be found. + UnknownOutputEndpoint *StatCounter + + // NoMulticastPendingQueueBufferSpace is the number of multicast packets that + // were dropped due to insufficent buffer space in the pending packet queue. + NoMulticastPendingQueueBufferSpace *StatCounter + // Errors is the number of IP packets received which could not be // successfully forwarded. Errors *StatCounter - // LINT.ThenChange(network/internal/ip/stats.go:multiCounterIPForwardingStats) + // LINT.ThenChange(network/internal/ip/stats.go:MultiCounterIPForwardingStats) } // IPStats collects IP-specific stats (both v4 and v6). diff --git a/pkg/tcpip/tests/integration/BUILD b/pkg/tcpip/tests/integration/BUILD index fa7588799..a195d1717 100644 --- a/pkg/tcpip/tests/integration/BUILD +++ b/pkg/tcpip/tests/integration/BUILD @@ -167,3 +167,24 @@ go_test( "@com_github_google_go_cmp//cmp:go_default_library", ], ) + +go_test( + name = "multicast_forward_test", + size = "small", + srcs = ["multicast_forward_test.go"], + deps = [ + "//pkg/refs", + "//pkg/refsvfs2", + "//pkg/tcpip", + "//pkg/tcpip/checker", + "//pkg/tcpip/header", + "//pkg/tcpip/link/channel", + "//pkg/tcpip/network/ipv4", + "//pkg/tcpip/stack", + "//pkg/tcpip/tests/utils", + "//pkg/tcpip/testutil", + "//pkg/tcpip/transport/udp", + "@com_github_google_go_cmp//cmp:go_default_library", + "@com_github_google_go_cmp//cmp/cmpopts:go_default_library", + ], +) diff --git a/pkg/tcpip/tests/integration/forward_test.go b/pkg/tcpip/tests/integration/forward_test.go index 89b468641..09e0611c0 100644 --- a/pkg/tcpip/tests/integration/forward_test.go +++ b/pkg/tcpip/tests/integration/forward_test.go @@ -350,15 +350,14 @@ func TestForwarding(t *testing.T) { } } -func TestMulticastForwarding(t *testing.T) { +func TestUnicastForwarding(t *testing.T) { const ( nicID1 = 1 nicID2 = 2 ) var ( - ipv4LinkLocalUnicastAddr = testutil.MustParse4("169.254.0.10") - ipv4LinkLocalMulticastAddr = testutil.MustParse4("224.0.0.10") + ipv4LinkLocalUnicastAddr = testutil.MustParse4("169.254.0.10") ipv6LinkLocalUnicastAddr = testutil.MustParse6("fe80::a") ipv6LinkLocalMulticastAddr = testutil.MustParse6("ff02::a") @@ -371,13 +370,6 @@ func TestMulticastForwarding(t *testing.T) { expectForward bool checker func(*testing.T, []byte) }{ - { - name: "IPv4 link-local multicast destination", - srcAddr: utils.RemoteIPv4Addr, - dstAddr: ipv4LinkLocalMulticastAddr, - rx: rxICMPv4EchoRequest, - expectForward: false, - }, { name: "IPv4 link-local source", srcAddr: ipv4LinkLocalUnicastAddr, @@ -402,17 +394,9 @@ func TestMulticastForwarding(t *testing.T) { forwardedICMPv4EchoRequestChecker(t, b, utils.RemoteIPv4Addr, utils.Ipv4Addr2.AddressWithPrefix.Address) }, }, - { - name: "IPv4 non-link-local multicast", - srcAddr: utils.RemoteIPv4Addr, - dstAddr: ipv4GlobalMulticastAddr, - rx: rxICMPv4EchoRequest, - expectForward: true, - checker: func(t *testing.T, b []byte) { - forwardedICMPv4EchoRequestChecker(t, b, utils.RemoteIPv4Addr, ipv4GlobalMulticastAddr) - }, - }, - + // TODO(https://gvisor.dev/issue/7338): Move the IPv6 multicast forwarding + // tests to TestMulticastForwarding. Currently, they rely on the unicast + // routing table. { name: "IPv6 link-local multicast destination", srcAddr: utils.RemoteIPv6Addr, @@ -420,6 +404,16 @@ func TestMulticastForwarding(t *testing.T) { rx: rxICMPv6EchoRequest, expectForward: false, }, + { + name: "IPv6 non-link-local multicast", + srcAddr: utils.RemoteIPv6Addr, + dstAddr: ipv6GlobalMulticastAddr, + rx: rxICMPv6EchoRequest, + expectForward: true, + checker: func(t *testing.T, b []byte) { + forwardedICMPv6EchoRequestChecker(t, b, utils.RemoteIPv6Addr, ipv6GlobalMulticastAddr) + }, + }, { name: "IPv6 link-local source", srcAddr: ipv6LinkLocalUnicastAddr, @@ -444,16 +438,6 @@ func TestMulticastForwarding(t *testing.T) { forwardedICMPv6EchoRequestChecker(t, b, utils.RemoteIPv6Addr, utils.Ipv6Addr2.AddressWithPrefix.Address) }, }, - { - name: "IPv6 non-link-local multicast", - srcAddr: utils.RemoteIPv6Addr, - dstAddr: ipv6GlobalMulticastAddr, - rx: rxICMPv6EchoRequest, - expectForward: true, - checker: func(t *testing.T, b []byte) { - forwardedICMPv6EchoRequestChecker(t, b, utils.RemoteIPv6Addr, ipv6GlobalMulticastAddr) - }, - }, } for _, test := range tests { @@ -544,16 +528,6 @@ func TestPerInterfaceForwarding(t *testing.T) { forwardedICMPv4EchoRequestChecker(t, b, utils.RemoteIPv4Addr, utils.Ipv4Addr2.AddressWithPrefix.Address) }, }, - { - name: "IPv4 multicast", - srcAddr: utils.RemoteIPv4Addr, - dstAddr: ipv4GlobalMulticastAddr, - rx: rxICMPv4EchoRequest, - checker: func(t *testing.T, b []byte) { - forwardedICMPv4EchoRequestChecker(t, b, utils.RemoteIPv4Addr, ipv4GlobalMulticastAddr) - }, - }, - { name: "IPv6 unicast", srcAddr: utils.RemoteIPv6Addr, @@ -563,6 +537,9 @@ func TestPerInterfaceForwarding(t *testing.T) { forwardedICMPv6EchoRequestChecker(t, b, utils.RemoteIPv6Addr, utils.Ipv6Addr2.AddressWithPrefix.Address) }, }, + // TODO(https://gvisor.dev/issue/7338): Move the IPv6 multicast forwarding + // tests to TestMulticastForwarding. Currently, they rely on the unicast + // routing table. { name: "IPv6 multicast", srcAddr: utils.RemoteIPv6Addr, @@ -656,7 +633,7 @@ func TestPerInterfaceForwarding(t *testing.T) { { nicID: nicID2, nicEP: e2, - otherNICID: nicID2, + otherNICID: nicID1, otherNICEP: e1, expectForwarding: false, }, diff --git a/pkg/tcpip/tests/integration/multicast_forward_test.go b/pkg/tcpip/tests/integration/multicast_forward_test.go new file mode 100644 index 000000000..34b9252ed --- /dev/null +++ b/pkg/tcpip/tests/integration/multicast_forward_test.go @@ -0,0 +1,585 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multicast_forward_test + +import ( + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "gvisor.dev/gvisor/pkg/refs" + "gvisor.dev/gvisor/pkg/refsvfs2" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/checker" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/tests/utils" + "gvisor.dev/gvisor/pkg/tcpip/testutil" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" +) + +const ( + incomingNICID = 1 + outgoingNICID = 2 + otherOutgoingNICID = 3 + otherNICID = 4 + unknownNICID = 5 + packetTTL = 64 + routeMinTTL = 2 +) + +type addrType int + +const ( + emptyAddr addrType = iota + anyAddr + linkLocalMulticastAddr + linkLocalUnicastAddr + multicastAddr + otherMulticastAddr + remoteUnicastAddr +) + +type endpointAddrType int + +const ( + incomingEndpointAddr endpointAddrType = iota + otherEndpointAddr + outgoingEndpointAddr + otherOutgoingEndpointAddr +) + +func getAddr(addrType addrType) tcpip.Address { + switch addrType { + case anyAddr: + return header.IPv4Any + case emptyAddr: + return "" + case linkLocalMulticastAddr: + return testutil.MustParse4("224.0.0.1") + case linkLocalUnicastAddr: + return testutil.MustParse4("169.254.0.10") + case multicastAddr: + return testutil.MustParse4("225.0.0.0") + case otherMulticastAddr: + return testutil.MustParse4("225.0.0.1") + case remoteUnicastAddr: + return utils.RemoteIPv4Addr + default: + panic(fmt.Sprintf("unsupported addrType: %d", addrType)) + } +} + +func getEndpointAddr(addrType endpointAddrType) tcpip.AddressWithPrefix { + switch addrType { + case incomingEndpointAddr: + return utils.RouterNIC1IPv4Addr.AddressWithPrefix + case otherEndpointAddr: + return utils.Host1IPv4Addr.AddressWithPrefix + case outgoingEndpointAddr: + return utils.RouterNIC2IPv4Addr.AddressWithPrefix + case otherOutgoingEndpointAddr: + return utils.Host2IPv4Addr.AddressWithPrefix + default: + panic(fmt.Sprintf("unsupported endpointAddrType: %d", addrType)) + } +} + +func TestAddMulticastRoute(t *testing.T) { + endpointConfigs := map[tcpip.NICID]endpointAddrType{ + incomingNICID: incomingEndpointAddr, + outgoingNICID: outgoingEndpointAddr, + otherNICID: otherEndpointAddr, + } + + tests := []struct { + name string + srcAddr, dstAddr addrType + routeIncomingNICID tcpip.NICID + routeOutgoingNICID tcpip.NICID + omitOutgoingInterfaces bool + injectPendingPacket bool + expectForward bool + wantErr tcpip.Error + }{ + { + name: "no pending packets", + srcAddr: remoteUnicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: nil, + }, + { + name: "pending packet forwarded", + srcAddr: remoteUnicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + injectPendingPacket: true, + expectForward: true, + }, + { + name: "unexpected input interface", + srcAddr: remoteUnicastAddr, + dstAddr: multicastAddr, + // The added route's incoming NICID does not match the pending packet's + // incoming NICID. As a result, the packet should not be forwarded. + routeIncomingNICID: otherNICID, + routeOutgoingNICID: outgoingNICID, + injectPendingPacket: true, + }, + { + name: "multicast source", + srcAddr: multicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrBadAddress{}, + }, + { + name: "any source", + srcAddr: anyAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrBadAddress{}, + }, + { + name: "link-local unicast source", + srcAddr: linkLocalUnicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrBadAddress{}, + }, + { + name: "empty source", + srcAddr: emptyAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrBadAddress{}, + }, + { + name: "unicast destination", + srcAddr: remoteUnicastAddr, + dstAddr: remoteUnicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrBadAddress{}, + }, + { + name: "empty destination", + srcAddr: remoteUnicastAddr, + dstAddr: emptyAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrBadAddress{}, + }, + { + name: "link-local multicast destination", + srcAddr: remoteUnicastAddr, + dstAddr: linkLocalMulticastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrBadAddress{}, + }, + { + name: "unknown input NICID", + srcAddr: remoteUnicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: unknownNICID, + routeOutgoingNICID: outgoingNICID, + wantErr: &tcpip.ErrUnknownNICID{}, + }, + { + name: "unknown output NICID", + srcAddr: remoteUnicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: unknownNICID, + wantErr: &tcpip.ErrUnknownNICID{}, + }, + { + name: "input NIC matches output NIC", + srcAddr: remoteUnicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: incomingNICID, + wantErr: &tcpip.ErrMulticastInputCannotBeOutput{}, + }, + { + name: "empty outgoing interfaces", + srcAddr: remoteUnicastAddr, + dstAddr: multicastAddr, + routeIncomingNICID: incomingNICID, + routeOutgoingNICID: outgoingNICID, + omitOutgoingInterfaces: true, + wantErr: &tcpip.ErrMissingRequiredFields{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol}, + }) + defer s.Close() + + endpoints := make(map[tcpip.NICID]*channel.Endpoint) + for nicID, addrType := range endpointConfigs { + ep := channel.New(1, ipv4.MaxTotalSize, "") + defer ep.Close() + + if err := s.CreateNIC(nicID, ep); err != nil { + t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err) + } + addr := tcpip.ProtocolAddress{ + Protocol: header.IPv4ProtocolNumber, + AddressWithPrefix: getEndpointAddr(addrType), + } + if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("s.AddProtocolAddress(%d, %#v, {}): %s", nicID, addr, err) + } + s.SetNICMulticastForwarding(nicID, ipv4.ProtocolNumber, true /* enabled */) + endpoints[nicID] = ep + } + + srcAddr := getAddr(test.srcAddr) + dstAddr := getAddr(test.dstAddr) + + if test.injectPendingPacket { + incomingEp, ok := endpoints[incomingNICID] + if !ok { + t.Fatalf("got endpoints[%d] = (_, false), want (_, true)", incomingNICID) + } + + utils.RxICMPv4EchoRequest(incomingEp, srcAddr, dstAddr, packetTTL) + p := incomingEp.Read() + + if p != nil { + // An ICMP error should never be sent in response to a multicast packet. + t.Fatalf("got incomingEp.Read() = %#v, want = nil", p) + } + } + + outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{ + {ID: test.routeOutgoingNICID, MinTTL: routeMinTTL}, + } + if test.omitOutgoingInterfaces { + outgoingInterfaces = nil + } + + addresses := stack.UnicastSourceAndMulticastDestination{ + Source: srcAddr, + Destination: dstAddr, + } + + route := stack.MulticastRoute{ + ExpectedInputInterface: test.routeIncomingNICID, + OutgoingInterfaces: outgoingInterfaces, + } + + err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route) + + if !cmp.Equal(err, test.wantErr, cmpopts.EquateErrors()) { + t.Errorf("got s.AddMulticastRoute(%d, %#v, %#v) = %s, want %s", ipv4.ProtocolNumber, addresses, route, err, test.wantErr) + } + + outgoingEp, ok := endpoints[outgoingNICID] + if !ok { + t.Fatalf("got endpoints[%d] = (_, false), want (_, true)", outgoingNICID) + } + + p := outgoingEp.Read() + + if (p != nil) != test.expectForward { + t.Fatalf("got outgoingEp.Read() = %#v, want = (_ == nil) = %t", p, test.expectForward) + } + + if test.expectForward { + checker.IPv4(t, stack.PayloadSince(p.NetworkHeader()), + checker.SrcAddr(srcAddr), + checker.DstAddr(dstAddr), + checker.TTL(packetTTL-1), + checker.ICMPv4( + checker.ICMPv4Type(header.ICMPv4Echo), + ), + ) + p.DecRef() + } + }) + } +} + +func TestMulticastForwarding(t *testing.T) { + endpointConfigs := map[tcpip.NICID]endpointAddrType{ + incomingNICID: incomingEndpointAddr, + outgoingNICID: outgoingEndpointAddr, + otherOutgoingNICID: otherOutgoingEndpointAddr, + otherNICID: otherEndpointAddr, + } + + contains := func(want tcpip.NICID, items []tcpip.NICID) bool { + for _, item := range items { + if want == item { + return true + } + } + return false + } + + tests := []struct { + name string + dstAddr addrType + ttl uint8 + routeInputInterface tcpip.NICID + disableMulticastForwarding bool + removeOutputInterface tcpip.NICID + joinMulticastGroup bool + expectedForwardingInterfaces []tcpip.NICID + }{ + { + name: "forward only", + dstAddr: multicastAddr, + ttl: packetTTL, + routeInputInterface: incomingNICID, + expectedForwardingInterfaces: []tcpip.NICID{outgoingNICID, otherOutgoingNICID}, + }, + { + name: "forward and local", + dstAddr: multicastAddr, + ttl: packetTTL, + routeInputInterface: incomingNICID, + joinMulticastGroup: true, + expectedForwardingInterfaces: []tcpip.NICID{outgoingNICID, otherOutgoingNICID}, + }, + { + name: "local only", + dstAddr: linkLocalMulticastAddr, + ttl: packetTTL, + routeInputInterface: incomingNICID, + joinMulticastGroup: true, + expectedForwardingInterfaces: []tcpip.NICID{}, + }, + { + name: "multicast forwarding disabled", + disableMulticastForwarding: true, + dstAddr: multicastAddr, + ttl: packetTTL, + routeInputInterface: incomingNICID, + expectedForwardingInterfaces: []tcpip.NICID{}, + }, + { + name: "unexpected input interface", + dstAddr: multicastAddr, + ttl: packetTTL, + routeInputInterface: otherNICID, + expectedForwardingInterfaces: []tcpip.NICID{}, + }, + { + name: "output interface removed", + dstAddr: multicastAddr, + ttl: packetTTL, + routeInputInterface: incomingNICID, + removeOutputInterface: outgoingNICID, + expectedForwardingInterfaces: []tcpip.NICID{otherOutgoingNICID}, + }, + { + name: "ttl greater than outgoingNICID route min", + dstAddr: multicastAddr, + ttl: routeMinTTL + 1, + routeInputInterface: incomingNICID, + expectedForwardingInterfaces: []tcpip.NICID{outgoingNICID, otherOutgoingNICID}, + }, + { + name: "ttl same as outgoingNICID route min", + dstAddr: multicastAddr, + ttl: routeMinTTL, + routeInputInterface: incomingNICID, + expectedForwardingInterfaces: []tcpip.NICID{outgoingNICID}, + }, + { + name: "ttl less than outgoingNICID route min", + dstAddr: multicastAddr, + ttl: routeMinTTL - 1, + routeInputInterface: incomingNICID, + expectedForwardingInterfaces: []tcpip.NICID{}, + }, + { + name: "no matching route", + dstAddr: otherMulticastAddr, + ttl: packetTTL, + routeInputInterface: incomingNICID, + expectedForwardingInterfaces: []tcpip.NICID{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol}, + }) + defer s.Close() + + endpoints := make(map[tcpip.NICID]*channel.Endpoint) + for nicID, addrType := range endpointConfigs { + ep := channel.New(1, ipv4.MaxTotalSize, "") + defer ep.Close() + + if err := s.CreateNIC(nicID, ep); err != nil { + t.Fatalf("s.CreateNIC(%d, _): %s", nicID, err) + } + addr := tcpip.ProtocolAddress{ + Protocol: ipv4.ProtocolNumber, + AddressWithPrefix: getEndpointAddr(addrType), + } + if err := s.AddProtocolAddress(nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("s.AddProtocolAddress(%d, %+v, {}): %s", nicID, addr, err) + } + + s.SetNICMulticastForwarding(nicID, ipv4.ProtocolNumber, !test.disableMulticastForwarding) + endpoints[nicID] = ep + } + + if err := s.SetForwardingDefaultAndAllNICs(ipv4.ProtocolNumber, true /* enabled */); err != nil { + t.Fatalf("SetForwardingDefaultAndAllNICs(%d, true): %s", ipv4.ProtocolNumber, err) + } + + srcAddr := getAddr(remoteUnicastAddr) + dstAddr := getAddr(test.dstAddr) + + outgoingInterfaces := []stack.MulticastRouteOutgoingInterface{ + {ID: outgoingNICID, MinTTL: routeMinTTL}, + {ID: otherOutgoingNICID, MinTTL: routeMinTTL + 1}, + } + addresses := stack.UnicastSourceAndMulticastDestination{ + Source: srcAddr, + Destination: getAddr(multicastAddr), + } + + route := stack.MulticastRoute{ + ExpectedInputInterface: test.routeInputInterface, + OutgoingInterfaces: outgoingInterfaces, + } + + if err := s.AddMulticastRoute(ipv4.ProtocolNumber, addresses, route); err != nil { + t.Fatalf("AddMulticastRoute(%d, %#v, %#v): %s", ipv4.ProtocolNumber, addresses, route, err) + } + + if test.removeOutputInterface != 0 { + if err := s.RemoveNIC(test.removeOutputInterface); err != nil { + t.Fatalf("RemoveNIC(%d): %s", test.removeOutputInterface, err) + } + } + + // Add a route that can be used to send an ICMP echo reply (if the packet + // is delivered locally). + s.SetRouteTable([]tcpip.Route{ + { + Destination: header.IPv4EmptySubnet, + NIC: otherNICID, + }, + }) + + if test.joinMulticastGroup { + if err := s.JoinGroup(ipv4.ProtocolNumber, incomingNICID, dstAddr); err != nil { + t.Fatalf("JoinGroup(%d, %d, %s): %s", ipv4.ProtocolNumber, incomingNICID, dstAddr, err) + } + } + + incomingEp, ok := endpoints[incomingNICID] + if !ok { + t.Fatalf("got endpoints[%d] = (_, false), want (_, true)", incomingNICID) + } + + utils.RxICMPv4EchoRequest(incomingEp, srcAddr, dstAddr, test.ttl) + p := incomingEp.Read() + + if p != nil { + // An ICMP error should never be sent in response to a multicast packet. + t.Fatalf("expected no ICMP packet through incoming NIC, instead found: %#v", p) + } + + for _, nicID := range []tcpip.NICID{outgoingNICID, otherOutgoingNICID} { + outgoingEp, ok := endpoints[nicID] + if !ok { + t.Fatalf("got endpoints[%d] = (_, false), want (_, true)", nicID) + } + + p := outgoingEp.Read() + + expectForward := contains(nicID, test.expectedForwardingInterfaces) + + if (p != nil) != expectForward { + t.Fatalf("got outgoingEp.Read() = %#v, want = (_ == nil) = %t", p, expectForward) + } + + if expectForward { + checker.IPv4(t, stack.PayloadSince(p.NetworkHeader()), + checker.SrcAddr(srcAddr), + checker.DstAddr(dstAddr), + checker.TTL(test.ttl-1), + checker.ICMPv4( + checker.ICMPv4Type(header.ICMPv4Echo), + ), + ) + p.DecRef() + } + } + + otherEp, ok := endpoints[otherNICID] + if !ok { + t.Fatalf("got endpoints[%d] = (_, false), want (_, true)", otherNICID) + } + + p = otherEp.Read() + + if (p != nil) != test.joinMulticastGroup { + t.Fatalf("got otherEp.Read() = %#v, want = (_ == nil) = %t", p, test.joinMulticastGroup) + } + + incomingEpAddrType, ok := endpointConfigs[incomingNICID] + if !ok { + t.Fatalf("got endpointConfigs[%d] = (_, false), want (_, true)", incomingNICID) + } + + if test.joinMulticastGroup { + checker.IPv4(t, stack.PayloadSince(p.NetworkHeader()), + checker.SrcAddr(getEndpointAddr(incomingEpAddrType).Address), + checker.DstAddr(srcAddr), + checker.ICMPv4( + checker.ICMPv4Type(header.ICMPv4EchoReply), + ), + ) + p.DecRef() + } + }) + } +} + +func TestMain(m *testing.M) { + refs.SetLeakMode(refs.LeaksPanic) + code := m.Run() + refsvfs2.DoLeakCheck() + os.Exit(code) +}