diff --git a/pkg/tcpip/network/ipv4/ipv4.go b/pkg/tcpip/network/ipv4/ipv4.go index c176350b4..75cc8f135 100644 --- a/pkg/tcpip/network/ipv4/ipv4.go +++ b/pkg/tcpip/network/ipv4/ipv4.go @@ -367,6 +367,18 @@ func (e *endpoint) disableLocked() { } } +// multicastEventDispatcher returns the multicast forwarding event dispatcher. +// +// Panics if a multicast forwarding event dispatcher does not exist. This +// indicates that multicast forwarding is enabled, but no dispatcher was +// provided. +func (e *endpoint) multicastEventDispatcher() stack.MulticastForwardingEventDispatcher { + if mcastDisp := e.protocol.options.MulticastForwardingDisp; mcastDisp != nil { + return mcastDisp + } + panic("e.procotol.options.MulticastForwardingDisp unexpectedly nil") +} + // DefaultTTL is the default time-to-live value for this endpoint. func (e *endpoint) DefaultTTL() uint8 { return e.protocol.DefaultTTL() @@ -886,10 +898,18 @@ func (e *endpoint) forwardMulticastPacket(h header.IPv4, pkt *stack.PacketBuffer return &ip.ErrNoMulticastPendingQueueBufferSpace{} } - // TODO(https://gvisor.dev/issue/7338): Emit an event for a missing route. - if result.GetRouteResultState == multicast.InstalledRouteFound { + switch result.GetRouteResultState { + case multicast.InstalledRouteFound: // Attempt to forward the pkt using an existing route. return e.forwardValidatedMulticastPacket(pkt, result.InstalledRoute) + case multicast.NoRouteFoundAndPendingInserted: + e.multicastEventDispatcher().OnMissingRoute(stack.MulticastPacketContext{ + stack.UnicastSourceAndMulticastDestination{h.SourceAddress(), h.DestinationAddress()}, + e.nic.ID(), + }) + case multicast.PacketQueuedInPendingRoute: + default: + panic(fmt.Sprintf("unexpected GetRouteResultState: %s", result.GetRouteResultState)) } return &ip.ErrNoRoute{} } @@ -937,8 +957,11 @@ func (e *endpoint) forwardValidatedMulticastPacket(pkt *stack.PacketBuffer, inst // 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. + h := header.IPv4(pkt.NetworkHeader().View()) + e.multicastEventDispatcher().OnUnexpectedInputInterface(stack.MulticastPacketContext{ + stack.UnicastSourceAndMulticastDestination{h.SourceAddress(), h.DestinationAddress()}, + e.nic.ID(), + }, installedRoute.ExpectedInputInterface) return &ip.ErrUnexpectedMulticastInputInterface{} } @@ -1749,6 +1772,10 @@ type Options struct { // AllowExternalLoopbackTraffic indicates that inbound loopback packets (i.e. // martian loopback packets) should be accepted. AllowExternalLoopbackTraffic bool + + // MulticastForwardingDisp is the multicast forwarding event dispatcher that + // an integrator can provide to receive multicast forwarding events. + MulticastForwardingDisp stack.MulticastForwardingEventDispatcher } // NewProtocolWithOptions returns an IPv4 network protocol. diff --git a/pkg/tcpip/network/ipv6/icmp_test.go b/pkg/tcpip/network/ipv6/icmp_test.go index ca815a8bc..58c359d3c 100644 --- a/pkg/tcpip/network/ipv6/icmp_test.go +++ b/pkg/tcpip/network/ipv6/icmp_test.go @@ -199,6 +199,15 @@ func handleICMPInIPv6(ep stack.NetworkEndpoint, src, dst tcpip.Address, icmp hea pkt.DecRef() } +var _ stack.MulticastForwardingEventDispatcher = (*fakeMulticastEventDispatcher)(nil) + +type fakeMulticastEventDispatcher struct{} + +func (m *fakeMulticastEventDispatcher) OnMissingRoute(context stack.MulticastPacketContext) {} + +func (m *fakeMulticastEventDispatcher) OnUnexpectedInputInterface(context stack.MulticastPacketContext, expectedInputInterface tcpip.NICID) { +} + type testContext struct { s *stack.Stack clock *faketime.ManualClock @@ -207,7 +216,7 @@ type testContext struct { func newTestContext() testContext { clock := faketime.NewManualClock() s := stack.New(stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{NewProtocol}, + NetworkProtocols: []stack.NetworkProtocolFactory{NewProtocolWithOptions(Options{MulticastForwardingDisp: &fakeMulticastEventDispatcher{}})}, TransportProtocols: []stack.TransportProtocolFactory{icmp.NewProtocol6, udp.NewProtocol}, Clock: clock, }) diff --git a/pkg/tcpip/network/ipv6/ipv6.go b/pkg/tcpip/network/ipv6/ipv6.go index 7a3cf35dc..7d8e739ef 100644 --- a/pkg/tcpip/network/ipv6/ipv6.go +++ b/pkg/tcpip/network/ipv6/ipv6.go @@ -1123,15 +1123,18 @@ func (e *endpoint) forwardMulticastPacket(h header.IPv6, pkt *stack.PacketBuffer return &ip.ErrNoMulticastPendingQueueBufferSpace{} } - // TODO(https://gvisor.dev/issue/7338): Emit an event for a missing route. switch result.GetRouteResultState { case multicast.InstalledRouteFound: // Attempt to forward the pkt using an existing route. return e.forwardValidatedMulticastPacket(pkt, result.InstalledRoute) case multicast.NoRouteFoundAndPendingInserted: + e.multicastEventDispatcher().OnMissingRoute(stack.MulticastPacketContext{ + stack.UnicastSourceAndMulticastDestination{h.SourceAddress(), h.DestinationAddress()}, + e.nic.ID(), + }) case multicast.PacketQueuedInPendingRoute: default: - panic(fmt.Sprintf("unexpected result.GetRouteResultState: %s", result.GetRouteResultState)) + panic(fmt.Sprintf("unexpected GetRouteResultState: %s", result.GetRouteResultState)) } return &ip.ErrNoRoute{} } @@ -1148,8 +1151,11 @@ func (e *endpoint) forwardValidatedMulticastPacket(pkt *stack.PacketBuffer, inst // 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. + h := header.IPv6(pkt.NetworkHeader().View()) + e.multicastEventDispatcher().OnUnexpectedInputInterface(stack.MulticastPacketContext{ + stack.UnicastSourceAndMulticastDestination{h.SourceAddress(), h.DestinationAddress()}, + e.nic.ID(), + }, installedRoute.ExpectedInputInterface) return &ip.ErrUnexpectedMulticastInputInterface{} } @@ -2264,6 +2270,18 @@ func (p *protocol) DefaultTTL() uint8 { return uint8(p.defaultTTL.Load()) } +// multicastEventDispatcher returns the multicast forwarding event dispatcher. +// +// Panics if a multicast forwarding event dispatcher does not exist. This +// indicates that multicast forwarding is enabled, but no dispatcher was +// provided. +func (e *endpoint) multicastEventDispatcher() stack.MulticastForwardingEventDispatcher { + if mcastDisp := e.protocol.options.MulticastForwardingDisp; mcastDisp != nil { + return mcastDisp + } + panic("e.procotol.options.MulticastForwardingDisp unexpectedly nil") +} + // Close implements stack.TransportProtocol. func (p *protocol) Close() { p.fragmentation.Release() @@ -2528,6 +2546,10 @@ type Options struct { // AllowExternalLoopbackTraffic indicates that inbound loopback packets (i.e. // martian loopback packets) should be accepted. AllowExternalLoopbackTraffic bool + + // MulticastForwardingDisp is the multicast forwarding event dispatcher that + // an integrator can provide to receive multicast forwarding events. + MulticastForwardingDisp stack.MulticastForwardingEventDispatcher } // NewProtocolWithOptions returns an IPv6 network protocol. diff --git a/pkg/tcpip/stack/registration.go b/pkg/tcpip/stack/registration.go index aed92f582..f3b22f206 100644 --- a/pkg/tcpip/stack/registration.go +++ b/pkg/tcpip/stack/registration.go @@ -819,6 +819,37 @@ type MulticastForwardingNetworkProtocol interface { MulticastRouteLastUsedTime(UnicastSourceAndMulticastDestination) (tcpip.MonotonicTime, tcpip.Error) } +// MulticastPacketContext is the context in which a multicast packet triggered +// a multicast forwarding event. +type MulticastPacketContext struct { + // SourceAndDestination contains the unicast source address and the multicast + // destination address found in the relevant multicast packet. + SourceAndDestination UnicastSourceAndMulticastDestination + // InputInterface is the interface on which the relevant multicast packet + // arrived. + InputInterface tcpip.NICID +} + +// MulticastForwardingEventDispatcher is the interface that integrators should +// implement to handle multicast routing events. +type MulticastForwardingEventDispatcher interface { + // OnMissingRoute is called when an incoming multicast packet does not match + // any installed route. + // + // The packet that triggered this event may be queued so that it can be + // transmitted once a route is installed. Even then, it may still be dropped + // as per the routing table's GC/eviction policy. + OnMissingRoute(MulticastPacketContext) + + // OnUnexpectedInputInterface is called when a multicast packet arrives at an + // interface that does not match the installed route's expected input + // interface. + // + // This may be an indication of a routing loop. The packet that triggered + // this event is dropped without being forwarded. + OnUnexpectedInputInterface(context MulticastPacketContext, expectedInputInterface tcpip.NICID) +} + // 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/tests/integration/multicast_forward_test.go b/pkg/tcpip/tests/integration/multicast_forward_test.go index c7260f266..eb6edf7d0 100644 --- a/pkg/tcpip/tests/integration/multicast_forward_test.go +++ b/pkg/tcpip/tests/integration/multicast_forward_test.go @@ -68,6 +68,33 @@ const ( otherOutgoingEndpointAddr ) +type onMissingRouteData struct { + context stack.MulticastPacketContext +} + +type onUnexpectedInputInterfaceData struct { + context stack.MulticastPacketContext + expectedInputInterface tcpip.NICID +} + +var _ stack.MulticastForwardingEventDispatcher = (*fakeMulticastEventDispatcher)(nil) + +type fakeMulticastEventDispatcher struct { + onMissingRouteData *onMissingRouteData + onUnexpectedInputInterfaceData *onUnexpectedInputInterfaceData +} + +func (m *fakeMulticastEventDispatcher) OnMissingRoute(context stack.MulticastPacketContext) { + m.onMissingRouteData = &onMissingRouteData{context} +} + +func (m *fakeMulticastEventDispatcher) OnUnexpectedInputInterface(context stack.MulticastPacketContext, expectedInputInterface tcpip.NICID) { + m.onUnexpectedInputInterfaceData = &onUnexpectedInputInterfaceData{ + context, + expectedInputInterface, + } +} + var ( v4Addrs = map[addrType]tcpip.Address{ anyAddr: header.IPv4Any, @@ -335,9 +362,12 @@ func TestAddMulticastRoute(t *testing.T) { for _, test := range tests { for _, protocol := range []tcpip.NetworkProtocolNumber{ipv4.ProtocolNumber, ipv6.ProtocolNumber} { t.Run(fmt.Sprintf("%s %d", test.name, protocol), func(t *testing.T) { + eventDispatcher := &fakeMulticastEventDispatcher{} s := stack.New(stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, - TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol}, + NetworkProtocols: []stack.NetworkProtocolFactory{ + ipv4.NewProtocolWithOptions(ipv4.Options{MulticastForwardingDisp: eventDispatcher}), + ipv6.NewProtocolWithOptions(ipv6.Options{MulticastForwardingDisp: eventDispatcher}), + }, }) defer s.Close() @@ -646,8 +676,12 @@ func TestRemoveMulticastRoute(t *testing.T) { for _, test := range tests { for _, protocol := range []tcpip.NetworkProtocolNumber{ipv4.ProtocolNumber, ipv6.ProtocolNumber} { t.Run(fmt.Sprintf("%s %d", test.name, protocol), func(t *testing.T) { + eventDispatcher := &fakeMulticastEventDispatcher{} s := stack.New(stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + NetworkProtocols: []stack.NetworkProtocolFactory{ + ipv4.NewProtocolWithOptions(ipv4.Options{MulticastForwardingDisp: eventDispatcher}), + ipv6.NewProtocolWithOptions(ipv6.Options{MulticastForwardingDisp: eventDispatcher}), + }, TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol}, }) defer s.Close() @@ -757,14 +791,16 @@ func TestMulticastForwarding(t *testing.T) { } tests := []struct { - name string - dstAddr addrType - ttl uint8 - routeInputInterface tcpip.NICID - disableMulticastForwarding bool - removeOutputInterface tcpip.NICID - joinMulticastGroup bool - expectedForwardingInterfaces []tcpip.NICID + name string + dstAddr addrType + ttl uint8 + routeInputInterface tcpip.NICID + disableMulticastForwarding bool + removeOutputInterface tcpip.NICID + expectMissingRouteEvent bool + expectUnexpectedInputInterfaceEvent bool + joinMulticastGroup bool + expectedForwardingInterfaces []tcpip.NICID }{ { name: "forward only", @@ -798,11 +834,12 @@ func TestMulticastForwarding(t *testing.T) { expectedForwardingInterfaces: []tcpip.NICID{}, }, { - name: "unexpected input interface", - dstAddr: multicastAddr, - ttl: packetTTL, - routeInputInterface: otherNICID, - expectedForwardingInterfaces: []tcpip.NICID{}, + name: "unexpected input interface", + dstAddr: multicastAddr, + ttl: packetTTL, + routeInputInterface: otherNICID, + expectUnexpectedInputInterfaceEvent: true, + expectedForwardingInterfaces: []tcpip.NICID{}, }, { name: "output interface removed", @@ -838,15 +875,27 @@ func TestMulticastForwarding(t *testing.T) { dstAddr: otherMulticastAddr, ttl: packetTTL, routeInputInterface: incomingNICID, + expectMissingRouteEvent: true, expectedForwardingInterfaces: []tcpip.NICID{}, }, } for _, test := range tests { for _, protocol := range []tcpip.NetworkProtocolNumber{ipv4.ProtocolNumber, ipv6.ProtocolNumber} { + ipv4EventDispatcher := &fakeMulticastEventDispatcher{} + ipv6EventDispatcher := &fakeMulticastEventDispatcher{} + + eventDispatchers := map[tcpip.NetworkProtocolNumber]*fakeMulticastEventDispatcher{ + ipv4.ProtocolNumber: ipv4EventDispatcher, + ipv6.ProtocolNumber: ipv6EventDispatcher, + } + t.Run(fmt.Sprintf("%s %d", test.name, protocol), func(t *testing.T) { s := stack.New(stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + NetworkProtocols: []stack.NetworkProtocolFactory{ + ipv4.NewProtocolWithOptions(ipv4.Options{MulticastForwardingDisp: ipv4EventDispatcher}), + ipv6.NewProtocolWithOptions(ipv6.Options{MulticastForwardingDisp: ipv6EventDispatcher}), + }, TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol}, }) defer s.Close() @@ -974,6 +1023,33 @@ func TestMulticastForwarding(t *testing.T) { checkEchoReply(t, protocol, p, getEndpointAddr(protocol, incomingEpAddrType).Address, srcAddr) p.DecRef() } + + eventDispatcher, ok := eventDispatchers[protocol] + if !ok { + t.Fatalf("eventDispatchers[%d] = (_, false), want (_, true)", protocol) + } + + wantUnexpectedInputInterfaceEvent := func() *onUnexpectedInputInterfaceData { + if test.expectUnexpectedInputInterfaceEvent { + return &onUnexpectedInputInterfaceData{stack.MulticastPacketContext{stack.UnicastSourceAndMulticastDestination{srcAddr, dstAddr}, incomingNICID}, test.routeInputInterface} + } + return nil + }() + + if diff := cmp.Diff(wantUnexpectedInputInterfaceEvent, eventDispatcher.onUnexpectedInputInterfaceData, cmp.AllowUnexported(onUnexpectedInputInterfaceData{})); diff != "" { + t.Errorf("onUnexpectedInputInterfaceData mismatch (-want +got):\n%s", diff) + } + + wantMissingRouteEvent := func() *onMissingRouteData { + if test.expectMissingRouteEvent { + return &onMissingRouteData{stack.MulticastPacketContext{stack.UnicastSourceAndMulticastDestination{srcAddr, dstAddr}, incomingNICID}} + } + return nil + }() + + if diff := cmp.Diff(wantMissingRouteEvent, eventDispatcher.onMissingRouteData, cmp.AllowUnexported(onMissingRouteData{})); diff != "" { + t.Errorf("onMissingRouteData mismatch (-want +got):\n%s", diff) + } }) } }