]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
ipv6: add `force_forwarding` sysctl to enable per-interface forwarding
authorGabriel Goller <g.goller@proxmox.com>
Tue, 22 Jul 2025 08:18:45 +0000 (10:18 +0200)
committerJakub Kicinski <kuba@kernel.org>
Fri, 25 Jul 2025 20:06:19 +0000 (13:06 -0700)
It is currently impossible to enable ipv6 forwarding on a per-interface
basis like in ipv4. To enable forwarding on an ipv6 interface we need to
enable it on all interfaces and disable it on the other interfaces using
a netfilter rule. This is especially cumbersome if you have lots of
interfaces and only want to enable forwarding on a few. According to the
sysctl docs [0] the `net.ipv6.conf.all.forwarding` enables forwarding
for all interfaces, while the interface-specific
`net.ipv6.conf.<interface>.forwarding` configures the interface
Host/Router configuration.

Introduce a new sysctl flag `force_forwarding`, which can be set on every
interface. The ip6_forwarding function will then check if the global
forwarding flag OR the force_forwarding flag is active and forward the
packet.

To preserve backwards-compatibility reset the flag (on all interfaces)
to 0 if the net.ipv6.conf.all.forwarding flag is set to 0.

Add a short selftest that checks if a packet gets forwarded with and
without `force_forwarding`.

[0]: https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

Acked-by: Nicolas Dichtel <nicolas.dichtel@6wind.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Link: https://patch.msgid.link/20250722081847.132632-1-g.goller@proxmox.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Documentation/networking/ip-sysctl.rst
include/linux/ipv6.h
include/uapi/linux/ipv6.h
include/uapi/linux/netconf.h
include/uapi/linux/sysctl.h
net/ipv6/addrconf.c
net/ipv6/ip6_output.c
tools/testing/selftests/net/Makefile
tools/testing/selftests/net/ipv6_force_forwarding.sh [new file with mode: 0755]

index 14700ea77e75c56c4b91b38b7679ad37c3230674..bb620f554598b5d10361cbe14fa9dbb68cdbef90 100644 (file)
@@ -2543,8 +2543,8 @@ conf/all/disable_ipv6 - BOOLEAN
 conf/all/forwarding - BOOLEAN
        Enable global IPv6 forwarding between all interfaces.
 
-       IPv4 and IPv6 work differently here; e.g. netfilter must be used
-       to control which interfaces may forward packets and which not.
+       IPv4 and IPv6 work differently here; the ``force_forwarding`` flag must
+       be used to control which interfaces may forward packets.
 
        This also sets all interfaces' Host/Router setting
        'forwarding' to the specified value.  See below for details.
@@ -2561,6 +2561,10 @@ proxy_ndp - BOOLEAN
 
        Default: 0 (disabled)
 
+force_forwarding - BOOLEAN
+       Enable forwarding on this interface only -- regardless of the setting on
+       ``conf/all/forwarding``. When setting ``conf.all.forwarding`` to 0,
+       the ``force_forwarding`` flag will be reset on all interfaces.
 
 fwmark_reflect - BOOLEAN
        Controls the fwmark of kernel-generated IPv6 reply packets that are not
index db0eb0d86b641cafa7d8f87060e900f379c422d4..bc6ec295917321b38489efb4a82897ad02ee9b52 100644 (file)
@@ -17,6 +17,7 @@ struct ipv6_devconf {
        __s32           hop_limit;
        __s32           mtu6;
        __s32           forwarding;
+       __s32           force_forwarding;
        __s32           disable_policy;
        __s32           proxy_ndp;
        __cacheline_group_end(ipv6_devconf_read_txrx);
index cf592d7b630fe65d51fe32997e3365ad898a2f42..d4d3ae774b269237bce020d51f526bcd97c0a8f1 100644 (file)
@@ -199,6 +199,7 @@ enum {
        DEVCONF_NDISC_EVICT_NOCARRIER,
        DEVCONF_ACCEPT_UNTRACKED_NA,
        DEVCONF_ACCEPT_RA_MIN_LFT,
+       DEVCONF_FORCE_FORWARDING,
        DEVCONF_MAX
 };
 
index fac4edd553798cd87a24278bd4683632cd562f7f..1c8c84d65ae37de163ed09ffec52e9dc2e52e31f 100644 (file)
@@ -19,6 +19,7 @@ enum {
        NETCONFA_IGNORE_ROUTES_WITH_LINKDOWN,
        NETCONFA_INPUT,
        NETCONFA_BC_FORWARDING,
+       NETCONFA_FORCE_FORWARDING,
        __NETCONFA_MAX
 };
 #define NETCONFA_MAX   (__NETCONFA_MAX - 1)
index 8981f00204db55fc1fbc859b4952633f969dea87..63d1464cb71c86d728b402cb9df47b96d0209880 100644 (file)
@@ -573,6 +573,7 @@ enum {
        NET_IPV6_ACCEPT_RA_FROM_LOCAL=26,
        NET_IPV6_ACCEPT_RA_RT_INFO_MIN_PLEN=27,
        NET_IPV6_RA_DEFRTR_METRIC=28,
+       NET_IPV6_FORCE_FORWARDING=29,
        __NET_IPV6_MAX
 };
 
index 4f1d7d110302afa9606a78b6b07f68e06dfc6e7f..81a067a2e5265fe89599399f01ff592f52792b97 100644 (file)
@@ -239,6 +239,7 @@ static struct ipv6_devconf ipv6_devconf __read_mostly = {
        .ndisc_evict_nocarrier  = 1,
        .ra_honor_pio_life      = 0,
        .ra_honor_pio_pflag     = 0,
+       .force_forwarding       = 0,
 };
 
 static struct ipv6_devconf ipv6_devconf_dflt __read_mostly = {
@@ -303,6 +304,7 @@ static struct ipv6_devconf ipv6_devconf_dflt __read_mostly = {
        .ndisc_evict_nocarrier  = 1,
        .ra_honor_pio_life      = 0,
        .ra_honor_pio_pflag     = 0,
+       .force_forwarding       = 0,
 };
 
 /* Check if link is ready: is it up and is a valid qdisc available */
@@ -857,6 +859,9 @@ static void addrconf_forward_change(struct net *net, __s32 newf)
                idev = __in6_dev_get_rtnl_net(dev);
                if (idev) {
                        int changed = (!idev->cnf.forwarding) ^ (!newf);
+                       /* Disabling all.forwarding sets 0 to force_forwarding for all interfaces */
+                       if (newf == 0)
+                               WRITE_ONCE(idev->cnf.force_forwarding, 0);
 
                        WRITE_ONCE(idev->cnf.forwarding, newf);
                        if (changed)
@@ -5710,6 +5715,7 @@ static void ipv6_store_devconf(const struct ipv6_devconf *cnf,
        array[DEVCONF_ACCEPT_UNTRACKED_NA] =
                READ_ONCE(cnf->accept_untracked_na);
        array[DEVCONF_ACCEPT_RA_MIN_LFT] = READ_ONCE(cnf->accept_ra_min_lft);
+       array[DEVCONF_FORCE_FORWARDING] = READ_ONCE(cnf->force_forwarding);
 }
 
 static inline size_t inet6_ifla6_size(void)
@@ -6738,6 +6744,75 @@ static int addrconf_sysctl_disable_policy(const struct ctl_table *ctl, int write
        return ret;
 }
 
+static void addrconf_force_forward_change(struct net *net, __s32 newf)
+{
+       struct net_device *dev;
+       struct inet6_dev *idev;
+
+       for_each_netdev(net, dev) {
+               idev = __in6_dev_get_rtnl_net(dev);
+               if (idev) {
+                       int changed = (!idev->cnf.force_forwarding) ^ (!newf);
+
+                       WRITE_ONCE(idev->cnf.force_forwarding, newf);
+                       if (changed)
+                               inet6_netconf_notify_devconf(dev_net(dev), RTM_NEWNETCONF,
+                                                            NETCONFA_FORCE_FORWARDING,
+                                                            dev->ifindex, &idev->cnf);
+               }
+       }
+}
+
+static int addrconf_sysctl_force_forwarding(const struct ctl_table *ctl, int write,
+                                           void *buffer, size_t *lenp, loff_t *ppos)
+{
+       struct inet6_dev *idev = ctl->extra1;
+       struct ctl_table tmp_ctl = *ctl;
+       struct net *net = ctl->extra2;
+       int *valp = ctl->data;
+       int new_val = *valp;
+       int old_val = *valp;
+       loff_t pos = *ppos;
+       int ret;
+
+       tmp_ctl.extra1 = SYSCTL_ZERO;
+       tmp_ctl.extra2 = SYSCTL_ONE;
+       tmp_ctl.data = &new_val;
+
+       ret = proc_douintvec_minmax(&tmp_ctl, write, buffer, lenp, ppos);
+
+       if (write && old_val != new_val) {
+               if (!rtnl_net_trylock(net))
+                       return restart_syscall();
+
+               WRITE_ONCE(*valp, new_val);
+
+               if (valp == &net->ipv6.devconf_dflt->force_forwarding) {
+                       inet6_netconf_notify_devconf(net, RTM_NEWNETCONF,
+                                                    NETCONFA_FORCE_FORWARDING,
+                                                    NETCONFA_IFINDEX_DEFAULT,
+                                                    net->ipv6.devconf_dflt);
+               } else if (valp == &net->ipv6.devconf_all->force_forwarding) {
+                       inet6_netconf_notify_devconf(net, RTM_NEWNETCONF,
+                                                    NETCONFA_FORCE_FORWARDING,
+                                                    NETCONFA_IFINDEX_ALL,
+                                                    net->ipv6.devconf_all);
+
+                       addrconf_force_forward_change(net, new_val);
+               } else {
+                       inet6_netconf_notify_devconf(net, RTM_NEWNETCONF,
+                                                    NETCONFA_FORCE_FORWARDING,
+                                                    idev->dev->ifindex,
+                                                    &idev->cnf);
+               }
+               rtnl_net_unlock(net);
+       }
+
+       if (ret)
+               *ppos = pos;
+       return ret;
+}
+
 static int minus_one = -1;
 static const int two_five_five = 255;
 static u32 ioam6_if_id_max = U16_MAX;
@@ -7208,6 +7283,13 @@ static const struct ctl_table addrconf_sysctl[] = {
                .extra1         = SYSCTL_ZERO,
                .extra2         = SYSCTL_TWO,
        },
+       {
+               .procname       = "force_forwarding",
+               .data           = &ipv6_devconf.force_forwarding,
+               .maxlen         = sizeof(int),
+               .mode           = 0644,
+               .proc_handler   = addrconf_sysctl_force_forwarding,
+       },
 };
 
 static int __addrconf_sysctl_register(struct net *net, char *dev_name,
index 0412f85446958e25b8c9838f1e956114a0b2ac6b..1e1410237b6ef0eca7a0aac13c4d7c56a77e0252 100644 (file)
@@ -511,7 +511,8 @@ int ip6_forward(struct sk_buff *skb)
        u32 mtu;
 
        idev = __in6_dev_get_safely(dev_get_by_index_rcu(net, IP6CB(skb)->iif));
-       if (READ_ONCE(net->ipv6.devconf_all->forwarding) == 0)
+       if (!READ_ONCE(net->ipv6.devconf_all->forwarding) &&
+           (!idev || !READ_ONCE(idev->cnf.force_forwarding)))
                goto error;
 
        if (skb->pkt_type != PACKET_HOST)
index 13e2678d418b03583a78c7e0eadfac8f5f869184..b31a71f2b37295973ca5aec14dd540146f2d7570 100644 (file)
@@ -116,6 +116,7 @@ TEST_GEN_FILES += skf_net_off
 TEST_GEN_FILES += tfo
 TEST_PROGS += tfo_passive.sh
 TEST_PROGS += broadcast_pmtu.sh
+TEST_PROGS += ipv6_force_forwarding.sh
 
 # YNL files, must be before "include ..lib.mk"
 YNL_GEN_FILES := busy_poller netlink-dumps
diff --git a/tools/testing/selftests/net/ipv6_force_forwarding.sh b/tools/testing/selftests/net/ipv6_force_forwarding.sh
new file mode 100755 (executable)
index 0000000..bf02433
--- /dev/null
@@ -0,0 +1,105 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test IPv6 force_forwarding interface property
+#
+# This test verifies that the force_forwarding property works correctly:
+# - When global forwarding is disabled, packets are not forwarded normally
+# - When force_forwarding is enabled on an interface, packets are forwarded
+#   regardless of the global forwarding setting
+
+source lib.sh
+
+cleanup() {
+    cleanup_ns $ns1 $ns2 $ns3
+}
+
+trap cleanup EXIT
+
+setup_test() {
+    # Create three namespaces: sender, router, receiver
+    setup_ns ns1 ns2 ns3
+
+    # Create veth pairs: ns1 <-> ns2 <-> ns3
+    ip link add name veth12 type veth peer name veth21
+    ip link add name veth23 type veth peer name veth32
+
+    # Move interfaces to namespaces
+    ip link set veth12 netns $ns1
+    ip link set veth21 netns $ns2
+    ip link set veth23 netns $ns2
+    ip link set veth32 netns $ns3
+
+    # Configure interfaces
+    ip -n $ns1 addr add 2001:db8:1::1/64 dev veth12 nodad
+    ip -n $ns2 addr add 2001:db8:1::2/64 dev veth21 nodad
+    ip -n $ns2 addr add 2001:db8:2::1/64 dev veth23 nodad
+    ip -n $ns3 addr add 2001:db8:2::2/64 dev veth32 nodad
+
+    # Bring up interfaces
+    ip -n $ns1 link set veth12 up
+    ip -n $ns2 link set veth21 up
+    ip -n $ns2 link set veth23 up
+    ip -n $ns3 link set veth32 up
+
+    # Add routes
+    ip -n $ns1 route add 2001:db8:2::/64 via 2001:db8:1::2
+    ip -n $ns3 route add 2001:db8:1::/64 via 2001:db8:2::1
+
+    # Disable global forwarding
+    ip netns exec $ns2 sysctl -qw net.ipv6.conf.all.forwarding=0
+}
+
+test_force_forwarding() {
+    local ret=0
+
+    echo "TEST: force_forwarding functionality"
+
+    # Check if force_forwarding sysctl exists
+    if ! ip netns exec $ns2 test -f /proc/sys/net/ipv6/conf/veth21/force_forwarding; then
+        echo "SKIP: force_forwarding not available"
+        return $ksft_skip
+    fi
+
+    # Test 1: Without force_forwarding, ping should fail
+    ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth21.force_forwarding=0
+    ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth23.force_forwarding=0
+
+    if ip netns exec $ns1 ping -6 -c 1 -W 2 2001:db8:2::2 &>/dev/null; then
+        echo "FAIL: ping succeeded when forwarding disabled"
+        ret=1
+    else
+        echo "PASS: forwarding disabled correctly"
+    fi
+
+    # Test 2: With force_forwarding enabled, ping should succeed
+    ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth21.force_forwarding=1
+    ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth23.force_forwarding=1
+
+    if ip netns exec $ns1 ping -6 -c 1 -W 2 2001:db8:2::2 &>/dev/null; then
+        echo "PASS: force_forwarding enabled forwarding"
+    else
+        echo "FAIL: ping failed with force_forwarding enabled"
+        ret=1
+    fi
+
+    return $ret
+}
+
+echo "IPv6 force_forwarding test"
+echo "=========================="
+
+setup_test
+test_force_forwarding
+ret=$?
+
+if [ $ret -eq 0 ]; then
+    echo "OK"
+    exit 0
+elif [ $ret -eq $ksft_skip ]; then
+    echo "SKIP"
+    exit $ksft_skip
+else
+    echo "FAIL"
+    exit 1
+fi