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>
pull/1309/head
Gabriel Goller 2025-07-22 10:18:45 +02:00 committed by Jakub Kicinski
parent 9312ee7649
commit f24987ef69
9 changed files with 200 additions and 3 deletions

View 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

View 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);

View File

@ -199,6 +199,7 @@ enum {
DEVCONF_NDISC_EVICT_NOCARRIER,
DEVCONF_ACCEPT_UNTRACKED_NA,
DEVCONF_ACCEPT_RA_MIN_LFT,
DEVCONF_FORCE_FORWARDING,
DEVCONF_MAX
};

View 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)

View 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
};

View 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,

View 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)

View 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

View File

@ -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