Skip to content

Commit f24987e

Browse files
kaffarellkuba-moo
authored andcommitted
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>
1 parent 9312ee7 commit f24987e

9 files changed

Lines changed: 200 additions & 3 deletions

File tree

Documentation/networking/ip-sysctl.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2543,8 +2543,8 @@ conf/all/disable_ipv6 - BOOLEAN
25432543
conf/all/forwarding - BOOLEAN
25442544
Enable global IPv6 forwarding between all interfaces.
25452545

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.
25482548

25492549
This also sets all interfaces' Host/Router setting
25502550
'forwarding' to the specified value. See below for details.
@@ -2561,6 +2561,10 @@ proxy_ndp - BOOLEAN
25612561

25622562
Default: 0 (disabled)
25632563

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.
25642568

25652569
fwmark_reflect - BOOLEAN
25662570
Controls the fwmark of kernel-generated IPv6 reply packets that are not

include/linux/ipv6.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct ipv6_devconf {
1717
__s32 hop_limit;
1818
__s32 mtu6;
1919
__s32 forwarding;
20+
__s32 force_forwarding;
2021
__s32 disable_policy;
2122
__s32 proxy_ndp;
2223
__cacheline_group_end(ipv6_devconf_read_txrx);

include/uapi/linux/ipv6.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ enum {
199199
DEVCONF_NDISC_EVICT_NOCARRIER,
200200
DEVCONF_ACCEPT_UNTRACKED_NA,
201201
DEVCONF_ACCEPT_RA_MIN_LFT,
202+
DEVCONF_FORCE_FORWARDING,
202203
DEVCONF_MAX
203204
};
204205

include/uapi/linux/netconf.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ enum {
1919
NETCONFA_IGNORE_ROUTES_WITH_LINKDOWN,
2020
NETCONFA_INPUT,
2121
NETCONFA_BC_FORWARDING,
22+
NETCONFA_FORCE_FORWARDING,
2223
__NETCONFA_MAX
2324
};
2425
#define NETCONFA_MAX (__NETCONFA_MAX - 1)

include/uapi/linux/sysctl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ enum {
573573
NET_IPV6_ACCEPT_RA_FROM_LOCAL=26,
574574
NET_IPV6_ACCEPT_RA_RT_INFO_MIN_PLEN=27,
575575
NET_IPV6_RA_DEFRTR_METRIC=28,
576+
NET_IPV6_FORCE_FORWARDING=29,
576577
__NET_IPV6_MAX
577578
};
578579

net/ipv6/addrconf.c

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ static struct ipv6_devconf ipv6_devconf __read_mostly = {
239239
.ndisc_evict_nocarrier = 1,
240240
.ra_honor_pio_life = 0,
241241
.ra_honor_pio_pflag = 0,
242+
.force_forwarding = 0,
242243
};
243244

244245
static struct ipv6_devconf ipv6_devconf_dflt __read_mostly = {
@@ -303,6 +304,7 @@ static struct ipv6_devconf ipv6_devconf_dflt __read_mostly = {
303304
.ndisc_evict_nocarrier = 1,
304305
.ra_honor_pio_life = 0,
305306
.ra_honor_pio_pflag = 0,
307+
.force_forwarding = 0,
306308
};
307309

308310
/* 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)
857859
idev = __in6_dev_get_rtnl_net(dev);
858860
if (idev) {
859861
int changed = (!idev->cnf.forwarding) ^ (!newf);
862+
/* Disabling all.forwarding sets 0 to force_forwarding for all interfaces */
863+
if (newf == 0)
864+
WRITE_ONCE(idev->cnf.force_forwarding, 0);
860865

861866
WRITE_ONCE(idev->cnf.forwarding, newf);
862867
if (changed)
@@ -5710,6 +5715,7 @@ static void ipv6_store_devconf(const struct ipv6_devconf *cnf,
57105715
array[DEVCONF_ACCEPT_UNTRACKED_NA] =
57115716
READ_ONCE(cnf->accept_untracked_na);
57125717
array[DEVCONF_ACCEPT_RA_MIN_LFT] = READ_ONCE(cnf->accept_ra_min_lft);
5718+
array[DEVCONF_FORCE_FORWARDING] = READ_ONCE(cnf->force_forwarding);
57135719
}
57145720

57155721
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
67386744
return ret;
67396745
}
67406746

6747+
static void addrconf_force_forward_change(struct net *net, __s32 newf)
6748+
{
6749+
struct net_device *dev;
6750+
struct inet6_dev *idev;
6751+
6752+
for_each_netdev(net, dev) {
6753+
idev = __in6_dev_get_rtnl_net(dev);
6754+
if (idev) {
6755+
int changed = (!idev->cnf.force_forwarding) ^ (!newf);
6756+
6757+
WRITE_ONCE(idev->cnf.force_forwarding, newf);
6758+
if (changed)
6759+
inet6_netconf_notify_devconf(dev_net(dev), RTM_NEWNETCONF,
6760+
NETCONFA_FORCE_FORWARDING,
6761+
dev->ifindex, &idev->cnf);
6762+
}
6763+
}
6764+
}
6765+
6766+
static int addrconf_sysctl_force_forwarding(const struct ctl_table *ctl, int write,
6767+
void *buffer, size_t *lenp, loff_t *ppos)
6768+
{
6769+
struct inet6_dev *idev = ctl->extra1;
6770+
struct ctl_table tmp_ctl = *ctl;
6771+
struct net *net = ctl->extra2;
6772+
int *valp = ctl->data;
6773+
int new_val = *valp;
6774+
int old_val = *valp;
6775+
loff_t pos = *ppos;
6776+
int ret;
6777+
6778+
tmp_ctl.extra1 = SYSCTL_ZERO;
6779+
tmp_ctl.extra2 = SYSCTL_ONE;
6780+
tmp_ctl.data = &new_val;
6781+
6782+
ret = proc_douintvec_minmax(&tmp_ctl, write, buffer, lenp, ppos);
6783+
6784+
if (write && old_val != new_val) {
6785+
if (!rtnl_net_trylock(net))
6786+
return restart_syscall();
6787+
6788+
WRITE_ONCE(*valp, new_val);
6789+
6790+
if (valp == &net->ipv6.devconf_dflt->force_forwarding) {
6791+
inet6_netconf_notify_devconf(net, RTM_NEWNETCONF,
6792+
NETCONFA_FORCE_FORWARDING,
6793+
NETCONFA_IFINDEX_DEFAULT,
6794+
net->ipv6.devconf_dflt);
6795+
} else if (valp == &net->ipv6.devconf_all->force_forwarding) {
6796+
inet6_netconf_notify_devconf(net, RTM_NEWNETCONF,
6797+
NETCONFA_FORCE_FORWARDING,
6798+
NETCONFA_IFINDEX_ALL,
6799+
net->ipv6.devconf_all);
6800+
6801+
addrconf_force_forward_change(net, new_val);
6802+
} else {
6803+
inet6_netconf_notify_devconf(net, RTM_NEWNETCONF,
6804+
NETCONFA_FORCE_FORWARDING,
6805+
idev->dev->ifindex,
6806+
&idev->cnf);
6807+
}
6808+
rtnl_net_unlock(net);
6809+
}
6810+
6811+
if (ret)
6812+
*ppos = pos;
6813+
return ret;
6814+
}
6815+
67416816
static int minus_one = -1;
67426817
static const int two_five_five = 255;
67436818
static u32 ioam6_if_id_max = U16_MAX;
@@ -7208,6 +7283,13 @@ static const struct ctl_table addrconf_sysctl[] = {
72087283
.extra1 = SYSCTL_ZERO,
72097284
.extra2 = SYSCTL_TWO,
72107285
},
7286+
{
7287+
.procname = "force_forwarding",
7288+
.data = &ipv6_devconf.force_forwarding,
7289+
.maxlen = sizeof(int),
7290+
.mode = 0644,
7291+
.proc_handler = addrconf_sysctl_force_forwarding,
7292+
},
72117293
};
72127294

72137295
static int __addrconf_sysctl_register(struct net *net, char *dev_name,

net/ipv6/ip6_output.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,8 @@ int ip6_forward(struct sk_buff *skb)
511511
u32 mtu;
512512

513513
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)))
515516
goto error;
516517

517518
if (skb->pkt_type != PACKET_HOST)

tools/testing/selftests/net/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ TEST_GEN_FILES += skf_net_off
116116
TEST_GEN_FILES += tfo
117117
TEST_PROGS += tfo_passive.sh
118118
TEST_PROGS += broadcast_pmtu.sh
119+
TEST_PROGS += ipv6_force_forwarding.sh
119120

120121
# YNL files, must be before "include ..lib.mk"
121122
YNL_GEN_FILES := busy_poller netlink-dumps
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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

0 commit comments

Comments
 (0)