Skip to content

Commit 13a53d0

Browse files
authored
Merge pull request #4852 from oshoval/poc
UDN: Filter RA according local GW Router LLA only to avoid multi path
2 parents 2c7ba78 + a091f10 commit 13a53d0

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed

go-controller/pkg/cni/helper_linux.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/vishvananda/netlink"
2121

2222
"k8s.io/klog/v2"
23+
"sigs.k8s.io/knftables"
2324

2425
"github.com/ovn-org/ovn-kubernetes/go-controller/pkg/types"
2526
"github.com/ovn-org/ovn-kubernetes/go-controller/pkg/util"
@@ -252,6 +253,13 @@ func setupInterface(netns ns.NetNS, containerID, ifName string, ifInfo *PodInter
252253
ifnameSuffix = fmt.Sprintf("_%d", containerVeth.Index)
253254
}
254255

256+
// If we have the ipv6 gateway LLA then this is a primary layer2 UDN
257+
if len(ifInfo.GatewayIPv6LLA) > 0 {
258+
if err = setupIngressFilter(ifName, ifInfo.GatewayIPv6LLA.String()); err != nil {
259+
return err
260+
}
261+
}
262+
255263
return nil
256264
})
257265
if err != nil {
@@ -597,6 +605,7 @@ func (*defaultPodRequestInterfaceOps) ConfigureInterface(pr *PodRequest, getter
597605
return nil, fmt.Errorf("unexpected configuration, pod request on dpu host. " +
598606
"device ID must be provided")
599607
}
608+
600609
// General case
601610
hostIface, contIface, err = setupInterface(netns, pr.SandboxID, pr.IfName, ifInfo)
602611
}
@@ -818,3 +827,68 @@ func (pr *PodRequest) deletePorts(ifaces []string, podNamespace, podName string)
818827
pr.deletePort(iface, podNamespace, podName)
819828
}
820829
}
830+
831+
// setupIngressFilter sets up an ingress filter using nftables to block
832+
// unwanted ICMPv6 Router Advertisement (RA) packets on a specific device.
833+
// It creates a new nftables table, chain, and rule to drop RA packets
834+
// that do not match the specified gateway link-local address (gwLLA) and
835+
// have a Router Advertisement (RA) lifetime not equal to 0.
836+
//
837+
// The nftables rule created by this function looks like:
838+
// `icmpv6 type nd-router-advert ip6 saddr != <gwLLA> @th,48,16 != 0 drop`
839+
//
840+
// Parameters:
841+
// - ifaceName: The name of the network interface where the ingress filter
842+
// should be applied.
843+
// - gwLLA: The gateway link-local address to match against in the RA packets.
844+
//
845+
// Returns:
846+
// - error: An error if the nftables setup fails, otherwise nil.
847+
func setupIngressFilter(ifaceName, gwLLA string) error {
848+
const (
849+
tableName = "ingress_filter" // Name of the nftables table to be created
850+
rasLifetime = "@th,48,16" // Offset for RA lifetime field in ICMPv6 packets
851+
)
852+
853+
// Initialize a new nftables table for the ingress filter
854+
nft, err := knftables.New(knftables.NetDevFamily, tableName)
855+
if err != nil {
856+
return fmt.Errorf("failed to initialize table: %w", err)
857+
}
858+
859+
// Delegate the setup of the filter with the specified table and lifetime matcher
860+
return setupIngressFilterWithTableAndLifetimeMatcher(nft, ifaceName, gwLLA, rasLifetime)
861+
}
862+
863+
func setupIngressFilterWithTableAndLifetimeMatcher(nft knftables.Interface, ifaceName, gwLLA, rasLifetime string) error {
864+
const (
865+
chainName = "input"
866+
)
867+
868+
tx := nft.NewTransaction()
869+
870+
tx.Add(&knftables.Table{})
871+
872+
tx.Add(&knftables.Chain{
873+
Name: chainName,
874+
Type: knftables.PtrTo(knftables.FilterType),
875+
Hook: knftables.PtrTo(knftables.IngressHook),
876+
Priority: knftables.PtrTo(knftables.FilterPriority),
877+
Device: knftables.PtrTo(ifaceName),
878+
})
879+
880+
tx.Add(&knftables.Rule{
881+
Chain: chainName,
882+
Rule: knftables.Concat(
883+
"icmpv6", "type", "nd-router-advert", "ip6", "saddr", "!=", gwLLA, rasLifetime, "!=", "0", "drop",
884+
),
885+
})
886+
887+
// Execute the transaction
888+
if err := nft.Run(context.Background(), tx); err != nil {
889+
return fmt.Errorf("could not update netdev nftables: %v", err)
890+
}
891+
892+
return nil
893+
894+
}

go-controller/pkg/cni/helper_linux_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
corev1 "k8s.io/api/core/v1"
2222
"k8s.io/client-go/kubernetes/fake"
2323
kexec "k8s.io/utils/exec"
24+
"sigs.k8s.io/knftables"
2425

2526
"github.com/ovn-org/ovn-kubernetes/go-controller/pkg/cni/mocks"
2627
"github.com/ovn-org/ovn-kubernetes/go-controller/pkg/cni/types"
@@ -1651,6 +1652,21 @@ func TestConfigureOVS_getPfEncapIpWithError(t *testing.T) {
16511652
}
16521653
}
16531654

1655+
func TestSetupIngressFilter(t *testing.T) {
1656+
nft := knftables.NewFake(knftables.NetDevFamily, "ingress_filter")
1657+
1658+
gwLLA := "fe80::1"
1659+
err := setupIngressFilterWithTableAndLifetimeMatcher(nft, "test-iface-1", gwLLA, "mark")
1660+
require.NoError(t, err)
1661+
output := nft.Dump()
1662+
1663+
expectedDump := `add table netdev ingress_filter
1664+
add chain netdev ingress_filter input { type filter hook ingress device "test-iface-1" priority 0 ; }
1665+
add rule netdev ingress_filter input icmpv6 type nd-router-advert ip6 saddr != fe80::1 mark != 0 drop`
1666+
1667+
assert.Equal(t, expectedDump, strings.TrimRight(output, "\n"), "The nftables dump output does not match the expected output")
1668+
}
1669+
16541670
// TODO(leih): Below functions are copied from pkg/node/base_node_network_controller_dpu_test.go.
16551671
// Move them to a common place to elimate duplications.
16561672
func genOVSFindCmd(timeout, table, column, condition string) string {

test/e2e/kubevirt.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,20 @@ runcmd:
16831683
nodeIPs := e2enode.CollectAddresses(nodes, v1.NodeInternalIP)
16841684

16851685
if td.role == "primary" {
1686+
if isIPv6Supported() && isInterconnectEnabled() {
1687+
step = by(vmi.Name, fmt.Sprintf("Checking IPv6 gateway before %s %s", td.resource.description, td.test.description))
1688+
1689+
nodeRunningVMI, err := fr.ClientSet.CoreV1().Nodes().Get(context.Background(), vmi.Status.NodeName, metav1.GetOptions{})
1690+
Expect(err).NotTo(HaveOccurred(), step)
1691+
1692+
expectedIPv6GatewayPath, err := kubevirt.GenerateGatewayIPv6RouterLLA(nodeRunningVMI, netConfig.networkName)
1693+
Expect(err).NotTo(HaveOccurred())
1694+
Eventually(kubevirt.RetrieveIPv6Gateways).
1695+
WithArguments(vmi).
1696+
WithTimeout(5*time.Second).
1697+
WithPolling(time.Second).
1698+
Should(Equal([]string{expectedIPv6GatewayPath}), "should filter remote ipv6 gateway nexthop")
1699+
}
16861700
step = by(vmi.Name, fmt.Sprintf("Check north/south traffic before %s %s", td.resource.description, td.test.description))
16871701
startNorthSouthIngressIperfTraffic(externalContainerName, nodeIPs, svc.Spec.Ports[0].NodePort, step)
16881702
checkNorthSouthIngressIperfTraffic(externalContainerName, nodeIPs, svc.Spec.Ports[0].NodePort, step)

0 commit comments

Comments
 (0)