Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

ipv6: add `force_forwarding` sysctl to enable per-interface forwarding

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>

authored by

Gabriel Goller and committed by
Jakub Kicinski
f24987ef 9312ee76

+200 -3
+6 -2
Documentation/networking/ip-sysctl.rst
··· 2543 2543 conf/all/forwarding - BOOLEAN 2544 2544 Enable global IPv6 forwarding between all interfaces. 2545 2545 2546 - IPv4 and IPv6 work differently here; e.g. netfilter must be used 2547 - to control which interfaces may forward packets and which not. 2546 + IPv4 and IPv6 work differently here; the ``force_forwarding`` flag must 2547 + be used to control which interfaces may forward packets. 2548 2548 2549 2549 This also sets all interfaces' Host/Router setting 2550 2550 'forwarding' to the specified value. See below for details. ··· 2561 2561 2562 2562 Default: 0 (disabled) 2563 2563 2564 + force_forwarding - BOOLEAN 2565 + Enable forwarding on this interface only -- regardless of the setting on 2566 + ``conf/all/forwarding``. When setting ``conf.all.forwarding`` to 0, 2567 + the ``force_forwarding`` flag will be reset on all interfaces. 2564 2568 2565 2569 fwmark_reflect - BOOLEAN 2566 2570 Controls the fwmark of kernel-generated IPv6 reply packets that are not
+1
include/linux/ipv6.h
··· 17 17 __s32 hop_limit; 18 18 __s32 mtu6; 19 19 __s32 forwarding; 20 + __s32 force_forwarding; 20 21 __s32 disable_policy; 21 22 __s32 proxy_ndp; 22 23 __cacheline_group_end(ipv6_devconf_read_txrx);
+1
include/uapi/linux/ipv6.h
··· 199 199 DEVCONF_NDISC_EVICT_NOCARRIER, 200 200 DEVCONF_ACCEPT_UNTRACKED_NA, 201 201 DEVCONF_ACCEPT_RA_MIN_LFT, 202 + DEVCONF_FORCE_FORWARDING, 202 203 DEVCONF_MAX 203 204 }; 204 205
+1
include/uapi/linux/netconf.h
··· 19 19 NETCONFA_IGNORE_ROUTES_WITH_LINKDOWN, 20 20 NETCONFA_INPUT, 21 21 NETCONFA_BC_FORWARDING, 22 + NETCONFA_FORCE_FORWARDING, 22 23 __NETCONFA_MAX 23 24 }; 24 25 #define NETCONFA_MAX (__NETCONFA_MAX - 1)
+1
include/uapi/linux/sysctl.h
··· 573 573 NET_IPV6_ACCEPT_RA_FROM_LOCAL=26, 574 574 NET_IPV6_ACCEPT_RA_RT_INFO_MIN_PLEN=27, 575 575 NET_IPV6_RA_DEFRTR_METRIC=28, 576 + NET_IPV6_FORCE_FORWARDING=29, 576 577 __NET_IPV6_MAX 577 578 }; 578 579
+82
net/ipv6/addrconf.c
··· 239 239 .ndisc_evict_nocarrier = 1, 240 240 .ra_honor_pio_life = 0, 241 241 .ra_honor_pio_pflag = 0, 242 + .force_forwarding = 0, 242 243 }; 243 244 244 245 static struct ipv6_devconf ipv6_devconf_dflt __read_mostly = { ··· 304 303 .ndisc_evict_nocarrier = 1, 305 304 .ra_honor_pio_life = 0, 306 305 .ra_honor_pio_pflag = 0, 306 + .force_forwarding = 0, 307 307 }; 308 308 309 309 /* Check if link is ready: is it up and is a valid qdisc available */ ··· 859 857 idev = __in6_dev_get_rtnl_net(dev); 860 858 if (idev) { 861 859 int changed = (!idev->cnf.forwarding) ^ (!newf); 860 + /* Disabling all.forwarding sets 0 to force_forwarding for all interfaces */ 861 + if (newf == 0) 862 + WRITE_ONCE(idev->cnf.force_forwarding, 0); 862 863 863 864 WRITE_ONCE(idev->cnf.forwarding, newf); 864 865 if (changed) ··· 5715 5710 array[DEVCONF_ACCEPT_UNTRACKED_NA] = 5716 5711 READ_ONCE(cnf->accept_untracked_na); 5717 5712 array[DEVCONF_ACCEPT_RA_MIN_LFT] = READ_ONCE(cnf->accept_ra_min_lft); 5713 + array[DEVCONF_FORCE_FORWARDING] = READ_ONCE(cnf->force_forwarding); 5718 5714 } 5719 5715 5720 5716 static inline size_t inet6_ifla6_size(void) ··· 6744 6738 return ret; 6745 6739 } 6746 6740 6741 + static void addrconf_force_forward_change(struct net *net, __s32 newf) 6742 + { 6743 + struct net_device *dev; 6744 + struct inet6_dev *idev; 6745 + 6746 + for_each_netdev(net, dev) { 6747 + idev = __in6_dev_get_rtnl_net(dev); 6748 + if (idev) { 6749 + int changed = (!idev->cnf.force_forwarding) ^ (!newf); 6750 + 6751 + WRITE_ONCE(idev->cnf.force_forwarding, newf); 6752 + if (changed) 6753 + inet6_netconf_notify_devconf(dev_net(dev), RTM_NEWNETCONF, 6754 + NETCONFA_FORCE_FORWARDING, 6755 + dev->ifindex, &idev->cnf); 6756 + } 6757 + } 6758 + } 6759 + 6760 + static int addrconf_sysctl_force_forwarding(const struct ctl_table *ctl, int write, 6761 + void *buffer, size_t *lenp, loff_t *ppos) 6762 + { 6763 + struct inet6_dev *idev = ctl->extra1; 6764 + struct ctl_table tmp_ctl = *ctl; 6765 + struct net *net = ctl->extra2; 6766 + int *valp = ctl->data; 6767 + int new_val = *valp; 6768 + int old_val = *valp; 6769 + loff_t pos = *ppos; 6770 + int ret; 6771 + 6772 + tmp_ctl.extra1 = SYSCTL_ZERO; 6773 + tmp_ctl.extra2 = SYSCTL_ONE; 6774 + tmp_ctl.data = &new_val; 6775 + 6776 + ret = proc_douintvec_minmax(&tmp_ctl, write, buffer, lenp, ppos); 6777 + 6778 + if (write && old_val != new_val) { 6779 + if (!rtnl_net_trylock(net)) 6780 + return restart_syscall(); 6781 + 6782 + WRITE_ONCE(*valp, new_val); 6783 + 6784 + if (valp == &net->ipv6.devconf_dflt->force_forwarding) { 6785 + inet6_netconf_notify_devconf(net, RTM_NEWNETCONF, 6786 + NETCONFA_FORCE_FORWARDING, 6787 + NETCONFA_IFINDEX_DEFAULT, 6788 + net->ipv6.devconf_dflt); 6789 + } else if (valp == &net->ipv6.devconf_all->force_forwarding) { 6790 + inet6_netconf_notify_devconf(net, RTM_NEWNETCONF, 6791 + NETCONFA_FORCE_FORWARDING, 6792 + NETCONFA_IFINDEX_ALL, 6793 + net->ipv6.devconf_all); 6794 + 6795 + addrconf_force_forward_change(net, new_val); 6796 + } else { 6797 + inet6_netconf_notify_devconf(net, RTM_NEWNETCONF, 6798 + NETCONFA_FORCE_FORWARDING, 6799 + idev->dev->ifindex, 6800 + &idev->cnf); 6801 + } 6802 + rtnl_net_unlock(net); 6803 + } 6804 + 6805 + if (ret) 6806 + *ppos = pos; 6807 + return ret; 6808 + } 6809 + 6747 6810 static int minus_one = -1; 6748 6811 static const int two_five_five = 255; 6749 6812 static u32 ioam6_if_id_max = U16_MAX; ··· 7282 7207 .proc_handler = proc_dointvec_minmax, 7283 7208 .extra1 = SYSCTL_ZERO, 7284 7209 .extra2 = SYSCTL_TWO, 7210 + }, 7211 + { 7212 + .procname = "force_forwarding", 7213 + .data = &ipv6_devconf.force_forwarding, 7214 + .maxlen = sizeof(int), 7215 + .mode = 0644, 7216 + .proc_handler = addrconf_sysctl_force_forwarding, 7285 7217 }, 7286 7218 }; 7287 7219
+2 -1
net/ipv6/ip6_output.c
··· 511 511 u32 mtu; 512 512 513 513 idev = __in6_dev_get_safely(dev_get_by_index_rcu(net, IP6CB(skb)->iif)); 514 - if (READ_ONCE(net->ipv6.devconf_all->forwarding) == 0) 514 + if (!READ_ONCE(net->ipv6.devconf_all->forwarding) && 515 + (!idev || !READ_ONCE(idev->cnf.force_forwarding))) 515 516 goto error; 516 517 517 518 if (skb->pkt_type != PACKET_HOST)
+1
tools/testing/selftests/net/Makefile
··· 116 116 TEST_GEN_FILES += tfo 117 117 TEST_PROGS += tfo_passive.sh 118 118 TEST_PROGS += broadcast_pmtu.sh 119 + TEST_PROGS += ipv6_force_forwarding.sh 119 120 120 121 # YNL files, must be before "include ..lib.mk" 121 122 YNL_GEN_FILES := busy_poller netlink-dumps
+105
tools/testing/selftests/net/ipv6_force_forwarding.sh
··· 1 + #!/bin/bash 2 + # SPDX-License-Identifier: GPL-2.0 3 + # 4 + # Test IPv6 force_forwarding interface property 5 + # 6 + # This test verifies that the force_forwarding property works correctly: 7 + # - When global forwarding is disabled, packets are not forwarded normally 8 + # - When force_forwarding is enabled on an interface, packets are forwarded 9 + # regardless of the global forwarding setting 10 + 11 + source lib.sh 12 + 13 + cleanup() { 14 + cleanup_ns $ns1 $ns2 $ns3 15 + } 16 + 17 + trap cleanup EXIT 18 + 19 + setup_test() { 20 + # Create three namespaces: sender, router, receiver 21 + setup_ns ns1 ns2 ns3 22 + 23 + # Create veth pairs: ns1 <-> ns2 <-> ns3 24 + ip link add name veth12 type veth peer name veth21 25 + ip link add name veth23 type veth peer name veth32 26 + 27 + # Move interfaces to namespaces 28 + ip link set veth12 netns $ns1 29 + ip link set veth21 netns $ns2 30 + ip link set veth23 netns $ns2 31 + ip link set veth32 netns $ns3 32 + 33 + # Configure interfaces 34 + ip -n $ns1 addr add 2001:db8:1::1/64 dev veth12 nodad 35 + ip -n $ns2 addr add 2001:db8:1::2/64 dev veth21 nodad 36 + ip -n $ns2 addr add 2001:db8:2::1/64 dev veth23 nodad 37 + ip -n $ns3 addr add 2001:db8:2::2/64 dev veth32 nodad 38 + 39 + # Bring up interfaces 40 + ip -n $ns1 link set veth12 up 41 + ip -n $ns2 link set veth21 up 42 + ip -n $ns2 link set veth23 up 43 + ip -n $ns3 link set veth32 up 44 + 45 + # Add routes 46 + ip -n $ns1 route add 2001:db8:2::/64 via 2001:db8:1::2 47 + ip -n $ns3 route add 2001:db8:1::/64 via 2001:db8:2::1 48 + 49 + # Disable global forwarding 50 + ip netns exec $ns2 sysctl -qw net.ipv6.conf.all.forwarding=0 51 + } 52 + 53 + test_force_forwarding() { 54 + local ret=0 55 + 56 + echo "TEST: force_forwarding functionality" 57 + 58 + # Check if force_forwarding sysctl exists 59 + if ! ip netns exec $ns2 test -f /proc/sys/net/ipv6/conf/veth21/force_forwarding; then 60 + echo "SKIP: force_forwarding not available" 61 + return $ksft_skip 62 + fi 63 + 64 + # Test 1: Without force_forwarding, ping should fail 65 + ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth21.force_forwarding=0 66 + ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth23.force_forwarding=0 67 + 68 + if ip netns exec $ns1 ping -6 -c 1 -W 2 2001:db8:2::2 &>/dev/null; then 69 + echo "FAIL: ping succeeded when forwarding disabled" 70 + ret=1 71 + else 72 + echo "PASS: forwarding disabled correctly" 73 + fi 74 + 75 + # Test 2: With force_forwarding enabled, ping should succeed 76 + ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth21.force_forwarding=1 77 + ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth23.force_forwarding=1 78 + 79 + if ip netns exec $ns1 ping -6 -c 1 -W 2 2001:db8:2::2 &>/dev/null; then 80 + echo "PASS: force_forwarding enabled forwarding" 81 + else 82 + echo "FAIL: ping failed with force_forwarding enabled" 83 + ret=1 84 + fi 85 + 86 + return $ret 87 + } 88 + 89 + echo "IPv6 force_forwarding test" 90 + echo "==========================" 91 + 92 + setup_test 93 + test_force_forwarding 94 + ret=$? 95 + 96 + if [ $ret -eq 0 ]; then 97 + echo "OK" 98 + exit 0 99 + elif [ $ret -eq $ksft_skip ]; then 100 + echo "SKIP" 101 + exit $ksft_skip 102 + else 103 + echo "FAIL" 104 + exit 1 105 + fi