* [PATCH net v3 0/3] net/ipv6: don't route packets with unknown source address
@ 2026-10-28 19:10 Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 1/3] net/ipv6: add ipv6_chk_addr_namespace Íñigo Huguet
` (2 more replies)
0 siblings, 3 replies; 8+ messages in thread
From: Íñigo Huguet @ 2026-10-28 19:10 UTC (permalink / raw)
To: Alexei Starovoitov, Andrew Lunn, Andrii Nakryiko, Daniel Borkmann,
David Ahern, David S. Miller, Eduard Zingerman, Eric Dumazet,
Florian Westphal, Ido Schimmel, Jakub Kicinski,
Kumar Kartikeya Dwivedi, Neal Cardwell, Pablo Neira Ayuso,
Paolo Abeni, Shuah Khan, Willem de Bruijn
Cc: Íñigo Huguet, bpf, Breno Leitao, coreteam,
Emil Tsalapatis, Fernando Fernandez Mancera, Ihor Solodrai,
Jeff Layton, Jiri Olsa, Joe Damato, John Fastabend, Junseo Lim,
Kees Cook, Kuniyuki Iwashima, Leon Hwang, Li RongQing,
linux-kselftest, Martin KaFai Lau, netdev, netfilter-devel,
Phil Sutter, Qi Tang, Sebastian Andrzej Siewior, Simon Horman,
Song Liu, Stanislav Fomichev, Willem de Bruijn, Xuanqiang Luo,
Yonghong Song, Íñigo Huguet, Davide Caratti
Don't allow routing packets with a source address that is not configured
in the host. Allow it only in certain cases like when using a
transparent socket, by setting the ANYSRC flag in flowi_flags.
Main reasons (see full explanation in the 2nd patch):
- Alignment with IPv4 stack
- Compliance with RFC 4862 (an invalid address MUST NOT be used as the
source address of outbound packets)
- Meet expectations of stop using an address after removing it or its
expiration lifetime.
Added selftests covering the patch, and also its IPv4 counterpart which
had no tests covering it. I also added selftests covering 2 features
that were impacted by v2 and discovered at review: anycast and loopback
subnets.
The change in the netfilter's ip6_route_me_harder function is the one
that I'm more unsure about. It was not clear to me the reason why it was
done like this in the IPv4 counterpart. Please review carefully.
Testing: tested with a manual reproducer executing the steps described
in the patch. Tested also with transparent sockets to ensure that the
packets are sent in that case. Also executed the following selftests
to prevent potential regressions: net/*, net/forwarding/* and
net/netfilter/*. I also ran Sashiko locally and applied most of its
change proposals.
I ignored on purpose some review comments from Sashiko saying that
the ANYSRC flag must be set in some additional places. The reasoning
to ignore them is that they are neither set in IPv4. If they were
not needed in IPv4 where the source address check has been in place
for many years, probably they are neither needed in the IPv6
counterpart. These are the functions where I ignored Sashiko's
comments:
- sctp_v6_get_dst
- nat_keepalive_send_ipv6
- rxe_find_route6
- tipc_udp_xmit
Signed-off-by: Íñigo Huguet <ihuguet@riseup.net>
---
v3:
- Fixed errors in the selftest xfrm_state by setting ANYSRC in
icmpv6_route_lookup.
- Fixed errors in the selftest vrf_route_leaking by searching the
address in the entire namespace, not in a specific L3 domain.
- Fixed loopback subnets (AnyIP) by adding a fallback to FIB local
routes if the exact address is not present.
- Fixed anycast address as source address by using ipv6_chk_acast_addr_src.
- Fixed TCP responses in timewait sockets by using inet_sk_transparent
if !sk_fullsock.
- Added forgotten flag in the IPv6 branch of bpf_skb_set_tunnel_key.
- Added many selftests:
- No tests existed for "check source address on ongoing packets",
neither for IPv4. Added them.
- No tests existed for anycast and loopback subnets, so I didn't
detected it in v2. Added tests for both features.
v2: https://lore.kernel.org/netdev/20260903121057.80006-1-ihuguet@riseup.net/
v1: https://lore.kernel.org/netdev/20260901115021.50057-1-ihuguet@riseup.net/
Íñigo Huguet (3):
net/ipv6: add ipv6_chk_addr_namespace
net/ipv6: don't route packets with unknown source address
selftests: net: add tests for saddr, anycast and loopback subnets
drivers/net/vrf.c | 1 +
include/net/addrconf.h | 4 +-
net/core/filter.c | 1 +
net/core/lwt_bpf.c | 1 +
net/ipv6/addrconf.c | 76 ++-
net/ipv6/af_inet6.c | 1 +
net/ipv6/anycast.c | 2 +-
net/ipv6/datagram.c | 3 +-
net/ipv6/icmp.c | 5 +
net/ipv6/inet6_connection_sock.c | 2 +
net/ipv6/ip6_output.c | 38 ++
net/ipv6/ip6_tunnel.c | 10 +-
net/ipv6/ip6_udp_tunnel.c | 1 +
net/ipv6/ndisc.c | 2 +-
net/ipv6/netfilter.c | 11 +-
net/ipv6/ping.c | 1 +
net/ipv6/raw.c | 1 +
net/ipv6/route.c | 4 +-
net/ipv6/syncookies.c | 1 +
net/ipv6/tcp_ipv6.c | 7 +-
net/ipv6/udp.c | 1 +
net/l2tp/l2tp_ip6.c | 2 +
tools/testing/selftests/net/.gitignore | 1 +
tools/testing/selftests/net/Makefile | 4 +
tools/testing/selftests/net/ip_saddr.c | 557 +++++++++++++++++
tools/testing/selftests/net/ip_saddr.sh | 575 ++++++++++++++++++
tools/testing/selftests/net/ipv6_anycast.sh | 68 +++
.../testing/selftests/net/loopback_subnet.sh | 135 ++++
28 files changed, 1472 insertions(+), 43 deletions(-)
create mode 100644 tools/testing/selftests/net/ip_saddr.c
create mode 100755 tools/testing/selftests/net/ip_saddr.sh
create mode 100755 tools/testing/selftests/net/ipv6_anycast.sh
create mode 100755 tools/testing/selftests/net/loopback_subnet.sh
--
2.54.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH net v3 1/3] net/ipv6: add ipv6_chk_addr_namespace
2026-10-28 19:10 [PATCH net v3 0/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
@ 2026-10-28 19:10 ` Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 3/3] selftests: net: add tests for saddr, anycast and loopback subnets Íñigo Huguet
2 siblings, 0 replies; 8+ messages in thread
From: Íñigo Huguet @ 2026-10-28 19:10 UTC (permalink / raw)
To: Alexei Starovoitov, Andrew Lunn, Andrii Nakryiko, Daniel Borkmann,
David Ahern, David S. Miller, Eduard Zingerman, Eric Dumazet,
Florian Westphal, Ido Schimmel, Jakub Kicinski,
Kumar Kartikeya Dwivedi, Neal Cardwell, Pablo Neira Ayuso,
Paolo Abeni, Shuah Khan, Willem de Bruijn
Cc: Íñigo Huguet, bpf, Breno Leitao, coreteam,
Emil Tsalapatis, Fernando Fernandez Mancera, Ihor Solodrai,
Jeff Layton, Jiri Olsa, Joe Damato, John Fastabend, Junseo Lim,
Kees Cook, Kuniyuki Iwashima, Leon Hwang, Li RongQing,
linux-kselftest, Martin KaFai Lau, netdev, netfilter-devel,
Phil Sutter, Qi Tang, Sebastian Andrzej Siewior, Simon Horman,
Song Liu, Stanislav Fomichev, Willem de Bruijn, Xuanqiang Luo,
Yonghong Song, Íñigo Huguet, Davide Caratti
The ipv6_chk_addr and related functions are all VRF aware, as most
callers need it. The next patch needs to check if the address exists in
the entire namespace, so add a new function for that.
Clarify the behaviour of ipv6_chk_addr and other existing functions
when dev=NULL, as it is not completely obvious that, in that case, the
address is only searched in the main L3 domain.
Signed-off-by: Íñigo Huguet <ihuguet@riseup.net>
---
include/net/addrconf.h | 4 ++-
net/ipv6/addrconf.c | 76 ++++++++++++++++++++++++++----------------
net/ipv6/anycast.c | 2 +-
net/ipv6/datagram.c | 2 +-
net/ipv6/ip6_tunnel.c | 10 +++---
net/ipv6/ndisc.c | 2 +-
net/ipv6/route.c | 4 +--
7 files changed, 60 insertions(+), 40 deletions(-)
diff --git a/include/net/addrconf.h b/include/net/addrconf.h
index 6818057cf727..43201e410c1b 100644
--- a/include/net/addrconf.h
+++ b/include/net/addrconf.h
@@ -120,7 +120,9 @@ int ipv6_chk_addr(struct net *net, const struct in6_addr *addr,
const struct net_device *dev, int strict);
int ipv6_chk_addr_and_flags(struct net *net, const struct in6_addr *addr,
const struct net_device *dev, bool skip_dev_check,
- int strict, u32 banned_flags);
+ bool skip_l3mdev_check, int strict,
+ u32 banned_flags);
+int ipv6_chk_addr_namespace(struct net *net, const struct in6_addr *addr);
#if defined(CONFIG_IPV6_MIP6) || defined(CONFIG_IPV6_MIP6_MODULE)
int ipv6_chk_home_addr(struct net *net, const struct in6_addr *addr);
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index c8c0dd04982d..b13a5846ec95 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1979,43 +1979,30 @@ static int ipv6_count_addresses(const struct inet6_dev *idev)
return cnt;
}
-int ipv6_chk_addr(struct net *net, const struct in6_addr *addr,
- const struct net_device *dev, int strict)
-{
- return ipv6_chk_addr_and_flags(net, addr, dev, !dev,
- strict, IFA_F_TENTATIVE);
-}
-EXPORT_SYMBOL(ipv6_chk_addr);
-
-/* device argument is used to find the L3 domain of interest. If
- * skip_dev_check is set, then the ifp device is not checked against
- * the passed in dev argument. So the 2 cases for addresses checks are:
- * 1. does the address exist in the L3 domain that dev is part of
- * (skip_dev_check = true), or
- *
- * 2. does the address exist on the specific device
- * (skip_dev_check = false)
- */
+/* See ipv6_chk_addr_and_flags */
static struct net_device *
__ipv6_chk_addr_and_flags(struct net *net, const struct in6_addr *addr,
const struct net_device *dev, bool skip_dev_check,
- int strict, u32 banned_flags)
+ bool skip_l3mdev_check, int strict, u32 banned_flags)
{
unsigned int hash = inet6_addr_hash(net, addr);
- struct net_device *l3mdev, *ndev;
+ struct net_device *l3mdev = NULL, *ndev;
struct inet6_ifaddr *ifp;
u32 ifp_flags;
- rcu_read_lock();
+ if (!dev)
+ skip_dev_check = true;
+ if (!skip_dev_check)
+ skip_l3mdev_check = false;
- l3mdev = l3mdev_master_dev_rcu(dev);
- if (skip_dev_check)
- dev = NULL;
+ rcu_read_lock();
+ if (dev && !skip_l3mdev_check)
+ l3mdev = l3mdev_master_dev_rcu(dev);
hlist_for_each_entry_rcu(ifp, &net->ipv6.inet6_addr_lst[hash], addr_lst) {
ndev = ifp->idev->dev;
- if (l3mdev_master_dev_rcu(ndev) != l3mdev)
+ if (!skip_l3mdev_check && l3mdev_master_dev_rcu(ndev) != l3mdev)
continue;
/* Decouple optimistic from tentative for evaluation here.
@@ -2026,7 +2013,7 @@ __ipv6_chk_addr_and_flags(struct net *net, const struct in6_addr *addr,
: ifp->flags;
if (ipv6_addr_equal(&ifp->addr, addr) &&
!(ifp_flags&banned_flags) &&
- (!dev || ndev == dev ||
+ (skip_dev_check || ndev == dev ||
!(ifp->scope&(IFA_LINK|IFA_HOST) || strict))) {
rcu_read_unlock();
return ndev;
@@ -2037,15 +2024,46 @@ __ipv6_chk_addr_and_flags(struct net *net, const struct in6_addr *addr,
return NULL;
}
+/* The 'dev' argument is used to match the exact device or find the L3 domain of
+ * interest.
+ * The 3 cases for addresses checks are:
+ * 1. With 'skip_dev_check == false': check if 'addr' exists on 'dev'.
+ * 2. With 'skip_dev_check == true && skip_l3mdev_check == false': check if
+ * 'addr' exists on the same L3 domain as 'dev'.
+ * 3. With 'skip_dev_check == true && skip_l3mdev_check == true': check if
+ * 'addr' exists in the entire namespace.
+ * For the purpose of searching in a specific L3 domain, 'dev == NULL' means
+ * "search in the main L3 domain".
+ */
int ipv6_chk_addr_and_flags(struct net *net, const struct in6_addr *addr,
const struct net_device *dev, bool skip_dev_check,
- int strict, u32 banned_flags)
+ bool skip_l3mdev_check, int strict,
+ u32 banned_flags)
{
return __ipv6_chk_addr_and_flags(net, addr, dev, skip_dev_check,
- strict, banned_flags) ? 1 : 0;
+ skip_l3mdev_check, strict,
+ banned_flags) ? 1 : 0;
}
EXPORT_SYMBOL(ipv6_chk_addr_and_flags);
+/* Check if 'addr' is configured on 'dev'. If 'dev' is NULL, check the entire
+ * main L3 domain
+ */
+int ipv6_chk_addr(struct net *net, const struct in6_addr *addr,
+ const struct net_device *dev, int strict)
+{
+ return ipv6_chk_addr_and_flags(net, addr, dev, !dev, false,
+ strict, IFA_F_TENTATIVE);
+}
+EXPORT_SYMBOL(ipv6_chk_addr);
+
+/* Check if 'addr' exists in the entire namespace, in any L3 domain */
+int ipv6_chk_addr_namespace(struct net *net, const struct in6_addr *addr)
+{
+ return ipv6_chk_addr_and_flags(net, addr, NULL, true, true, 0,
+ IFA_F_TENTATIVE);
+}
+EXPORT_SYMBOL(ipv6_chk_addr_namespace);
/* Compares an address/prefix_len with addresses on device @dev.
* If one is found it returns true.
@@ -2098,14 +2116,14 @@ EXPORT_SYMBOL(ipv6_chk_prefix);
* ipv6_dev_find - find the first device with a given source address.
* @net: the net namespace
* @addr: the source address
- * @dev: used to find the L3 domain of interest
+ * @dev: used to find the L3 domain of interest (NULL = main L3 domain)
*
* The caller should be protected by RCU, or RTNL.
*/
struct net_device *ipv6_dev_find(struct net *net, const struct in6_addr *addr,
struct net_device *dev)
{
- return __ipv6_chk_addr_and_flags(net, addr, dev, !dev, 1,
+ return __ipv6_chk_addr_and_flags(net, addr, dev, !dev, false, 1,
IFA_F_TENTATIVE);
}
EXPORT_SYMBOL(ipv6_dev_find);
diff --git a/net/ipv6/anycast.c b/net/ipv6/anycast.c
index be6dac8a8566..6b00e97b3041 100644
--- a/net/ipv6/anycast.c
+++ b/net/ipv6/anycast.c
@@ -82,7 +82,7 @@ int ipv6_sock_ac_join(struct sock *sk, int ifindex, const struct in6_addr *addr)
if (ifindex)
dev = netdev_get_by_index(net, ifindex, &dev_tracker, GFP_KERNEL);
- if (ipv6_chk_addr_and_flags(net, addr, dev, true, 0, IFA_F_TENTATIVE)) {
+ if (ipv6_chk_addr_and_flags(net, addr, dev, true, false, 0, IFA_F_TENTATIVE)) {
err = -EINVAL;
goto error;
}
diff --git a/net/ipv6/datagram.c b/net/ipv6/datagram.c
index 191c9733ff9f..7b71a932544d 100644
--- a/net/ipv6/datagram.c
+++ b/net/ipv6/datagram.c
@@ -867,7 +867,7 @@ int ip6_datagram_send_ctl(struct net *net, struct sock *sk,
int strict = __ipv6_addr_src_scope(addr_type) <= IPV6_ADDR_SCOPE_LINKLOCAL;
if (!ipv6_can_nonlocal_bind(net, inet_sk(sk)) &&
!ipv6_chk_addr_and_flags(net, &src_info->ipi6_addr,
- dev, !strict, 0,
+ dev, !strict, false, 0,
IFA_F_TENTATIVE) &&
!ipv6_chk_acast_addr_src(net, dev,
&src_info->ipi6_addr))
diff --git a/net/ipv6/ip6_tunnel.c b/net/ipv6/ip6_tunnel.c
index 64a184bca467..baf5bba80346 100644
--- a/net/ipv6/ip6_tunnel.c
+++ b/net/ipv6/ip6_tunnel.c
@@ -794,10 +794,10 @@ int ip6_tnl_rcv_ctl(struct ip6_tnl *t,
ldev = dev_get_by_index_rcu(net, p->link);
if ((ipv6_addr_is_multicast(laddr) ||
- likely(ipv6_chk_addr_and_flags(net, laddr, ldev, false,
+ likely(ipv6_chk_addr_and_flags(net, laddr, ldev, false, false,
0, IFA_F_TENTATIVE))) &&
((p->flags & IP6_TNL_F_ALLOW_LOCAL_REMOTE) ||
- likely(!ipv6_chk_addr_and_flags(net, raddr, ldev, true,
+ likely(!ipv6_chk_addr_and_flags(net, raddr, ldev, true, false,
0, IFA_F_TENTATIVE))))
ret = 1;
}
@@ -1059,14 +1059,14 @@ int ip6_tnl_xmit_ctl(struct ip6_tnl *t,
if (p->link)
ldev = dev_get_by_index_rcu(net, p->link);
- if (unlikely(!ipv6_chk_addr_and_flags(net, laddr, ldev, false,
+ if (unlikely(!ipv6_chk_addr_and_flags(net, laddr, ldev, false, false,
0, IFA_F_TENTATIVE)))
pr_warn_ratelimited("%s xmit: Local address not yet configured!\n",
p->name);
else if (!(p->flags & IP6_TNL_F_ALLOW_LOCAL_REMOTE) &&
!ipv6_addr_is_multicast(raddr) &&
- unlikely(ipv6_chk_addr_and_flags(net, raddr, ldev,
- true, 0, IFA_F_TENTATIVE)))
+ unlikely(ipv6_chk_addr_and_flags(net, raddr, ldev, true,
+ false, 0, IFA_F_TENTATIVE)))
pr_warn_ratelimited("%s xmit: Routing loop! Remote address found on this node!\n",
p->name);
else
diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
index 0ed1a619372b..0bb131be969e 100644
--- a/net/ipv6/ndisc.c
+++ b/net/ipv6/ndisc.c
@@ -746,7 +746,7 @@ static void ndisc_solicit(struct neighbour *neigh, struct sk_buff *skb)
int probes = atomic_read(&neigh->probes);
if (skb && ipv6_chk_addr_and_flags(dev_net(dev), &ipv6_hdr(skb)->saddr,
- dev, false, 1,
+ dev, false, false, 1,
IFA_F_TENTATIVE|IFA_F_OPTIMISTIC))
saddr = &ipv6_hdr(skb)->saddr;
probes -= NEIGH_VAR(neigh->parms, UCAST_PROBES);
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 94e5ef1c4f83..dae25a6638c4 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -3492,7 +3492,7 @@ static int ip6_validate_gw(struct net *net, struct fib6_config *cfg,
* prefix route was assigned to, which might be non-loopback.
*/
if (dev &&
- ipv6_chk_addr_and_flags(net, gw_addr, dev, skip_dev, 0, 0)) {
+ ipv6_chk_addr_and_flags(net, gw_addr, dev, skip_dev, false, 0, 0)) {
NL_SET_ERR_MSG(extack, "Gateway can not be a local address");
goto out;
}
@@ -3543,7 +3543,7 @@ static int ip6_validate_gw(struct net *net, struct fib6_config *cfg,
* egress device has been resolved.
*/
if (need_addr_check &&
- ipv6_chk_addr_and_flags(net, gw_addr, dev, skip_dev, 0, 0)) {
+ ipv6_chk_addr_and_flags(net, gw_addr, dev, skip_dev, false, 0, 0)) {
NL_SET_ERR_MSG(extack, "Gateway can not be a local address");
goto out;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address
2026-10-28 19:10 [PATCH net v3 0/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 1/3] net/ipv6: add ipv6_chk_addr_namespace Íñigo Huguet
@ 2026-10-28 19:10 ` Íñigo Huguet
2026-10-29 0:02 ` Kuniyuki Iwashima
2026-10-29 19:12 ` sashiko-bot
2026-10-28 19:10 ` [PATCH net v3 3/3] selftests: net: add tests for saddr, anycast and loopback subnets Íñigo Huguet
2 siblings, 2 replies; 8+ messages in thread
From: Íñigo Huguet @ 2026-10-28 19:10 UTC (permalink / raw)
To: Alexei Starovoitov, Andrew Lunn, Andrii Nakryiko, Daniel Borkmann,
David Ahern, David S. Miller, Eduard Zingerman, Eric Dumazet,
Florian Westphal, Ido Schimmel, Jakub Kicinski,
Kumar Kartikeya Dwivedi, Neal Cardwell, Pablo Neira Ayuso,
Paolo Abeni, Shuah Khan, Willem de Bruijn
Cc: Íñigo Huguet, bpf, Breno Leitao, coreteam,
Emil Tsalapatis, Fernando Fernandez Mancera, Ihor Solodrai,
Jeff Layton, Jiri Olsa, Joe Damato, John Fastabend, Junseo Lim,
Kees Cook, Kuniyuki Iwashima, Leon Hwang, Li RongQing,
linux-kselftest, Martin KaFai Lau, netdev, netfilter-devel,
Phil Sutter, Qi Tang, Sebastian Andrzej Siewior, Simon Horman,
Song Liu, Stanislav Fomichev, Willem de Bruijn, Xuanqiang Luo,
Yonghong Song, Íñigo Huguet, Davide Caratti
Don't allow routing packets with a source address that is not configured
in the host. Allow it only in certain cases like when using a
transparent socket, by setting the ANYSRC flag in flowi_flags.
Until now, it was possible to send such a packet if a route can be found
in the routing table for it. For example:
1. Configure two addresses 1:2::3:4/64 and 1:2::3:5/64
2. Establish a TCP connection to 1:2::3:4
3. Remove the address from the interface, but keep the other.
4. Packets are still sent out by the TCP connection because of
the automatic route created for the 2nd address. No incoming
packets are accepted, though.
This patch prevents the outgoing packets to be sent in normal
circumnstances.
This aligns the behaviour with the IPv4 stack. To determine the places
where the ANYSRC needs to be set, I set the flag in the same places as
the IPv4 stack does.
Apart from consolidating the behaviour of both stacks, there is a more
important reason why this is needed. RFC 4862 states that "an invalid
address MUST NOT be used as the source address of outbound packets".
Therefore, sending packets with a source address considered "invalid",
like an expired address, is disallowed.
This issue was reported by a user to whom IPv6 packets were leaked after
removing the IPv6 address for the interface. The same happens if the
address has its valid_lft expired.
There is a valid use case that would break with this change: loopback
subnets (aka AnyIP). They work in the following way: the administrator
adds a route like `local 2001:db8:cafe::/64 dev lo`, so that the host
will accept connections to all the addresses of that subnet. However,
there is no address assigned to any interface, only a route, so that
outgoing packets would be dropped because the source address doesn't
belong to any interface in the host.
In IPv4, loopback subnets work because __ip_dev_find fallbacks to a
FIB lookup. For IPv6 it is unclear if such a change would be desired
by all the current callers of ipv6_dev_find, ipv6_chk_addr and
ipv6_chk_addr_and_flags, likely not. Because of that, add the FIB lookup
fallback into ip6_output, where the check of the source address has been
added.
Signed-off-by: Íñigo Huguet <ihuguet@riseup.net>
---
drivers/net/vrf.c | 1 +
net/core/filter.c | 1 +
net/core/lwt_bpf.c | 1 +
net/ipv6/af_inet6.c | 1 +
net/ipv6/datagram.c | 1 +
net/ipv6/icmp.c | 5 +++++
net/ipv6/inet6_connection_sock.c | 2 ++
net/ipv6/ip6_output.c | 38 ++++++++++++++++++++++++++++++++
net/ipv6/ip6_udp_tunnel.c | 1 +
net/ipv6/netfilter.c | 11 +++++++--
net/ipv6/ping.c | 1 +
net/ipv6/raw.c | 1 +
net/ipv6/syncookies.c | 1 +
net/ipv6/tcp_ipv6.c | 7 +++++-
net/ipv6/udp.c | 1 +
net/l2tp/l2tp_ip6.c | 2 ++
16 files changed, 72 insertions(+), 3 deletions(-)
diff --git a/drivers/net/vrf.c b/drivers/net/vrf.c
index 79d433f49f80..4222e598290c 100644
--- a/drivers/net/vrf.c
+++ b/drivers/net/vrf.c
@@ -432,6 +432,7 @@ static netdev_tx_t vrf_process_v6_outbound(struct sk_buff *skb,
fl6.flowlabel = ip6_flowinfo(iph);
fl6.flowi6_mark = skb->mark;
fl6.flowi6_proto = iph->nexthdr;
+ fl6.flowi6_flags = FLOWI_FLAG_ANYSRC;
dst = ip6_dst_lookup_flow(net, NULL, &fl6, NULL);
if (IS_ERR(dst) || dst == dst_null)
diff --git a/net/core/filter.c b/net/core/filter.c
index 70dc621672f2..65ff54f6a29d 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -5077,6 +5077,7 @@ BPF_CALL_4(bpf_skb_set_tunnel_key, struct sk_buff *, skb,
sizeof(from->local_ipv6));
info->key.label = cpu_to_be32(from->tunnel_label) &
IPV6_FLOWLABEL_MASK;
+ info->key.flow_flags = FLOWI_FLAG_ANYSRC;
} else {
info->key.u.ipv4.dst = cpu_to_be32(from->remote_ipv4);
info->key.u.ipv4.src = cpu_to_be32(from->local_ipv4);
diff --git a/net/core/lwt_bpf.c b/net/core/lwt_bpf.c
index da49364ec63d..e8b954282689 100644
--- a/net/core/lwt_bpf.c
+++ b/net/core/lwt_bpf.c
@@ -233,6 +233,7 @@ static int bpf_lwt_xmit_reroute(struct sk_buff *skb)
fl6.flowi6_mark = skb->mark;
fl6.flowi6_uid = sock_net_uid(net, sk);
fl6.flowlabel = ip6_flowinfo(iph6);
+ fl6.flowi6_flags = FLOWI_FLAG_ANYSRC;
fl6.flowi6_proto = iph6->nexthdr;
fl6.daddr = iph6->daddr;
fl6.saddr = iph6->saddr;
diff --git a/net/ipv6/af_inet6.c b/net/ipv6/af_inet6.c
index a9b8b4b42a1a..8e3c6e095419 100644
--- a/net/ipv6/af_inet6.c
+++ b/net/ipv6/af_inet6.c
@@ -816,6 +816,7 @@ int inet6_sk_rebuild_header(struct sock *sk)
fl6->fl6_dport = inet->inet_dport;
fl6->fl6_sport = inet->inet_sport;
fl6->flowi6_uid = sk_uid(sk);
+ fl6->flowi6_flags = inet_sk_flowi_flags(sk);
security_sk_classify_flow(sk, flowi6_to_flowi_common(fl6));
ip6_ecmp_set_mp_hash(sock_net(sk), fl6, sk->sk_txhash);
diff --git a/net/ipv6/datagram.c b/net/ipv6/datagram.c
index 7b71a932544d..f09bbffec0e7 100644
--- a/net/ipv6/datagram.c
+++ b/net/ipv6/datagram.c
@@ -52,6 +52,7 @@ static void ip6_datagram_flow_key_init(struct flowi6 *fl6,
fl6->flowi6_mark = sk->sk_mark;
fl6->fl6_dport = inet->inet_dport;
fl6->fl6_sport = inet->inet_sport;
+ fl6->flowi6_flags = inet_sk_flowi_flags(sk);
fl6->flowlabel = ip6_make_flowinfo(np->tclass, np->flow_label);
fl6->flowi6_uid = sk_uid(sk);
diff --git a/net/ipv6/icmp.c b/net/ipv6/icmp.c
index a95b0351824f..b4b54297c923 100644
--- a/net/ipv6/icmp.c
+++ b/net/ipv6/icmp.c
@@ -393,6 +393,11 @@ static struct dst_entry *icmpv6_route_lookup(struct net *net,
if (err)
goto relookup_failed;
+ /* New src is the original dst. It may not be a local address, like with
+ * encapsulated traffic.
+ */
+ fl2.flowi6_flags |= FLOWI_FLAG_ANYSRC;
+
err = ip6_dst_lookup(net, sk, &dst2, &fl2);
if (err)
goto relookup_failed;
diff --git a/net/ipv6/inet6_connection_sock.c b/net/ipv6/inet6_connection_sock.c
index 3e4ce8cb478e..f295f3efe243 100644
--- a/net/ipv6/inet6_connection_sock.c
+++ b/net/ipv6/inet6_connection_sock.c
@@ -45,6 +45,7 @@ struct dst_entry *inet6_csk_route_req(const struct sock *sk,
fl6->flowi6_mark = ireq->ir_mark;
fl6->fl6_dport = ireq->ir_rmt_port;
fl6->fl6_sport = htons(ireq->ir_num);
+ fl6->flowi6_flags = inet_sk_flowi_flags(sk);
fl6->flowi6_uid = sk_uid(sk);
security_req_classify_flow(req, flowi6_to_flowi_common(fl6));
@@ -71,6 +72,7 @@ struct dst_entry *inet6_csk_route_socket(struct sock *sk,
fl6->daddr = sk->sk_v6_daddr;
fl6->saddr = np->saddr;
fl6->flowlabel = np->flow_label;
+ fl6->flowi6_flags = inet_sk_flowi_flags(sk);
IP6_ECN_flow_xmit(sk, fl6->flowlabel);
if (sk->sk_protocol == IPPROTO_TCP)
diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
index 25fe0ba98b1a..8715cf43f00a 100644
--- a/net/ipv6/ip6_output.c
+++ b/net/ipv6/ip6_output.c
@@ -1134,6 +1134,33 @@ static struct dst_entry *ip6_sk_dst_check(struct sock *sk,
return dst;
}
+/* Check if the address belongs to the host */
+static bool ip6_is_local_saddr(struct net *net, struct in6_addr *saddr)
+{
+ struct fib6_result fib_result;
+ struct flowi6 fl6;
+ bool result;
+
+ if (ipv6_chk_addr_namespace(net, saddr) ||
+ ipv6_chk_acast_addr_src(net, NULL, saddr))
+ return true;
+
+ /* Fallback to FIB local routes so that loopback subnets work. */
+ memset(&fl6, 0, sizeof(fl6));
+ fl6.daddr = *saddr;
+ result = false;
+
+ rcu_read_lock();
+ if (fib6_lookup(net, 0, &fl6, &fib_result, 0) == 0 &&
+ fib_result.fib6_type == RTN_LOCAL &&
+ fib_result.nh->nh_common.nhc_dev) {
+ result = true;
+ }
+ rcu_read_unlock();
+
+ return result;
+}
+
static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
struct dst_entry **dst, struct flowi6 *fl6)
{
@@ -1191,6 +1218,17 @@ static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
if (err)
goto out_err_release;
+ /* From RFC 4862, "an invalid address MUST NOT be used as the source
+ * address of outbound packets". Exceptions to this must set the ANYSRC
+ * flag (i.e. transparent sockets).
+ */
+ if (!ipv6_addr_any(&fl6->saddr) &&
+ !(fl6->flowi6_flags & FLOWI_FLAG_ANYSRC) &&
+ !ip6_is_local_saddr(net, &fl6->saddr)) {
+ err = -ENETUNREACH;
+ goto out_err_release;
+ }
+
#ifdef CONFIG_IPV6_OPTIMISTIC_DAD
/*
* Here if the dst entry we've looked up
diff --git a/net/ipv6/ip6_udp_tunnel.c b/net/ipv6/ip6_udp_tunnel.c
index 32525a051a6f..1727c36ff6a4 100644
--- a/net/ipv6/ip6_udp_tunnel.c
+++ b/net/ipv6/ip6_udp_tunnel.c
@@ -161,6 +161,7 @@ struct dst_entry *udp_tunnel6_dst_lookup(struct sk_buff *skb,
fl6.fl6_sport = sport;
fl6.fl6_dport = dport;
fl6.flowlabel = ip6_make_flowinfo(dsfield, key->label);
+ fl6.flowi6_flags = key->flow_flags;
dst = ip6_dst_lookup_flow(net, sk, &fl6, NULL);
if (IS_ERR(dst)) {
diff --git a/net/ipv6/netfilter.c b/net/ipv6/netfilter.c
index a7025ec87035..716fc6e246de 100644
--- a/net/ipv6/netfilter.c
+++ b/net/ipv6/netfilter.c
@@ -29,8 +29,9 @@ int ip6_route_me_harder(struct net *net, struct sock *sk_partial, struct sk_buff
struct flow_keys flkeys;
unsigned int hh_len;
struct dst_entry *dst;
- int strict = (ipv6_addr_type(&iph->daddr) &
- (IPV6_ADDR_MULTICAST | IPV6_ADDR_LINKLOCAL));
+ int saddr_type = ipv6_addr_type(&iph->saddr);
+ int daddr_type = ipv6_addr_type(&iph->daddr);
+ int strict = daddr_type & (IPV6_ADDR_MULTICAST | IPV6_ADDR_LINKLOCAL);
struct flowi6 fl6 = {
.flowi6_l3mdev = l3mdev_master_ifindex(dev),
.flowi6_mark = skb->mark,
@@ -41,6 +42,12 @@ int ip6_route_me_harder(struct net *net, struct sock *sk_partial, struct sk_buff
};
int err;
+ fl6.flowi6_flags = sk ? inet_sk_flowi_flags(sk) : 0;
+ if (saddr_type & (IPV6_ADDR_UNICAST | IPV6_ADDR_LINKLOCAL))
+ fl6.flowi6_flags |= FLOWI_FLAG_ANYSRC;
+ else
+ fl6.saddr = in6addr_any;
+
if (sk && sk->sk_bound_dev_if)
fl6.flowi6_oif = sk->sk_bound_dev_if;
else if (strict)
diff --git a/net/ipv6/ping.c b/net/ipv6/ping.c
index 6e90d0bf9f3d..d0964396c2c5 100644
--- a/net/ipv6/ping.c
+++ b/net/ipv6/ping.c
@@ -140,6 +140,7 @@ static int ping_v6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
fl6.flowi6_proto = IPPROTO_ICMPV6;
fl6.saddr = np->saddr;
fl6.daddr = *daddr;
+ fl6.flowi6_flags = inet_sk_flowi_flags(sk);
fl6.flowi6_mark = ipc6.sockc.mark;
fl6.flowi6_uid = sk_uid(sk);
fl6.fl6_icmp_type = user_icmph.icmp6_type;
diff --git a/net/ipv6/raw.c b/net/ipv6/raw.c
index b965258cf9e5..ef5ddbe7ac10 100644
--- a/net/ipv6/raw.c
+++ b/net/ipv6/raw.c
@@ -887,6 +887,7 @@ static int rawv6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
fl6.flowi6_oif = READ_ONCE(np->ucast_oif);
security_sk_classify_flow(sk, flowi6_to_flowi_common(&fl6));
+ fl6.flowi6_flags = inet_sk_flowi_flags(sk);
if (hdrincl)
fl6.flowi6_flags |= FLOWI_FLAG_KNOWN_NH;
diff --git a/net/ipv6/syncookies.c b/net/ipv6/syncookies.c
index b581cb1ee2e8..3d665e93fcac 100644
--- a/net/ipv6/syncookies.c
+++ b/net/ipv6/syncookies.c
@@ -242,6 +242,7 @@ struct sock *cookie_v6_check(struct sock *sk, struct sk_buff *skb)
fl6.flowi6_mark = ireq->ir_mark;
fl6.fl6_dport = ireq->ir_rmt_port;
fl6.fl6_sport = inet_sk(sk)->inet_sport;
+ fl6.flowi6_flags = inet_sk_flowi_flags(sk);
fl6.flowi6_uid = sk_uid(sk);
security_req_classify_flow(req, flowi6_to_flowi_common(&fl6));
diff --git a/net/ipv6/tcp_ipv6.c b/net/ipv6/tcp_ipv6.c
index e006a30360ea..5248cbd4b595 100644
--- a/net/ipv6/tcp_ipv6.c
+++ b/net/ipv6/tcp_ipv6.c
@@ -271,8 +271,9 @@ static int tcp_v6_connect(struct sock *sk, struct sockaddr_unsized *uaddr,
fl6->flowi6_mark = sk->sk_mark;
fl6->fl6_dport = usin->sin6_port;
fl6->fl6_sport = inet->inet_sport;
+ fl6->flowi6_flags = inet_sk_flowi_flags(sk);
if (IS_ENABLED(CONFIG_IP_ROUTE_MULTIPATH) && !fl6->fl6_sport)
- fl6->flowi6_flags = FLOWI_FLAG_ANY_SPORT;
+ fl6->flowi6_flags |= FLOWI_FLAG_ANY_SPORT;
fl6->flowi6_uid = sk_uid(sk);
opt = rcu_dereference_protected(np->opt, lockdep_sock_is_held(sk));
@@ -982,6 +983,10 @@ static void tcp_v6_send_response(const struct sock *sk, struct sk_buff *skb, u32
fl6.fl6_dport = t1->dest;
fl6.fl6_sport = t1->source;
fl6.flowi6_uid = sock_net_uid(net, sk && sk_fullsock(sk) ? sk : NULL);
+ if (sk && sk_fullsock(sk))
+ fl6.flowi6_flags = inet_sk_flowi_flags(sk);
+ else if (sk && inet_sk_transparent(sk))
+ fl6.flowi6_flags = FLOWI_FLAG_ANYSRC;
security_skb_classify_flow(skb, flowi6_to_flowi_common(&fl6));
/* Pass a socket to ip6_dst_lookup either it is for RST
diff --git a/net/ipv6/udp.c b/net/ipv6/udp.c
index 59f9872c3552..7492d155726a 100644
--- a/net/ipv6/udp.c
+++ b/net/ipv6/udp.c
@@ -1650,6 +1650,7 @@ int udpv6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
if (ipv6_addr_any(&fl6->saddr) && !ipv6_addr_any(&np->saddr))
fl6->saddr = np->saddr;
fl6->fl6_sport = inet->inet_sport;
+ fl6->flowi6_flags = inet_sk_flowi_flags(sk);
if (cgroup_bpf_enabled(CGROUP_UDP6_SENDMSG) && !connected) {
err = BPF_CGROUP_RUN_PROG_UDP6_SENDMSG_LOCK(sk,
diff --git a/net/l2tp/l2tp_ip6.c b/net/l2tp/l2tp_ip6.c
index bdaae1b64d25..22ae5bc2669a 100644
--- a/net/l2tp/l2tp_ip6.c
+++ b/net/l2tp/l2tp_ip6.c
@@ -633,6 +633,8 @@ static int l2tp_ip6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
else if (!fl6.flowi6_oif)
fl6.flowi6_oif = READ_ONCE(np->ucast_oif);
+ fl6.flowi6_flags = inet_sk_flowi_flags(sk);
+
security_sk_classify_flow(sk, flowi6_to_flowi_common(&fl6));
fl6.flowlabel = ip6_make_flowinfo(ipc6.tclass, fl6.flowlabel);
--
2.54.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH net v3 3/3] selftests: net: add tests for saddr, anycast and loopback subnets
2026-10-28 19:10 [PATCH net v3 0/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 1/3] net/ipv6: add ipv6_chk_addr_namespace Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
@ 2026-10-28 19:10 ` Íñigo Huguet
2026-10-29 19:12 ` sashiko-bot
2 siblings, 1 reply; 8+ messages in thread
From: Íñigo Huguet @ 2026-10-28 19:10 UTC (permalink / raw)
To: Alexei Starovoitov, Andrew Lunn, Andrii Nakryiko, Daniel Borkmann,
David Ahern, David S. Miller, Eduard Zingerman, Eric Dumazet,
Florian Westphal, Ido Schimmel, Jakub Kicinski,
Kumar Kartikeya Dwivedi, Neal Cardwell, Pablo Neira Ayuso,
Paolo Abeni, Shuah Khan, Willem de Bruijn
Cc: Íñigo Huguet, bpf, Breno Leitao, coreteam,
Emil Tsalapatis, Fernando Fernandez Mancera, Ihor Solodrai,
Jeff Layton, Jiri Olsa, Joe Damato, John Fastabend, Junseo Lim,
Kees Cook, Kuniyuki Iwashima, Leon Hwang, Li RongQing,
linux-kselftest, Martin KaFai Lau, netdev, netfilter-devel,
Phil Sutter, Qi Tang, Sebastian Andrzej Siewior, Simon Horman,
Song Liu, Stanislav Fomichev, Willem de Bruijn, Xuanqiang Luo,
Yonghong Song, Íñigo Huguet, Davide Caratti
Add selftests covering the "check source address in outgoing packets"
behaviour covered by the previous patch. Add tests for IPv4 too, because
although this is not a new behaviour, there were no selftests for it.
Also, add selftests covering anycast and loopback subnets. During the
development of the previous patch these features were impacted and
detected at review time. Add selftests for them to avoid new
regressions.
The *_table tests in loopback_subnet.sh are marked as xfail because
currently loopback subnets are only working fine in the local table.
As this is unrelated to the current main patch, I plan to address it in
a different patch series.
Signed-off-by: Íñigo Huguet <ihuguet@riseup.net>
---
tools/testing/selftests/net/.gitignore | 1 +
tools/testing/selftests/net/Makefile | 4 +
tools/testing/selftests/net/ip_saddr.c | 557 +++++++++++++++++
tools/testing/selftests/net/ip_saddr.sh | 575 ++++++++++++++++++
tools/testing/selftests/net/ipv6_anycast.sh | 68 +++
.../testing/selftests/net/loopback_subnet.sh | 135 ++++
6 files changed, 1340 insertions(+)
create mode 100644 tools/testing/selftests/net/ip_saddr.c
create mode 100755 tools/testing/selftests/net/ip_saddr.sh
create mode 100755 tools/testing/selftests/net/ipv6_anycast.sh
create mode 100755 tools/testing/selftests/net/loopback_subnet.sh
diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore
index 21412510ac26..eceaac7cb8cf 100644
--- a/tools/testing/selftests/net/.gitignore
+++ b/tools/testing/selftests/net/.gitignore
@@ -13,6 +13,7 @@ io_uring_zerocopy_tx
ioam6_parser
ip_defrag
ip_local_port_range
+ip_saddr
ipsec
ipv6_flowlabel
ipv6_flowlabel_mgr
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index df215e1a6262..08d0fae1778d 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -56,6 +56,8 @@ TEST_PROGS := \
ip6_tunnel.sh \
ip_defrag.sh \
ip_local_port_range.sh \
+ ip_saddr.sh \
+ ipv6_anycast.sh \
ipv6_flowlabel.sh \
ipv6_force_forwarding.sh \
ipv6_route_update_soft_lockup.sh \
@@ -63,6 +65,7 @@ TEST_PROGS := \
l2_tos_ttl_inherit.sh \
l2tp.sh \
link_netns.py \
+ loopback_subnet.sh \
lwt_dst_cache_ref_loop.sh \
macvlan_mcast_shared_mac.sh \
msg_zerocopy.sh \
@@ -153,6 +156,7 @@ TEST_GEN_FILES := \
ioam6_parser \
ip_defrag \
ip_local_port_range \
+ ip_saddr \
ipsec \
ipv6_flowlabel \
msg_zerocopy \
diff --git a/tools/testing/selftests/net/ip_saddr.c b/tools/testing/selftests/net/ip_saddr.c
new file mode 100644
index 000000000000..ed9b0a360b28
--- /dev/null
+++ b/tools/testing/selftests/net/ip_saddr.c
@@ -0,0 +1,557 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+
+#define MESSAGE "Hello!\n"
+
+union addr46 {
+ struct in_addr addr4;
+ struct in6_addr addr6;
+};
+
+/* -----------------------------------------------
+ * Functions for the TCP and UDP tests:
+ * - Create a STREAM or DGRAM socket, optionally with the TRANSPARENT sockopt
+ * - Act only as server, the companion script will act as client
+ * - Wait for a TCP connection or UDP datagram from the client
+ * - Wait until the client sends a sync message (via FIFO file) to continue
+ * (the client may need to do some actions before the next step)
+ * - Send a message to the client
+ * ------------------------------------------------
+ */
+static int create_sock_ipv6(int proto, int port, bool transparent)
+{
+ struct sockaddr_in6 address;
+ int sk, opt;
+
+ sk = socket(AF_INET6, proto, 0);
+ if (sk < 0) {
+ perror("Failed to create socket");
+ exit(EXIT_FAILURE);
+ }
+
+ opt = 1;
+ if (setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
+ perror("Failed to set SO_REUSEADDR");
+ exit(EXIT_FAILURE);
+ }
+ opt = 0;
+ if (setsockopt(sk, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt))) {
+ perror("Failed to set IPV6_V6ONLY");
+ exit(EXIT_FAILURE);
+ }
+ opt = 1;
+ if (setsockopt(sk, IPPROTO_IPV6, IPV6_RECVPKTINFO, &opt, sizeof(opt))) {
+ perror("Failed to set IPV6_PKTINFO");
+ exit(EXIT_FAILURE);
+ }
+ opt = 1;
+ if (transparent &&
+ setsockopt(sk, IPPROTO_IPV6, IPV6_TRANSPARENT, &opt, sizeof(opt))) {
+ perror("Failed to set IPV6_TRANSPARENT");
+ exit(EXIT_FAILURE);
+ }
+
+ memset(&address, 0, sizeof(address));
+ address.sin6_family = AF_INET6;
+ address.sin6_addr = in6addr_any;
+ address.sin6_port = htons(port);
+
+ if (bind(sk, (struct sockaddr *)&address, sizeof(address))) {
+ perror("Failed to bind");
+ exit(EXIT_FAILURE);
+ }
+
+ return sk;
+}
+
+static int create_sock_ipv4(int proto, int port, bool transparent)
+{
+ struct sockaddr_in address;
+ int sk, opt;
+
+ sk = socket(AF_INET, proto, 0);
+ if (sk < 0) {
+ perror("Failed to create socket");
+ exit(EXIT_FAILURE);
+ }
+
+ opt = 1;
+ if (setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
+ perror("Failed to set SO_REUSEADDR");
+ exit(EXIT_FAILURE);
+ }
+ opt = 1;
+ if (setsockopt(sk, IPPROTO_IP, IP_PKTINFO, &opt, sizeof(opt))) {
+ perror("Failed to set IP_PKTINFO");
+ exit(EXIT_FAILURE);
+ }
+ opt = 1;
+ if (transparent &&
+ setsockopt(sk, IPPROTO_IP, IP_TRANSPARENT, &opt, sizeof(opt))) {
+ perror("Failed to set IP_TRANSPARENT");
+ exit(EXIT_FAILURE);
+ }
+
+ memset(&address, 0, sizeof(address));
+ address.sin_family = AF_INET;
+ address.sin_addr.s_addr = htonl(INADDR_ANY);
+ address.sin_port = htons(port);
+
+ if (bind(sk, (struct sockaddr *)&address, sizeof(address))) {
+ perror("Failed to bind");
+ exit(EXIT_FAILURE);
+ }
+
+ return sk;
+}
+
+static void sync_wait(int sync_fd)
+{
+ uint8_t buff[256];
+
+ if (sync_fd && read(sync_fd, buff, sizeof(buff) - 1) < 0) {
+ perror("Failed to read from sync file");
+ exit(EXIT_FAILURE);
+ }
+}
+
+static void wait_and_send_tcp(int sk, int sync_fd)
+{
+ int con;
+
+ if (listen(sk, 5) < 0) {
+ perror("Failed to listen");
+ exit(EXIT_FAILURE);
+ }
+ con = accept(sk, NULL, NULL);
+ if (con < 0) {
+ perror("Failed to accept");
+ exit(EXIT_FAILURE);
+ }
+
+ sync_wait(sync_fd);
+
+ if (send(con, MESSAGE, strlen(MESSAGE), 0) < 0) {
+ perror("Failed to send");
+ exit(EXIT_FAILURE);
+ }
+
+ close(con);
+}
+
+static void maybe_chg_saddr_dport(char *data, struct msghdr *msg)
+{
+ struct in6_addr local_addr6;
+ struct in_addr local_addr4;
+ int cmsg_level, cmsg_type;
+ struct cmsghdr *cmsg;
+ size_t datalen;
+ char *portstr;
+
+ portstr = strchr(data, ' ');
+ if (!portstr)
+ return;
+
+ // remove trailing LF and split into 2 strs
+ datalen = strlen(data);
+ if (datalen > 0 && data[datalen - 1] == '\n')
+ data[datalen - 1] = '\0';
+ *(portstr++) = '\0';
+
+ // get the new saddr and dport
+ if (inet_pton(AF_INET, data, &local_addr4) == 1) {
+ struct sockaddr_in *peer_addr;
+
+ peer_addr = (struct sockaddr_in *)msg->msg_name;
+ peer_addr->sin_port = htons(atoi(portstr));
+ cmsg_level = IPPROTO_IP;
+ cmsg_type = IP_PKTINFO;
+ } else if (inet_pton(AF_INET6, data, &local_addr6) == 1) {
+ struct sockaddr_in6 *peer_addr;
+
+ peer_addr = (struct sockaddr_in6 *)msg->msg_name;
+ peer_addr->sin6_port = htons(atoi(portstr));
+ cmsg_level = IPPROTO_IPV6;
+ cmsg_type = IPV6_PKTINFO;
+ } else {
+ return;
+ }
+
+ for (cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) {
+ if (cmsg->cmsg_level != cmsg_level ||
+ cmsg->cmsg_type != cmsg_type) {
+ continue;
+ }
+ if (cmsg->cmsg_level == IPPROTO_IP &&
+ cmsg->cmsg_type == IP_PKTINFO) {
+ struct in_pktinfo *pi;
+
+ pi = (struct in_pktinfo *)CMSG_DATA(cmsg);
+ pi->ipi_addr = local_addr4;
+ pi->ipi_spec_dst = local_addr4;
+ break;
+ }
+ if (cmsg->cmsg_level == IPPROTO_IPV6 &&
+ cmsg->cmsg_type == IPV6_PKTINFO) {
+ struct in6_pktinfo *pi;
+
+ pi = (struct in6_pktinfo *)CMSG_DATA(cmsg);
+ pi->ipi6_addr = local_addr6;
+ break;
+ }
+ }
+}
+
+static void wait_and_send_udp(int sk, int sync_fd)
+{
+ uint8_t data_buff[256], ctrl_buff[256];
+ ssize_t nbytes;
+ union {
+ struct sockaddr_in addr4;
+ struct sockaddr_in6 addr6;
+ } peer_addr;
+ struct iovec iov = {
+ .iov_base = data_buff,
+ .iov_len = sizeof(data_buff) - 1,
+ };
+ struct msghdr msg = {
+ .msg_name = &peer_addr,
+ .msg_namelen = sizeof(peer_addr),
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = ctrl_buff,
+ .msg_controllen = sizeof(ctrl_buff),
+ };
+
+ nbytes = recvmsg(sk, &msg, 0);
+ if (nbytes < 0) {
+ perror("Failed to receive");
+ exit(EXIT_FAILURE);
+ }
+
+ // The "test_foreign_addr_*" tests needs us to reply to the same address
+ // but different port, and also from a different local address. The new
+ // saddr and dport are received in the previous message with the format
+ // "new_source_address new_dest_port"
+ data_buff[nbytes] = '\0';
+ maybe_chg_saddr_dport((char *)data_buff, &msg);
+
+ sync_wait(sync_fd);
+
+ // Change the data, but keep the same ancillary data so that we use the
+ // same addresses as we received.
+ iov.iov_base = MESSAGE;
+ iov.iov_len = strlen(MESSAGE);
+
+ if (sendmsg(sk, &msg, 0) < 0) {
+ /* Contrary to the TCP case, as UDP doesn't keep a "connection",
+ * the test cases where the source address is not configured in
+ * the host return an error here. It is expected, ignore it.
+ */
+ }
+}
+
+/* -----------------------------------------------
+ * Functions for the HDRINCL tests:
+ * - Create a RAW socket with the HDRINCL sockopt
+ * - If acting as server, wait for incoming data and print it
+ * - If acting as client, craft a packet with the saddr and daddr from the args
+ * and send it.
+ * ------------------------------------------------
+ */
+static int create_sock_ipv4_hdrincl(void)
+{
+ struct sockaddr_in address;
+ int sk, opt;
+
+ sk = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
+ if (sk < 0) {
+ perror("Failed to create socket");
+ exit(EXIT_FAILURE);
+ }
+
+ opt = 1;
+ if (setsockopt(sk, IPPROTO_IP, IP_HDRINCL, &opt, sizeof(opt)) < 0) {
+ perror("Failed to set IP_HDRINCL");
+ exit(EXIT_FAILURE);
+ }
+
+ memset(&address, 0, sizeof(address));
+ address.sin_family = AF_INET;
+ address.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ if (bind(sk, (struct sockaddr *)&address, sizeof(address))) {
+ perror("Failed to bind");
+ exit(EXIT_FAILURE);
+ }
+
+ return sk;
+}
+
+static int create_sock_ipv6_hdrincl(void)
+{
+ struct sockaddr_in6 address;
+ int sk, opt;
+
+ sk = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW);
+ if (sk < 0) {
+ perror("Failed to create socket");
+ exit(EXIT_FAILURE);
+ }
+
+ opt = 1;
+ if (setsockopt(sk, IPPROTO_IPV6, IPV6_HDRINCL, &opt, sizeof(opt)) < 0) {
+ perror("Failed to set IPV6_HDRINCL");
+ exit(EXIT_FAILURE);
+ }
+
+ memset(&address, 0, sizeof(address));
+ address.sin6_family = AF_INET6;
+ address.sin6_addr = in6addr_any;
+
+ if (bind(sk, (struct sockaddr *)&address, sizeof(address))) {
+ perror("Failed to bind");
+ exit(EXIT_FAILURE);
+ }
+
+ return sk;
+}
+
+size_t create_packet_ipv6(uint8_t *buff,
+ struct in6_addr saddr,
+ struct in6_addr daddr)
+{
+ struct ip6_hdr *iph = (struct ip6_hdr *)buff;
+ uint8_t *data = (uint8_t *)(buff + sizeof(struct ip6_hdr));
+
+ // header
+ iph->ip6_flow = htonl((6 << 28) | (0 << 20) | 0);
+ iph->ip6_plen = htons(sizeof(MESSAGE));
+ iph->ip6_nxt = IPPROTO_RAW;
+ iph->ip6_hops = 64;
+ iph->ip6_src = saddr;
+ iph->ip6_dst = daddr;
+
+ // data
+ memcpy(data, MESSAGE, sizeof(MESSAGE));
+
+ return sizeof(struct ip6_hdr) + sizeof(MESSAGE);
+}
+
+size_t create_packet_ipv4(uint8_t *buff,
+ struct in_addr saddr,
+ struct in_addr daddr)
+{
+ struct ip *iph = (struct ip *)buff;
+ uint8_t *data = (uint8_t *)(buff + sizeof(struct ip));
+
+ // header
+ iph->ip_hl = 5; // Header length (5 * 4 bytes = 20 bytes)
+ iph->ip_v = 4; // IPv4
+ iph->ip_tos = 0;
+ iph->ip_len = htons(sizeof(struct ip) + sizeof(MESSAGE));
+ iph->ip_id = htons(12345);
+ iph->ip_off = 0;
+ iph->ip_ttl = 64;
+ iph->ip_p = IPPROTO_RAW;
+ iph->ip_src = saddr;
+ iph->ip_dst = daddr;
+ iph->ip_sum = 0;
+
+ // data
+ memcpy(data, MESSAGE, sizeof(MESSAGE));
+
+ return sizeof(struct ip) + sizeof(MESSAGE);
+}
+
+static void send_or_receive_hdrincl(int sk,
+ bool is_ipv6,
+ bool send_msg,
+ union addr46 *local_addr,
+ union addr46 *peer_addr)
+{
+ uint8_t buff[256];
+ ssize_t msglen;
+
+ if (send_msg && is_ipv6) {
+ struct sockaddr_in6 dest;
+
+ memset(&dest, 0, sizeof(dest));
+ dest.sin6_family = AF_INET6;
+ dest.sin6_addr = peer_addr->addr6;
+ msglen = create_packet_ipv6(buff,
+ local_addr->addr6,
+ peer_addr->addr6);
+ if (sendto(sk, buff, msglen, 0, &dest, sizeof(dest)) < 0) {
+ perror("Failed to send");
+ exit(EXIT_FAILURE);
+ }
+ } else if (send_msg && !is_ipv6) {
+ struct sockaddr_in dest;
+
+ memset(&dest, 0, sizeof(dest));
+ dest.sin_family = AF_INET;
+ dest.sin_addr = peer_addr->addr4;
+ msglen = create_packet_ipv4(buff,
+ local_addr->addr4,
+ peer_addr->addr4);
+ if (sendto(sk, buff, msglen, 0, &dest, sizeof(dest)) < 0) {
+ perror("Failed to send");
+ exit(EXIT_FAILURE);
+ }
+ } else {
+ msglen = read(sk, buff, sizeof(buff) - 1);
+ if (msglen < 0) {
+ perror("Failed to read");
+ exit(EXIT_FAILURE);
+ }
+ buff[msglen] = '\0';
+
+ // output the received message
+ if (is_ipv6)
+ printf("%s", buff);
+ else if (msglen >= sizeof(struct ip))
+ printf("%s", buff + sizeof(struct ip));
+ }
+}
+
+/* -----------------------------------------------
+ * Arguments parsing
+ * ------------------------------------------------
+ */
+static void parse_gral_args(int argc,
+ char *argv[],
+ bool *out_is_ipv6,
+ int *out_proto)
+{
+ if (argc < 3) {
+ fprintf(stderr, "Expected more arguments");
+ exit(EXIT_FAILURE);
+ }
+
+ if (strncmp(argv[1], "ipv4", strlen("ipv4")) == 0) {
+ *out_is_ipv6 = false;
+ } else if (strncmp(argv[1], "ipv6", strlen("ipv6")) == 0) {
+ *out_is_ipv6 = true;
+ } else {
+ fprintf(stderr, "Valid value for arg1: ipv4, ipv6");
+ exit(EXIT_FAILURE);
+ }
+
+ if (strncmp(argv[2], "tcp", strlen("tcp")) == 0) {
+ *out_proto = SOCK_STREAM;
+ } else if (strncmp(argv[2], "udp", strlen("udp")) == 0) {
+ *out_proto = SOCK_DGRAM;
+ } else if (strncmp(argv[2], "raw", strlen("raw")) == 0) {
+ *out_proto = SOCK_RAW;
+ } else {
+ fprintf(stderr, "Valid value for arg2: tcp, udp, raw");
+ exit(EXIT_FAILURE);
+ }
+}
+
+static void parse_tcp_udp_args(int argc,
+ char *argv[],
+ int *out_port,
+ bool *out_transparent,
+ int *out_sync_fd)
+{
+ if (argc < 5) {
+ fprintf(stderr, "Expected more arguments");
+ exit(EXIT_FAILURE);
+ }
+
+ *out_port = atoi(argv[3]);
+
+ if (strncmp(argv[4], "transparent", strlen("transparent")) == 0) {
+ *out_transparent = true;
+ } else if (strncmp(argv[4], "normal", strlen("normal")) == 0) {
+ *out_transparent = false;
+ } else {
+ fprintf(stderr, "Valid values for arg3: normal, transparent");
+ exit(EXIT_FAILURE);
+ }
+
+ *out_sync_fd = 0;
+ if (argc > 5 && strlen(argv[5]) > 0) {
+ *out_sync_fd = open(argv[5], O_RDWR);
+ if (*out_sync_fd < 0) {
+ perror("Failed to open sync file");
+ exit(EXIT_FAILURE);
+ }
+ }
+}
+
+static void parse_hdrincl_args(int argc,
+ char *argv[],
+ bool is_ipv6,
+ bool *out_send_msg,
+ union addr46 *out_local_addr,
+ union addr46 *out_peer_addr)
+{
+ if (argc == 4) {
+ fprintf(stderr, "Expected more arguments");
+ exit(EXIT_FAILURE);
+ }
+
+ *out_send_msg = argc < 4 ? false : true;
+ if (*out_send_msg && is_ipv6) {
+ inet_pton(AF_INET6, argv[3], out_local_addr);
+ inet_pton(AF_INET6, argv[4], out_peer_addr);
+ } else if (*out_send_msg && !is_ipv6) {
+ inet_pton(AF_INET, argv[3], out_local_addr);
+ inet_pton(AF_INET, argv[4], out_peer_addr);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ union addr46 local_addr, peer_addr;
+ bool is_ipv6, transparent, send_msg;
+ int proto, port;
+ int sync_fd;
+ int sk;
+
+ parse_gral_args(argc, argv, &is_ipv6, &proto);
+ if (proto != SOCK_RAW)
+ parse_tcp_udp_args(argc, argv, &port, &transparent, &sync_fd);
+ else
+ parse_hdrincl_args(argc,
+ argv,
+ is_ipv6,
+ &send_msg,
+ &local_addr,
+ &peer_addr);
+
+ if (is_ipv6 && proto != SOCK_RAW)
+ sk = create_sock_ipv6(proto, port, transparent);
+ else if (!is_ipv6 && proto != SOCK_RAW)
+ sk = create_sock_ipv4(proto, port, transparent);
+ else if (is_ipv6 && proto == SOCK_RAW)
+ sk = create_sock_ipv6_hdrincl();
+ else // !is_ipv6 && proto == SOCK_RAW
+ sk = create_sock_ipv4_hdrincl();
+
+ if (proto == SOCK_STREAM)
+ wait_and_send_tcp(sk, sync_fd);
+ else if (proto == SOCK_DGRAM)
+ wait_and_send_udp(sk, sync_fd);
+ else // SOCK_RAW
+ send_or_receive_hdrincl(sk,
+ is_ipv6,
+ send_msg,
+ &local_addr,
+ &peer_addr);
+
+ close(sk);
+ return 0;
+}
diff --git a/tools/testing/selftests/net/ip_saddr.sh b/tools/testing/selftests/net/ip_saddr.sh
new file mode 100755
index 000000000000..9345b743afd0
--- /dev/null
+++ b/tools/testing/selftests/net/ip_saddr.sh
@@ -0,0 +1,575 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+# Test the correct behaviour with outgoing packets regarding the source address.
+# RFC 4862: an invalid address MUST NOT be used as the source address of
+# outbound packets.
+# However, with the TRANSPARENT and HDRINCL sockopts, any saddr is allowed.
+#
+# There are 2 kind of tests, all server & client in different namespaces:
+# - TCP / UDP:
+# 1. The client establish a connection with the server
+# 2. The server address is removed/invalidated
+# 3. The server tries to send a message to the client
+# - HDRINCL (using raw socket):
+# 1. The server start listening
+# 2. The client address is removed/invalidated
+# 3. The client tries to send a message to the server
+#
+# The expectation is that the message is not received by the peer, except when
+# using TRANSPARENT or HDRINCL where we expect to receive it.
+#
+# Test the following permutations:
+# - IPv4 / IPv6
+# - TCP / UDP / raw-with-HDRINCL
+# - For TCP & UDP: normal socket / TRANSPARENT socket
+# - One of the following address invalidation actions:
+# - Don't invalidate (keep the address configured). In this case we expect
+# to always receive the message.
+# - Manually delete the address.
+# - Leave the address to expire its valid_lft.
+# - Use a foreign address (an address that it's not configured on the host).
+# This one can't be tested with TCP, the connection cannot be established.
+#
+# Important: we have a secondary address on the same network configured all the
+# time in both the client and the server. We do this because, otherwise, the
+# kernel doesn't know a route when we invalidate/remove the main address. If it
+# failed because of this, we wouldn't be testing the validation of the source
+# address, which is this file's goal
+
+# shellcheck disable=SC2329
+
+ALL_TESTS="
+ test_keep_addr_ipv4_tcp_normal
+ test_keep_addr_ipv4_udp_normal
+ test_keep_addr_ipv6_tcp_normal
+ test_keep_addr_ipv6_udp_normal
+ test_keep_addr_ipv4_tcp_transparent
+ test_keep_addr_ipv4_udp_transparent
+ test_keep_addr_ipv6_tcp_transparent
+ test_keep_addr_ipv6_udp_transparent
+ test_keep_addr_ipv4_hdrincl
+ test_keep_addr_ipv6_hdrincl
+ test_remove_addr_ipv4_tcp_normal
+ test_remove_addr_ipv4_udp_normal
+ test_remove_addr_ipv6_tcp_normal
+ test_remove_addr_ipv6_udp_normal
+ test_remove_addr_ipv4_tcp_transparent
+ test_remove_addr_ipv4_udp_transparent
+ test_remove_addr_ipv6_tcp_transparent
+ test_remove_addr_ipv6_udp_transparent
+ test_remove_addr_ipv4_hdrincl
+ test_remove_addr_ipv6_hdrincl
+ test_expire_addr_ipv4_tcp_normal
+ test_expire_addr_ipv4_udp_normal
+ test_expire_addr_ipv6_tcp_normal
+ test_expire_addr_ipv6_udp_normal
+ test_expire_addr_ipv4_tcp_transparent
+ test_expire_addr_ipv4_udp_transparent
+ test_expire_addr_ipv6_tcp_transparent
+ test_expire_addr_ipv6_udp_transparent
+ test_expire_addr_ipv4_hdrincl
+ test_expire_addr_ipv6_hdrincl
+ test_foreign_addr_ipv4_udp_normal
+ test_foreign_addr_ipv6_udp_normal
+ test_foreign_addr_ipv4_udp_transparent
+ test_foreign_addr_ipv6_udp_transparent
+ test_foreign_addr_ipv4_hdrincl
+ test_foreign_addr_ipv6_hdrincl
+"
+
+source lib.sh
+
+SRV_ADDR4=172.16.1.1/16
+SRV_ADDR6=2001:db8:1::1/64
+CLI_ADDR4=172.16.1.2/16
+CLI_ADDR6=2001:db8:1::2/64
+SENDER_ADDR4=172.16.1.3/16
+SENDER_ADDR6=2001:db8:1::3/64
+FOREIGN_ADDR4=172.16.1.4/16
+FOREIGN_ADDR6=2001:db8:1::4/64
+PORT=12345
+
+##########################
+# Global setup & cleanup
+##########################
+
+cleanup() {
+ cleanup_ns "$ns_srv" "$ns_cli"
+ rm -rf "$SRV_SYNC_DIR"
+}
+
+setup() {
+ setup_ns ns_srv ns_cli
+ ip -n $ns_srv link add eth0 netns $ns_cli type veth peer eth0
+
+ ip -n $ns_cli link set lo up
+ ip -n $ns_cli link set eth0 up
+ ip -n $ns_cli addr add "$CLI_ADDR4" dev eth0
+ ip -n $ns_cli addr add "$CLI_ADDR6" dev eth0 nodad
+
+ ip -n $ns_srv link set lo up
+ ip -n $ns_srv link set eth0 up
+ ip -n $ns_srv addr add "$SRV_ADDR4" dev eth0
+ ip -n $ns_srv addr add "$SRV_ADDR6" dev eth0 nodad
+
+ SRV_SYNC_DIR="$(mktemp -d)"
+ SRV_SYNC_FILE="$SRV_SYNC_DIR/sync_fifo"
+ mkfifo "$SRV_SYNC_FILE"
+}
+
+#################################
+# Tests setup, cleanup & helpers
+#################################
+
+setup_test_tcp_udp() {
+ local ipvx="$1"
+ local proto="$2"
+ local sock_type="$3"
+ local sync_file="$4"
+ local nodad addr_args
+ CLOSE_FD3=
+ RET=0
+
+ if [ "$sock_type" = "hdrincl" ]; then
+ RET=$ksft_fail
+ return 1
+ fi
+
+ shift 4
+ [ "$ipvx" = "ipv6" ] && nodad="nodad"
+ [ "$#" -gt 0 ] && addr_args="$*"
+
+ if [ "$ipvx" = "ipv6" ]; then
+ SENDER_ADDR="$SENDER_ADDR6"
+ FOREIGN_ADDR="$FOREIGN_ADDR6"
+ elif [ "$ipvx" = "ipv4" ]; then
+ SENDER_ADDR="$SENDER_ADDR4"
+ FOREIGN_ADDR="$FOREIGN_ADDR4"
+ else
+ RET=$ksft_fail
+ return 1
+ fi
+
+ # start the C helper server and wait until it's listening
+ ip netns exec $ns_srv \
+ ./ip_saddr "$ipvx" "$proto" "$PORT" "$sock_type" "$sync_file" &
+ SRV_PID=$!
+ wait_local_port_listen "$ns_srv" "$PORT" "$proto"
+
+ # the message is sent from the server, configure SENDER_ADDR on it
+ ip -n $ns_srv addr add "$SENDER_ADDR" dev eth0 $nodad $addr_args
+}
+
+setup_test_hdrincl() {
+ local ipvx="$1"
+ local sock_type="$2"
+ local nodad addr_args
+ RET=0
+
+ if [ "$sock_type" != "hdrincl" ]; then
+ RET=$ksft_fail
+ return 1
+ fi
+
+ shift 2
+ [ "$ipvx" = "ipv6" ] && nodad="nodad"
+ [ "$#" -gt 0 ] && addr_args="$*"
+
+ if [ "$ipvx" = "ipv6" ]; then
+ SENDER_ADDR="$SENDER_ADDR6"
+ FOREIGN_ADDR="$FOREIGN_ADDR6"
+ SRV_ADDR="$SRV_ADDR6"
+ elif [ "$ipvx" = "ipv4" ]; then
+ SENDER_ADDR="$SENDER_ADDR4"
+ FOREIGN_ADDR="$FOREIGN_ADDR4"
+ SRV_ADDR="$SRV_ADDR4"
+ else
+ RET=$ksft_fail
+ return 1
+ fi
+
+ # the message is sent from the client, configure SENDER_ADDR on it
+ ip -n $ns_cli addr add "$SENDER_ADDR" dev eth0 $nodad $addr_args
+
+ # start the C helper server and wait until it's listening
+ exec 3< <(exec ip netns exec $ns_srv ./ip_saddr "$ipvx" "raw")
+ SRV_PID=$!
+ CLOSE_FD3=yes
+ wait_raw_sock_listen_any_addr "$ipvx" "$ns_srv"
+}
+
+set_test_result() {
+ local sock_type="$1"
+ local normal_expect_empty="$2"
+ local srv_msg="$3"
+
+ if [ "$normal_expect_empty" = "yes" ] && [ "$sock_type" = "normal" ]
+ then
+ [ "$srv_msg" != "" ] && RET=$ksft_fail
+ else
+ [ "$srv_msg" = "" ] && RET=$ksft_fail
+ fi
+}
+
+cleanup_test() {
+ kill "$SRV_PID" &>/dev/null
+ wait "$SRV_PID"
+ ip -n $ns_srv addr del "$SENDER_ADDR" dev eth0 &>/dev/null
+ ip -n $ns_cli addr del "$SENDER_ADDR" dev eth0 &>/dev/null
+ [ "$CLOSE_FD3" = "yes" ] && exec 3<&-
+}
+
+wait_raw_sock_listen_any_addr() {
+ local ipvx="$1"
+ local ns="$2"
+ local ptrn proto found i
+
+ if [ "$ipvx" = "ipv4" ]; then
+ ptrn='^00000000:'
+ proto=raw
+ elif [ "$ipvx" = "ipv6" ]; then
+ ptrn='^00000000000000000000000000000000:'
+ proto=raw6
+ else
+ RET=$ksft_fail
+ return 1
+ fi
+ for i in $(seq 10); do
+ found="$(ip netns exec "$ns" cat "/proc/net/$proto" | \
+ awk -vptrn="$ptrn" '$2 ~ ptrn {print "yes"}')"
+ [ "$found" = "yes" ] && return 0
+ sleep 0.1
+ done
+ RET=$ksft_fail
+ return 1
+}
+
+wait_addr_missing() {
+ local ns="$1"
+ local iface="$2"
+ local addr="$3"
+ local timeout="${4:-1}"
+
+ for i in $(seq $((timeout * 10))); do
+ ip -n "$ns" -br a show dev "$iface" | grep -q "$addr" || return 0
+ sleep 0.1
+ done
+ RET=$ksft_fail
+ return 1
+}
+
+##########################
+# Test cases
+##########################
+
+# Open a TCP/UDP connection and wait for a message from the server. The address
+# is still configured, so the expectation is to always receive the message.
+test_keep_addr_tcp_udp() {
+ local test_name="$1"
+ local ipvx="$2"
+ local proto="$3"
+ local sock_type="$4"
+ local srv_msg
+
+ setup_test_tcp_udp "$ipvx" "$proto" "$sock_type" ""
+ srv_msg="$(ip netns exec $ns_cli bash -c "
+ exec 3<>'/dev/$proto/${SENDER_ADDR%/*}/$PORT'
+ [ '$proto' = 'udp' ] && echo 'x' >&3
+ timeout 2s cat <&3
+ exec 3<&-
+ ")"
+ set_test_result "$sock_type" "no" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+# Send a message to the server through a raw socket with HDRINCL.
+test_keep_addr_hdrincl() {
+ local test_name="$1"
+ local ipvx="$2"
+ local srv_msg
+
+ setup_test_hdrincl "$ipvx" "hdrincl"
+ ip netns exec $ns_cli \
+ ./ip_saddr "$ipvx" "raw" "${SENDER_ADDR%/*}" "${SRV_ADDR%/*}"
+ srv_msg="$(timeout 2s cat <&3)"
+ set_test_result "hdrincl" "" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+# Open a TCP/UDP connection to the server, remove the address from the server
+# and wait for a message from it. As the address is no longer present, the
+# expectation is not to receive the message, except for transparent sockets.
+test_remove_addr_tcp_udp() {
+ local test_name="$1"
+ local ipvx="$2"
+ local proto="$3"
+ local sock_type="$4"
+ local srv_msg
+
+ setup_test_tcp_udp "$ipvx" "$proto" "$sock_type" "$SRV_SYNC_FILE"
+ srv_msg="$(ip netns exec $ns_cli bash -c "
+ exec 3<>'/dev/$proto/${SENDER_ADDR%/*}/$PORT'
+ [ '$proto' = 'udp' ] && echo 'x' >&3
+ ip -n $ns_srv addr del '$SENDER_ADDR' dev eth0
+ wait_addr_missing '$ns_srv' eth0 '$SENDER_ADDR'
+ echo 'x' > '$SRV_SYNC_FILE'
+ timeout 2s cat <&3
+ exec 3<&-
+ ")"
+ set_test_result "$sock_type" "yes" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+# Start listening by the server, remove the address from the client and send a
+# message to the server through a raw socket with HDRINCL. Because of the
+# HDRINCL, the expectation is to receive the message despite the removed address
+test_remove_addr_hdrincl() {
+ local test_name="$1"
+ local ipvx="$2"
+ local srv_msg
+
+ setup_test_hdrincl "$ipvx" "hdrincl"
+ ip -n $ns_cli addr del "$SENDER_ADDR" dev eth0
+ wait_addr_missing "$ns_cli" eth0 "$SENDER_ADDR"
+ ip netns exec $ns_cli \
+ ./ip_saddr "$ipvx" "raw" "${SENDER_ADDR%/*}" "${SRV_ADDR%/*}"
+ srv_msg="$(timeout 2s cat <&3)"
+ set_test_result "hdrincl" "" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+# Open a TCP/UDP connection to the server, wait until its address expires and
+# wait for a message from it. As the address is no longer present, the
+# expectation is not to receive the message, except for transparent sockets.
+test_expire_addr_tcp_udp() {
+ local test_name="$1"
+ local ipvx="$2"
+ local proto="$3"
+ local sock_type="$4"
+ local srv_msg
+
+ setup_test_tcp_udp "$ipvx" "$proto" "$sock_type" "$SRV_SYNC_FILE" preferred_lft 1 valid_lft 1
+ srv_msg="$(ip netns exec $ns_cli bash -c "
+ exec 3<>'/dev/$proto/${SENDER_ADDR%/*}/$PORT'
+ [ '$proto' = 'udp' ] && echo 'x' >&3
+ wait_addr_missing '$ns_srv' eth0 '$SENDER_ADDR' 5
+ echo 'x' > '$SRV_SYNC_FILE'
+ timeout 2s cat <&3
+ exec 3<&-
+ ")"
+ set_test_result "$sock_type" "yes" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+# Start listening by the server, wait until the client's address expires and
+# send a message to the server through a raw socket with HDRINCL. Because of the
+# HDRINCL, the expectation is to receive the message despite the expired address
+test_expire_addr_hdrincl() {
+ local test_name="$1"
+ local ipvx="$2"
+ local srv_msg
+
+ setup_test_hdrincl "$ipvx" "hdrincl" preferred_lft 1 valid_lft 1
+ wait_addr_missing "$ns_cli" eth0 "$SENDER_ADDR" 5
+ ip netns exec $ns_cli \
+ ./ip_saddr "$ipvx" "raw" "${SENDER_ADDR%/*}" "${SRV_ADDR%/*}"
+ srv_msg="$(timeout 2s cat <&3)"
+ set_test_result "hdrincl" "" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+# Try to receive an UDP message from the server using a non-existent source
+# address. The expectation is not to receive the message except with transparent
+# sockets. This cannot be tested with TCP because the incoming packets are
+# dropped, so the TCP connection cannot be established.
+test_foreign_addr_udp() {
+ local test_name="$1"
+ local ipvx="$2"
+ local sock_type="$3"
+ local srv_msg
+
+ setup_test_tcp_udp "$ipvx" "udp" "$sock_type" ""
+ srv_msg="$(ip netns exec $ns_cli bash -c "
+ exec 3<>'/dev/udp/${FOREIGN_ADDR%/*}/$PORT'
+ cli_port=\"\$(ss -uanp | grep \"pid=\$BASHPID,fd=3\" | awk '{print \$4}' | sed 's/.*://')\"
+ echo \"${FOREIGN_ADDR%/*} \$cli_port\" >/dev/udp/${SENDER_ADDR%/*}/$PORT
+ timeout 2s cat <&3
+ exec 3<&-
+ ")"
+ set_test_result "$sock_type" "yes" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+# Send a message to the server through a raw socket with HDRINCL. As the socket
+# is configured as HDRINCL, the expectation is to receive the message.
+test_foreign_addr_hdrincl() {
+ local test_name="$1"
+ local ipvx="$2"
+ local srv_msg
+
+ setup_test_hdrincl "$ipvx" "hdrincl"
+ ip netns exec $ns_cli \
+ ./ip_saddr "$ipvx" "raw" "${FOREIGN_ADDR%/*}" "${SRV_ADDR%/*}"
+ srv_msg="$(timeout 2s cat <&3)"
+ set_test_result "hdrincl" "" "$srv_msg"
+ cleanup_test
+ log_test "$test_name"
+}
+
+test_keep_addr_ipv4_tcp_normal() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv4 tcp normal
+}
+
+test_keep_addr_ipv4_udp_normal() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv4 udp normal
+}
+
+test_keep_addr_ipv6_tcp_normal() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv6 tcp normal
+}
+
+test_keep_addr_ipv6_udp_normal() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv6 udp normal
+}
+
+test_keep_addr_ipv4_tcp_transparent() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv4 tcp transparent
+}
+
+test_keep_addr_ipv4_udp_transparent() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv4 udp transparent
+}
+
+test_keep_addr_ipv6_tcp_transparent() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv6 tcp transparent
+}
+
+test_keep_addr_ipv6_udp_transparent() {
+ test_keep_addr_tcp_udp "${FUNCNAME[0]}" ipv6 udp transparent
+}
+
+test_keep_addr_ipv4_hdrincl() {
+ test_keep_addr_hdrincl "${FUNCNAME[0]}" ipv4
+}
+
+test_keep_addr_ipv6_hdrincl() {
+ test_keep_addr_hdrincl "${FUNCNAME[0]}" ipv6
+}
+
+test_remove_addr_ipv4_tcp_normal() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv4 tcp normal
+}
+
+test_remove_addr_ipv4_udp_normal() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv4 udp normal
+}
+
+test_remove_addr_ipv6_tcp_normal() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv6 tcp normal
+}
+
+test_remove_addr_ipv6_udp_normal() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv6 udp normal
+}
+
+test_remove_addr_ipv4_tcp_transparent() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv4 tcp transparent
+}
+
+test_remove_addr_ipv4_udp_transparent() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv4 udp transparent
+}
+
+test_remove_addr_ipv6_tcp_transparent() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv6 tcp transparent
+}
+
+test_remove_addr_ipv6_udp_transparent() {
+ test_remove_addr_tcp_udp "${FUNCNAME[0]}" ipv6 udp transparent
+}
+
+test_remove_addr_ipv4_hdrincl() {
+ test_remove_addr_hdrincl "${FUNCNAME[0]}" ipv4
+}
+
+test_remove_addr_ipv6_hdrincl() {
+ test_remove_addr_hdrincl "${FUNCNAME[0]}" ipv6
+}
+
+test_expire_addr_ipv4_tcp_normal() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv4 tcp normal
+}
+
+test_expire_addr_ipv4_udp_normal() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv4 udp normal
+}
+
+test_expire_addr_ipv6_tcp_normal() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv6 tcp normal
+}
+
+test_expire_addr_ipv6_udp_normal() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv6 udp normal
+}
+
+test_expire_addr_ipv4_tcp_transparent() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv4 tcp transparent
+}
+
+test_expire_addr_ipv4_udp_transparent() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv4 udp transparent
+}
+
+test_expire_addr_ipv6_tcp_transparent() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv6 tcp transparent
+}
+
+test_expire_addr_ipv6_udp_transparent() {
+ test_expire_addr_tcp_udp "${FUNCNAME[0]}" ipv6 udp transparent
+}
+
+test_expire_addr_ipv4_hdrincl() {
+ test_expire_addr_hdrincl "${FUNCNAME[0]}" ipv4
+}
+
+test_expire_addr_ipv6_hdrincl() {
+ test_expire_addr_hdrincl "${FUNCNAME[0]}" ipv6
+}
+
+test_foreign_addr_ipv4_udp_normal() {
+ test_foreign_addr_udp "${FUNCNAME[0]}" ipv4 normal
+}
+
+test_foreign_addr_ipv6_udp_normal() {
+ test_foreign_addr_udp "${FUNCNAME[0]}" ipv6 normal
+}
+
+test_foreign_addr_ipv4_udp_transparent() {
+ test_foreign_addr_udp "${FUNCNAME[0]}" ipv4 transparent
+}
+
+test_foreign_addr_ipv6_udp_transparent() {
+ test_foreign_addr_udp "${FUNCNAME[0]}" ipv6 transparent
+}
+
+test_foreign_addr_ipv4_hdrincl() {
+ test_foreign_addr_hdrincl "${FUNCNAME[0]}" ipv4
+}
+
+test_foreign_addr_ipv6_hdrincl() {
+ test_foreign_addr_hdrincl "${FUNCNAME[0]}" ipv6
+}
+
+export -f wait_addr_missing
+trap cleanup EXIT
+setup
+tests_run
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/net/ipv6_anycast.sh b/tools/testing/selftests/net/ipv6_anycast.sh
new file mode 100755
index 000000000000..5fa7a2db2a81
--- /dev/null
+++ b/tools/testing/selftests/net/ipv6_anycast.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test that IPv6 anycast addresses work. There are two ways to configure IPv6
+# anycast addresses: by opening a socket with the IPV6_JOIN_ANYCAST sockopt,
+# and automatically joining to the subnet-router anycast address. For simplicity
+# we'll use the 2nd method: when forwarding is enabled, the kernel automatically
+# joins the subnet-router anycast address (prefix with all host bits zero).
+
+ALL_TESTS="
+ test_ipv6_ping_anycast
+ test_ipv6_ping_anycast_src
+"
+
+source lib.sh
+
+H1_ADDR=2001:db8::1/64
+H2_ADDR=2001:db8::2/64
+ANYCAST_ADDR="2001:db8::"
+
+# shellcheck disable=SC2329
+cleanup()
+{
+ cleanup_ns $ns1 $ns2
+}
+
+
+setup() {
+ setup_ns ns1 ns2
+ ip -netns $ns1 link add veth0 type veth peer name veth0 netns $ns2
+
+ ip netns exec $ns1 sysctl -qw net.ipv6.conf.veth0.forwarding=0
+ ip netns exec $ns2 sysctl -qw net.ipv6.conf.veth0.forwarding=1
+ ip netns exec $ns2 sysctl -qw net.ipv6.icmp.echo_ignore_anycast=0
+ ip netns exec $ns2 sysctl -qw net.ipv6.anycast_src_echo_reply=0
+
+ ip -netns $ns1 -6 addr add "$H1_ADDR" dev veth0 nodad
+ ip -netns $ns1 link set dev veth0 up
+ ip -netns $ns2 -6 addr add "$H2_ADDR" dev veth0 nodad
+ ip -netns $ns2 link set dev veth0 up
+}
+
+# shellcheck disable=SC2329
+test_ipv6_ping_anycast() {
+ RET=0
+ if ! ip netns exec $ns1 ping -6 -c 3 -w 5 "$ANYCAST_ADDR" &>/dev/null; then
+ RET=$ksft_fail
+ fi
+ log_test "${FUNCNAME[0]}"
+}
+
+# shellcheck disable=SC2329
+test_ipv6_ping_anycast_src() {
+ # Also check that the echo-reply came from saddr=$ANYCAST_ADDR
+ RET=0
+ ip netns exec $ns2 sysctl -qw net.ipv6.anycast_src_echo_reply=1
+ if ! ip netns exec $ns1 ping -6 -c 3 -w 5 "$ANYCAST_ADDR" 2>/dev/null \
+ | grep "$ANYCAST_ADDR:" &>/dev/null; then
+ RET=$ksft_fail
+ fi
+ ip netns exec $ns2 sysctl -qw net.ipv6.anycast_src_echo_reply=0
+ log_test "${FUNCNAME[0]}"
+}
+
+trap cleanup EXIT
+setup
+tests_run
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/net/loopback_subnet.sh b/tools/testing/selftests/net/loopback_subnet.sh
new file mode 100755
index 000000000000..c846e65f01f3
--- /dev/null
+++ b/tools/testing/selftests/net/loopback_subnet.sh
@@ -0,0 +1,135 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+# Test loopback subnets (a.k.a. AnyIP). The topology is a client and a server,
+# with the server having a loopback subnet. Test permutations of the following
+# variations:
+# - ipv4 / ipv6
+# - ping from client / localhost ping from the server itself
+# - loopback subnet in the local table / loopback subnet in other table
+#
+# ns_srv | ns_cli
+# ---------------------------------|--------------
+# [ lo ] [ eth0 ]---|---[ eth0 ]
+# LO_SUBNET(route) SRV_IFADDR | CLI_IFADDR
+
+# shellcheck disable=SC2329
+
+ALL_TESTS="
+ test_loopback_subnet_from_localhost_ipv4
+ test_loopback_subnet_from_localhost_ipv6
+ test_loopback_subnet_from_remote_ipv4
+ test_loopback_subnet_from_remote_ipv6
+ test_loopback_subnet_from_localhost_ipv4_table
+ test_loopback_subnet_from_localhost_ipv6_table
+ test_loopback_subnet_from_remote_ipv4_table
+ test_loopback_subnet_from_remote_ipv6_table
+"
+
+source "lib.sh"
+
+LO_SUBNET4=192.168.88.0/24
+LO_ADDR4=192.168.88.14 # Just any address in LO_SUBNET4
+LO_SUBNET6=2001:db8:88::/64
+LO_ADDR6=2001:db8:88::54 # Just any address in LO_SUBNET6
+SRV_IFADDR4=192.168.99.1
+SRV_IFADDR6=2001:db8:99::1
+CLI_IFADDR4=192.168.99.2
+CLI_IFADDR6=2001:db8:99::2
+RT_TABLE=200
+
+cleanup() {
+ cleanup_ns $ns_srv $ns_cli
+}
+
+setup() {
+ setup_ns ns_srv ns_cli
+ ip -n $ns_srv link add eth0 netns $ns_cli type veth peer eth0
+
+ ip -n $ns_srv link set eth0 up
+ ip -n $ns_srv link set lo up
+ ip -n $ns_srv addr add "$SRV_IFADDR4/24" dev eth0
+ ip -n $ns_srv addr add "$SRV_IFADDR6/64" dev eth0 nodad
+
+ ip -n $ns_cli link set eth0 up
+ ip -n $ns_cli addr add "$CLI_IFADDR4/24" dev eth0
+ ip -n $ns_cli addr add "$CLI_IFADDR6/64" dev eth0 nodad
+ ip -n $ns_cli route add "$LO_SUBNET4" via "$SRV_IFADDR4"
+ ip -n $ns_cli route add "$LO_SUBNET6" via "$SRV_IFADDR6"
+}
+
+setup_loopback_subnets() {
+ local rt_table="${1:-local}"
+
+ ip -n $ns_srv route add local "$LO_SUBNET4" dev lo table "$rt_table"
+ ip -n $ns_srv route add local "$LO_SUBNET6" dev lo table "$rt_table"
+ if [[ $rt_table != local && $rt_table != 255 ]]; then
+ ip -n $ns_srv rule add from all lookup "$rt_table"
+ fi
+}
+
+cleanup_loopback_subnets() {
+ local rt_table="${1:-local}"
+
+ ip -n $ns_srv route del local "$LO_SUBNET4" dev lo table "$rt_table"
+ ip -n $ns_srv route del local "$LO_SUBNET6" dev lo table "$rt_table"
+ if [[ $rt_table != local && $rt_table != 255 ]]; then
+ ip -n $ns_srv rule del from all lookup "$rt_table"
+ fi
+}
+
+run_test() {
+ local test_name="$1"
+ local ns="$2"
+ local ping_addr="$3"
+ local rt_table="$4"
+
+ RET=0
+ setup_loopback_subnets "$rt_table"
+ if ! ip netns exec "$ns" ping -c 3 "$ping_addr" &>/dev/null; then
+ if [[ $FAIL_TO_XFAIL = yes ]]; then
+ RET=$ksft_xfail
+ else
+ RET=$ksft_fail
+ fi
+ fi
+ cleanup_loopback_subnets "$rt_table"
+ log_test "$test_name"
+}
+
+test_loopback_subnet_from_localhost_ipv4() {
+ run_test "${FUNCNAME[0]}" "$ns_srv" "$LO_ADDR4"
+}
+
+test_loopback_subnet_from_localhost_ipv6() {
+ run_test "${FUNCNAME[0]}" "$ns_srv" "$LO_ADDR6"
+}
+
+test_loopback_subnet_from_remote_ipv4() {
+ run_test "${FUNCNAME[0]}" "$ns_cli" "$LO_ADDR4"
+}
+
+test_loopback_subnet_from_remote_ipv6() {
+ run_test "${FUNCNAME[0]}" "$ns_cli" "$LO_ADDR6"
+}
+
+test_loopback_subnet_from_localhost_ipv4_table() {
+ xfail run_test "${FUNCNAME[0]}" "$ns_srv" "$LO_ADDR4" "$RT_TABLE"
+}
+
+test_loopback_subnet_from_localhost_ipv6_table() {
+ xfail run_test "${FUNCNAME[0]}" "$ns_srv" "$LO_ADDR6" "$RT_TABLE"
+}
+
+test_loopback_subnet_from_remote_ipv4_table() {
+ xfail run_test "${FUNCNAME[0]}" "$ns_cli" "$LO_ADDR4" "$RT_TABLE"
+}
+
+test_loopback_subnet_from_remote_ipv6_table() {
+ xfail run_test "${FUNCNAME[0]}" "$ns_cli" "$LO_ADDR6" "$RT_TABLE"
+}
+
+trap cleanup EXIT
+setup
+tests_run
+exit $EXIT_STATUS
--
2.54.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address
2026-10-28 19:10 ` [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
@ 2026-10-29 0:02 ` Kuniyuki Iwashima
2026-10-29 6:28 ` Íñigo Huguet
2026-10-29 19:12 ` sashiko-bot
1 sibling, 1 reply; 8+ messages in thread
From: Kuniyuki Iwashima @ 2026-10-29 0:02 UTC (permalink / raw)
To: Íñigo Huguet
Cc: Alexei Starovoitov, Andrew Lunn, Andrii Nakryiko, Daniel Borkmann,
David Ahern, David S. Miller, Eduard Zingerman, Eric Dumazet,
Florian Westphal, Ido Schimmel, Jakub Kicinski,
Kumar Kartikeya Dwivedi, Neal Cardwell, Pablo Neira Ayuso,
Paolo Abeni, Shuah Khan, Willem de Bruijn, bpf, Breno Leitao,
coreteam, Emil Tsalapatis, Fernando Fernandez Mancera,
Ihor Solodrai, Jeff Layton, Jiri Olsa, Joe Damato, John Fastabend,
Junseo Lim, Kees Cook, Leon Hwang, Li RongQing, linux-kselftest,
Martin KaFai Lau, netdev, netfilter-devel, Phil Sutter, Qi Tang,
Sebastian Andrzej Siewior, Simon Horman, Song Liu,
Stanislav Fomichev, Willem de Bruijn, Xuanqiang Luo,
Yonghong Song, Íñigo Huguet, Davide Caratti
On Wed, Oct 28, 2026 at 12:12 PM Íñigo Huguet <ihuguet@riseup.net> wrote:
>
> Don't allow routing packets with a source address that is not configured
> in the host. Allow it only in certain cases like when using a
> transparent socket, by setting the ANYSRC flag in flowi_flags.
This changes longstanding user-visible behaviour,
so the series should target net-next.
>
> Until now, it was possible to send such a packet if a route can be found
> in the routing table for it. For example:
> 1. Configure two addresses 1:2::3:4/64 and 1:2::3:5/64
> 2. Establish a TCP connection to 1:2::3:4
> 3. Remove the address from the interface, but keep the other.
> 4. Packets are still sent out by the TCP connection because of
> the automatic route created for the 2nd address. No incoming
> packets are accepted, though.
>
> This patch prevents the outgoing packets to be sent in normal
> circumnstances.
>
> This aligns the behaviour with the IPv4 stack. To determine the places
> where the ANYSRC needs to be set, I set the flag in the same places as
> the IPv4 stack does.
>
> Apart from consolidating the behaviour of both stacks, there is a more
> important reason why this is needed. RFC 4862 states that "an invalid
> address MUST NOT be used as the source address of outbound packets".
> Therefore, sending packets with a source address considered "invalid",
> like an expired address, is disallowed.
>
> This issue was reported by a user to whom IPv6 packets were leaked after
> removing the IPv6 address for the interface. The same happens if the
> address has its valid_lft expired.
>
> There is a valid use case that would break with this change: loopback
> subnets (aka AnyIP). They work in the following way: the administrator
> adds a route like `local 2001:db8:cafe::/64 dev lo`, so that the host
> will accept connections to all the addresses of that subnet. However,
> there is no address assigned to any interface, only a route, so that
> outgoing packets would be dropped because the source address doesn't
> belong to any interface in the host.
>
> In IPv4, loopback subnets work because __ip_dev_find fallbacks to a
> FIB lookup. For IPv6 it is unclear if such a change would be desired
> by all the current callers of ipv6_dev_find, ipv6_chk_addr and
> ipv6_chk_addr_and_flags, likely not. Because of that, add the FIB lookup
> fallback into ip6_output, where the check of the source address has been
> added.
>
> Signed-off-by: Íñigo Huguet <ihuguet@riseup.net>
>
> ---
> drivers/net/vrf.c | 1 +
> net/core/filter.c | 1 +
> net/core/lwt_bpf.c | 1 +
> net/ipv6/af_inet6.c | 1 +
> net/ipv6/datagram.c | 1 +
> net/ipv6/icmp.c | 5 +++++
> net/ipv6/inet6_connection_sock.c | 2 ++
> net/ipv6/ip6_output.c | 38 ++++++++++++++++++++++++++++++++
> net/ipv6/ip6_udp_tunnel.c | 1 +
> net/ipv6/netfilter.c | 11 +++++++--
> net/ipv6/ping.c | 1 +
> net/ipv6/raw.c | 1 +
> net/ipv6/syncookies.c | 1 +
> net/ipv6/tcp_ipv6.c | 7 +++++-
> net/ipv6/udp.c | 1 +
> net/l2tp/l2tp_ip6.c | 2 ++
> 16 files changed, 72 insertions(+), 3 deletions(-)
>
> diff --git a/drivers/net/vrf.c b/drivers/net/vrf.c
> index 79d433f49f80..4222e598290c 100644
> --- a/drivers/net/vrf.c
> +++ b/drivers/net/vrf.c
> @@ -432,6 +432,7 @@ static netdev_tx_t vrf_process_v6_outbound(struct sk_buff *skb,
> fl6.flowlabel = ip6_flowinfo(iph);
> fl6.flowi6_mark = skb->mark;
> fl6.flowi6_proto = iph->nexthdr;
> + fl6.flowi6_flags = FLOWI_FLAG_ANYSRC;
>
> dst = ip6_dst_lookup_flow(net, NULL, &fl6, NULL);
> if (IS_ERR(dst) || dst == dst_null)
> diff --git a/net/core/filter.c b/net/core/filter.c
> index 70dc621672f2..65ff54f6a29d 100644
> --- a/net/core/filter.c
> +++ b/net/core/filter.c
> @@ -5077,6 +5077,7 @@ BPF_CALL_4(bpf_skb_set_tunnel_key, struct sk_buff *, skb,
> sizeof(from->local_ipv6));
> info->key.label = cpu_to_be32(from->tunnel_label) &
> IPV6_FLOWLABEL_MASK;
> + info->key.flow_flags = FLOWI_FLAG_ANYSRC;
> } else {
> info->key.u.ipv4.dst = cpu_to_be32(from->remote_ipv4);
> info->key.u.ipv4.src = cpu_to_be32(from->local_ipv4);
> diff --git a/net/core/lwt_bpf.c b/net/core/lwt_bpf.c
> index da49364ec63d..e8b954282689 100644
> --- a/net/core/lwt_bpf.c
> +++ b/net/core/lwt_bpf.c
> @@ -233,6 +233,7 @@ static int bpf_lwt_xmit_reroute(struct sk_buff *skb)
> fl6.flowi6_mark = skb->mark;
> fl6.flowi6_uid = sock_net_uid(net, sk);
> fl6.flowlabel = ip6_flowinfo(iph6);
> + fl6.flowi6_flags = FLOWI_FLAG_ANYSRC;
> fl6.flowi6_proto = iph6->nexthdr;
> fl6.daddr = iph6->daddr;
> fl6.saddr = iph6->saddr;
> diff --git a/net/ipv6/af_inet6.c b/net/ipv6/af_inet6.c
> index a9b8b4b42a1a..8e3c6e095419 100644
> --- a/net/ipv6/af_inet6.c
> +++ b/net/ipv6/af_inet6.c
> @@ -816,6 +816,7 @@ int inet6_sk_rebuild_header(struct sock *sk)
> fl6->fl6_dport = inet->inet_dport;
> fl6->fl6_sport = inet->inet_sport;
> fl6->flowi6_uid = sk_uid(sk);
> + fl6->flowi6_flags = inet_sk_flowi_flags(sk);
> security_sk_classify_flow(sk, flowi6_to_flowi_common(fl6));
>
> ip6_ecmp_set_mp_hash(sock_net(sk), fl6, sk->sk_txhash);
> diff --git a/net/ipv6/datagram.c b/net/ipv6/datagram.c
> index 7b71a932544d..f09bbffec0e7 100644
> --- a/net/ipv6/datagram.c
> +++ b/net/ipv6/datagram.c
> @@ -52,6 +52,7 @@ static void ip6_datagram_flow_key_init(struct flowi6 *fl6,
> fl6->flowi6_mark = sk->sk_mark;
> fl6->fl6_dport = inet->inet_dport;
> fl6->fl6_sport = inet->inet_sport;
> + fl6->flowi6_flags = inet_sk_flowi_flags(sk);
> fl6->flowlabel = ip6_make_flowinfo(np->tclass, np->flow_label);
> fl6->flowi6_uid = sk_uid(sk);
>
> diff --git a/net/ipv6/icmp.c b/net/ipv6/icmp.c
> index a95b0351824f..b4b54297c923 100644
> --- a/net/ipv6/icmp.c
> +++ b/net/ipv6/icmp.c
> @@ -393,6 +393,11 @@ static struct dst_entry *icmpv6_route_lookup(struct net *net,
> if (err)
> goto relookup_failed;
>
> + /* New src is the original dst. It may not be a local address, like with
> + * encapsulated traffic.
> + */
> + fl2.flowi6_flags |= FLOWI_FLAG_ANYSRC;
> +
> err = ip6_dst_lookup(net, sk, &dst2, &fl2);
> if (err)
> goto relookup_failed;
> diff --git a/net/ipv6/inet6_connection_sock.c b/net/ipv6/inet6_connection_sock.c
> index 3e4ce8cb478e..f295f3efe243 100644
> --- a/net/ipv6/inet6_connection_sock.c
> +++ b/net/ipv6/inet6_connection_sock.c
> @@ -45,6 +45,7 @@ struct dst_entry *inet6_csk_route_req(const struct sock *sk,
> fl6->flowi6_mark = ireq->ir_mark;
> fl6->fl6_dport = ireq->ir_rmt_port;
> fl6->fl6_sport = htons(ireq->ir_num);
> + fl6->flowi6_flags = inet_sk_flowi_flags(sk);
> fl6->flowi6_uid = sk_uid(sk);
> security_req_classify_flow(req, flowi6_to_flowi_common(fl6));
>
> @@ -71,6 +72,7 @@ struct dst_entry *inet6_csk_route_socket(struct sock *sk,
> fl6->daddr = sk->sk_v6_daddr;
> fl6->saddr = np->saddr;
> fl6->flowlabel = np->flow_label;
> + fl6->flowi6_flags = inet_sk_flowi_flags(sk);
> IP6_ECN_flow_xmit(sk, fl6->flowlabel);
>
> if (sk->sk_protocol == IPPROTO_TCP)
> diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
> index 25fe0ba98b1a..8715cf43f00a 100644
> --- a/net/ipv6/ip6_output.c
> +++ b/net/ipv6/ip6_output.c
> @@ -1134,6 +1134,33 @@ static struct dst_entry *ip6_sk_dst_check(struct sock *sk,
> return dst;
> }
>
> +/* Check if the address belongs to the host */
> +static bool ip6_is_local_saddr(struct net *net, struct in6_addr *saddr)
> +{
> + struct fib6_result fib_result;
> + struct flowi6 fl6;
> + bool result;
> +
> + if (ipv6_chk_addr_namespace(net, saddr) ||
> + ipv6_chk_acast_addr_src(net, NULL, saddr))
> + return true;
> +
> + /* Fallback to FIB local routes so that loopback subnets work. */
> + memset(&fl6, 0, sizeof(fl6));
> + fl6.daddr = *saddr;
> + result = false;
> +
> + rcu_read_lock();
> + if (fib6_lookup(net, 0, &fl6, &fib_result, 0) == 0 &&
> + fib_result.fib6_type == RTN_LOCAL &&
> + fib_result.nh->nh_common.nhc_dev) {
> + result = true;
> + }
> + rcu_read_unlock();
> +
> + return result;
> +}
> +
> static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
> struct dst_entry **dst, struct flowi6 *fl6)
> {
> @@ -1191,6 +1218,17 @@ static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
> if (err)
> goto out_err_release;
>
> + /* From RFC 4862, "an invalid address MUST NOT be used as the source
> + * address of outbound packets". Exceptions to this must set the ANYSRC
> + * flag (i.e. transparent sockets).
> + */
> + if (!ipv6_addr_any(&fl6->saddr) &&
> + !(fl6->flowi6_flags & FLOWI_FLAG_ANYSRC) &&
> + !ip6_is_local_saddr(net, &fl6->saddr)) {
> + err = -ENETUNREACH;
> + goto out_err_release;
> + }
> +
> #ifdef CONFIG_IPV6_OPTIMISTIC_DAD
> /*
> * Here if the dst entry we've looked up
> diff --git a/net/ipv6/ip6_udp_tunnel.c b/net/ipv6/ip6_udp_tunnel.c
> index 32525a051a6f..1727c36ff6a4 100644
> --- a/net/ipv6/ip6_udp_tunnel.c
> +++ b/net/ipv6/ip6_udp_tunnel.c
> @@ -161,6 +161,7 @@ struct dst_entry *udp_tunnel6_dst_lookup(struct sk_buff *skb,
> fl6.fl6_sport = sport;
> fl6.fl6_dport = dport;
> fl6.flowlabel = ip6_make_flowinfo(dsfield, key->label);
> + fl6.flowi6_flags = key->flow_flags;
>
> dst = ip6_dst_lookup_flow(net, sk, &fl6, NULL);
> if (IS_ERR(dst)) {
> diff --git a/net/ipv6/netfilter.c b/net/ipv6/netfilter.c
> index a7025ec87035..716fc6e246de 100644
> --- a/net/ipv6/netfilter.c
> +++ b/net/ipv6/netfilter.c
> @@ -29,8 +29,9 @@ int ip6_route_me_harder(struct net *net, struct sock *sk_partial, struct sk_buff
> struct flow_keys flkeys;
> unsigned int hh_len;
> struct dst_entry *dst;
> - int strict = (ipv6_addr_type(&iph->daddr) &
> - (IPV6_ADDR_MULTICAST | IPV6_ADDR_LINKLOCAL));
> + int saddr_type = ipv6_addr_type(&iph->saddr);
> + int daddr_type = ipv6_addr_type(&iph->daddr);
> + int strict = daddr_type & (IPV6_ADDR_MULTICAST | IPV6_ADDR_LINKLOCAL);
> struct flowi6 fl6 = {
> .flowi6_l3mdev = l3mdev_master_ifindex(dev),
> .flowi6_mark = skb->mark,
> @@ -41,6 +42,12 @@ int ip6_route_me_harder(struct net *net, struct sock *sk_partial, struct sk_buff
> };
> int err;
>
> + fl6.flowi6_flags = sk ? inet_sk_flowi_flags(sk) : 0;
> + if (saddr_type & (IPV6_ADDR_UNICAST | IPV6_ADDR_LINKLOCAL))
> + fl6.flowi6_flags |= FLOWI_FLAG_ANYSRC;
> + else
> + fl6.saddr = in6addr_any;
> +
> if (sk && sk->sk_bound_dev_if)
> fl6.flowi6_oif = sk->sk_bound_dev_if;
> else if (strict)
> diff --git a/net/ipv6/ping.c b/net/ipv6/ping.c
> index 6e90d0bf9f3d..d0964396c2c5 100644
> --- a/net/ipv6/ping.c
> +++ b/net/ipv6/ping.c
> @@ -140,6 +140,7 @@ static int ping_v6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
> fl6.flowi6_proto = IPPROTO_ICMPV6;
> fl6.saddr = np->saddr;
> fl6.daddr = *daddr;
> + fl6.flowi6_flags = inet_sk_flowi_flags(sk);
> fl6.flowi6_mark = ipc6.sockc.mark;
> fl6.flowi6_uid = sk_uid(sk);
> fl6.fl6_icmp_type = user_icmph.icmp6_type;
> diff --git a/net/ipv6/raw.c b/net/ipv6/raw.c
> index b965258cf9e5..ef5ddbe7ac10 100644
> --- a/net/ipv6/raw.c
> +++ b/net/ipv6/raw.c
> @@ -887,6 +887,7 @@ static int rawv6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
> fl6.flowi6_oif = READ_ONCE(np->ucast_oif);
> security_sk_classify_flow(sk, flowi6_to_flowi_common(&fl6));
>
> + fl6.flowi6_flags = inet_sk_flowi_flags(sk);
> if (hdrincl)
> fl6.flowi6_flags |= FLOWI_FLAG_KNOWN_NH;
>
> diff --git a/net/ipv6/syncookies.c b/net/ipv6/syncookies.c
> index b581cb1ee2e8..3d665e93fcac 100644
> --- a/net/ipv6/syncookies.c
> +++ b/net/ipv6/syncookies.c
> @@ -242,6 +242,7 @@ struct sock *cookie_v6_check(struct sock *sk, struct sk_buff *skb)
> fl6.flowi6_mark = ireq->ir_mark;
> fl6.fl6_dport = ireq->ir_rmt_port;
> fl6.fl6_sport = inet_sk(sk)->inet_sport;
> + fl6.flowi6_flags = inet_sk_flowi_flags(sk);
> fl6.flowi6_uid = sk_uid(sk);
> security_req_classify_flow(req, flowi6_to_flowi_common(&fl6));
>
> diff --git a/net/ipv6/tcp_ipv6.c b/net/ipv6/tcp_ipv6.c
> index e006a30360ea..5248cbd4b595 100644
> --- a/net/ipv6/tcp_ipv6.c
> +++ b/net/ipv6/tcp_ipv6.c
> @@ -271,8 +271,9 @@ static int tcp_v6_connect(struct sock *sk, struct sockaddr_unsized *uaddr,
> fl6->flowi6_mark = sk->sk_mark;
> fl6->fl6_dport = usin->sin6_port;
> fl6->fl6_sport = inet->inet_sport;
> + fl6->flowi6_flags = inet_sk_flowi_flags(sk);
> if (IS_ENABLED(CONFIG_IP_ROUTE_MULTIPATH) && !fl6->fl6_sport)
> - fl6->flowi6_flags = FLOWI_FLAG_ANY_SPORT;
> + fl6->flowi6_flags |= FLOWI_FLAG_ANY_SPORT;
> fl6->flowi6_uid = sk_uid(sk);
>
> opt = rcu_dereference_protected(np->opt, lockdep_sock_is_held(sk));
> @@ -982,6 +983,10 @@ static void tcp_v6_send_response(const struct sock *sk, struct sk_buff *skb, u32
> fl6.fl6_dport = t1->dest;
> fl6.fl6_sport = t1->source;
> fl6.flowi6_uid = sock_net_uid(net, sk && sk_fullsock(sk) ? sk : NULL);
> + if (sk && sk_fullsock(sk))
> + fl6.flowi6_flags = inet_sk_flowi_flags(sk);
> + else if (sk && inet_sk_transparent(sk))
> + fl6.flowi6_flags = FLOWI_FLAG_ANYSRC;
> security_skb_classify_flow(skb, flowi6_to_flowi_common(&fl6));
>
> /* Pass a socket to ip6_dst_lookup either it is for RST
> diff --git a/net/ipv6/udp.c b/net/ipv6/udp.c
> index 59f9872c3552..7492d155726a 100644
> --- a/net/ipv6/udp.c
> +++ b/net/ipv6/udp.c
> @@ -1650,6 +1650,7 @@ int udpv6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
> if (ipv6_addr_any(&fl6->saddr) && !ipv6_addr_any(&np->saddr))
> fl6->saddr = np->saddr;
> fl6->fl6_sport = inet->inet_sport;
> + fl6->flowi6_flags = inet_sk_flowi_flags(sk);
>
> if (cgroup_bpf_enabled(CGROUP_UDP6_SENDMSG) && !connected) {
> err = BPF_CGROUP_RUN_PROG_UDP6_SENDMSG_LOCK(sk,
> diff --git a/net/l2tp/l2tp_ip6.c b/net/l2tp/l2tp_ip6.c
> index bdaae1b64d25..22ae5bc2669a 100644
> --- a/net/l2tp/l2tp_ip6.c
> +++ b/net/l2tp/l2tp_ip6.c
> @@ -633,6 +633,8 @@ static int l2tp_ip6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
> else if (!fl6.flowi6_oif)
> fl6.flowi6_oif = READ_ONCE(np->ucast_oif);
>
> + fl6.flowi6_flags = inet_sk_flowi_flags(sk);
> +
> security_sk_classify_flow(sk, flowi6_to_flowi_common(&fl6));
>
> fl6.flowlabel = ip6_make_flowinfo(ipc6.tclass, fl6.flowlabel);
> --
> 2.54.0
>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address
2026-10-29 0:02 ` Kuniyuki Iwashima
@ 2026-10-29 6:28 ` Íñigo Huguet
0 siblings, 0 replies; 8+ messages in thread
From: Íñigo Huguet @ 2026-10-29 6:28 UTC (permalink / raw)
To: Kuniyuki Iwashima
Cc: Íñigo Huguet, Alexei Starovoitov, Andrew Lunn,
Andrii Nakryiko, Daniel Borkmann, David Ahern, David S. Miller,
Eduard Zingerman, Eric Dumazet, Florian Westphal, Ido Schimmel,
Jakub Kicinski, Kumar Kartikeya Dwivedi, Neal Cardwell,
Pablo Neira Ayuso, Paolo Abeni, Shuah Khan, Willem de Bruijn, bpf,
Breno Leitao, coreteam, Emil Tsalapatis,
Fernando Fernandez Mancera, Ihor Solodrai, Jeff Layton, Jiri Olsa,
Joe Damato, John Fastabend, Junseo Lim, Kees Cook, Leon Hwang,
Li RongQing, linux-kselftest, Martin KaFai Lau, netdev,
netfilter-devel, Phil Sutter, Qi Tang, Sebastian Andrzej Siewior,
Simon Horman, Song Liu, Stanislav Fomichev, Willem de Bruijn,
Xuanqiang Luo, Yonghong Song, Davide Caratti
On Thu, Oct 29, 2026 at 1:02 AM Kuniyuki Iwashima <kuniyu@google.com> wrote:
> This changes longstanding user-visible behaviour,
> so the series should target net-next.
You're absolutely right and I already agreed on sending it to
net-next, but sent to net by mistake.
Please review here and I'll send v4 to net-next.
--
Íñigo Huguet
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address
2026-10-28 19:10 ` [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
2026-10-29 0:02 ` Kuniyuki Iwashima
@ 2026-10-29 19:12 ` sashiko-bot
1 sibling, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-29 19:12 UTC (permalink / raw)
To: Íñigo Huguet; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] net/ipv6: uninitialized fib_result in ip6_is_local_saddr()
- [Medium] sctp: missing flowi6_flags initialization in sctp_v6_get_dst()
--
commit a60f53e33acaf53f5beb0a9e9e20fd1e7c8f5aac
Author: Íñigo Huguet <ihuguet@riseup.net>
net/ipv6: don't route packets with unknown source address
This patch prevents routing outbound IPv6 packets when the source address
is not configured on the host (with a fallback to FIB local routes for
loopback subnets), unless FLOWI_FLAG_ANYSRC is set in flowi6_flags.
[ ... ]
> diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
> index 25fe0ba98b1a6..8715cf43f00ac 100644
> --- a/net/ipv6/ip6_output.c
> +++ b/net/ipv6/ip6_output.c
> @@ -1134,6 +1134,33 @@ static struct dst_entry *ip6_sk_dst_check(struct sock *sk,
> return dst;
> }
>
> +/* Check if the address belongs to the host */
> +static bool ip6_is_local_saddr(struct net *net, struct in6_addr *saddr)
> +{
> + struct fib6_result fib_result;
> + struct flowi6 fl6;
> + bool result;
> +
> + if (ipv6_chk_addr_namespace(net, saddr) ||
> + ipv6_chk_acast_addr_src(net, NULL, saddr))
> + return true;
> +
> + /* Fallback to FIB local routes so that loopback subnets work. */
> + memset(&fl6, 0, sizeof(fl6));
> + fl6.daddr = *saddr;
> + result = false;
> +
> + rcu_read_lock();
> + if (fib6_lookup(net, 0, &fl6, &fib_result, 0) == 0 &&
> + fib_result.fib6_type == RTN_LOCAL &&
> + fib_result.nh->nh_common.nhc_dev) {
> + result = true;
> + }
> + rcu_read_unlock();
> +
> + return result;
> +}
[Severity: Critical]
Should fib_result be zero-initialized in ip6_is_local_saddr() before
calling fib6_lookup()?
In net/ipv6/ip6_output.c, ip6_is_local_saddr() declares fib_result on the
stack without initializing it, leaving fib_result.rt6 with uninitialized
stack data.
When custom IPv6 FIB rules are present (net->ipv6.fib6_has_custom_rules is
true), fib6_lookup() in net/ipv6/fib6_rules.c calls fib_rules_lookup() with
arg.lookup_ptr = fib6_table_lookup and arg.result = &fib_result:
net/ipv6/fib6_rules.c:fib6_lookup() {
if (net->ipv6.fib6_has_custom_rules) {
struct fib_lookup_arg arg = {
.lookup_ptr = fib6_table_lookup,
.lookup_data = &oif,
.result = res,
.flags = FIB_LOOKUP_NOREF,
};
...
err = fib_rules_lookup(net->ipv6.fib6_rules_ops,
flowi6_to_flowi(fl6), flags, &arg);
During rule evaluation, fib_rules_lookup() in net/core/fib_rules.c invokes
fib6_rule_action() -> fib6_rule_action_alt() -> fib6_table_lookup() ->
rt6_select(). In net/ipv6/route.c, rt6_select() and __find_rr_leaf() only
populate res->f6i, res->nh, res->fib6_flags, and res->fib6_type, leaving
res->rt6 untouched.
When fib6_rule_action() returns 0, fib_rules_lookup() then calls
fib6_rule_suppress() in net/ipv6/fib6_rules.c:
net/ipv6/fib6_rules.c:fib6_rule_suppress() {
struct fib6_result *res = arg->result;
struct rt6_info *rt = res->rt6;
struct net_device *dev = NULL;
if (!rt)
return false;
if (rt->rt6i_idev)
dev = rt->rt6i_idev->dev;
if (rt->rt6i_dst.plen <= rule->suppress_prefixlen)
goto suppress_route;
Because fib_result.rt6 is uninitialized, any non-zero stack value causes
fib6_rule_suppress() to dereference rt->rt6i_idev and rt->rt6i_dst.plen,
and potentially call ip6_rt_put_flags() on an arbitrary stack address.
> @@ -1191,6 +1218,17 @@ static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
> if (err)
> goto out_err_release;
>
> + /* From RFC 4862, "an invalid address MUST NOT be used as the source
> + * address of outbound packets". Exceptions to this must set the ANYSRC
> + * flag (i.e. transparent sockets).
> + */
> + if (!ipv6_addr_any(&fl6->saddr) &&
> + !(fl6->flowi6_flags & FLOWI_FLAG_ANYSRC) &&
> + !ip6_is_local_saddr(net, &fl6->saddr)) {
> + err = -ENETUNREACH;
> + goto out_err_release;
> + }
> +
> #ifdef CONFIG_IPV6_OPTIMISTIC_DAD
> /*
> * Here if the dst entry we've looked up
[Severity: Medium]
Does sctp_v6_get_dst() in net/sctp/ipv6.c also need to set
fl6->flowi6_flags = inet_sk_flowi_flags(sk) so that transparent IPv6 SCTP
sockets continue to work with this check?
IPv6 SCTP supports IPV6_TRANSPARENT and allows binding to non-local IPv6
addresses via sctp_inet6_bind_verify() -> sctp_v6_available():
net/sctp/ipv6.c:sctp_v6_available() {
...
res = ipv6_can_nonlocal_bind(net, &sp->inet) ||
ipv6_chk_addr(net, in6, dev, 0);
However, in net/sctp/ipv6.c, sctp_v6_get_dst() zero-initializes _fl and
calls ip6_dst_lookup_flow(sock_net(sk), sk, fl6, final_p) without setting
fl6->flowi6_flags from inet_sk_flowi_flags(sk):
net/sctp/ipv6.c:sctp_v6_get_dst() {
...
memset(&_fl, 0, sizeof(_fl));
...
dst = ip6_dst_lookup_flow(sock_net(sk), sk, fl6, final_p);
...
fl6->saddr = laddr->a.v6.sin6_addr;
fl6->fl6_sport = laddr->a.v6.sin6_port;
final_p = fl6_update_dst(fl6, rcu_dereference(np->opt), &final);
bdst = ip6_dst_lookup_flow(sock_net(sk), sk, fl6, final_p);
Because FLOWI_FLAG_ANYSRC is not set in fl6->flowi6_flags, outbound route
lookups for an IPv6 SCTP socket with IPV6_TRANSPARENT bound to a non-local
address will now fail in ip6_dst_lookup_tail() with -ENETUNREACH.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028191103.623156-1-ihuguet@riseup.net?part=2
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH net v3 3/3] selftests: net: add tests for saddr, anycast and loopback subnets
2026-10-28 19:10 ` [PATCH net v3 3/3] selftests: net: add tests for saddr, anycast and loopback subnets Íñigo Huguet
@ 2026-10-29 19:12 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-29 19:12 UTC (permalink / raw)
To: Íñigo Huguet; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Medium] selftests: net: race between UDP send and addr del in test_remove_addr_tcp_udp()
- [Medium] selftests: net: missing IPv6 FIB rule in setup_loopback_subnets()
- [Medium] selftests: net: ip_saddr IPV6_PKTINFO bypasses routing saddr check in UDPv6 tests
- [Medium] selftests: net: ip_saddr hdrincl tests never exercise FLOWI_FLAG_ANYSRC check
- [Medium] selftests: net: subshell RET loss and custom loops in wait_addr_missing()
- [Low] selftests: net: missing sockaddr cast in send_or_receive_hdrincl()
--
commit ac326b57c470225e1a2b925bc997e96e565c7e5a
Author: Íñigo Huguet <ihuguet@riseup.net>
selftests: net: add tests for saddr, anycast and loopback subnets
This commit adds networking selftests covering outgoing packet source address
validation for IPv4 and IPv6 across TCP, UDP, and raw sockets with HDRINCL,
along with tests for IPv6 anycast and loopback subnets.
> diff --git a/tools/testing/selftests/net/ip_saddr.c b/tools/testing/selftests/net/ip_saddr.c
> new file mode 100644
> index 0000000000000..ed9b0a360b281
> --- /dev/null
> +++ b/tools/testing/selftests/net/ip_saddr.c
> @@ -0,0 +1,557 @@
[ ... ]
> +static int create_sock_ipv6(int proto, int port, bool transparent)
> +{
> + struct sockaddr_in6 address;
> + int sk, opt;
[ ... ]
> + opt = 1;
> + if (setsockopt(sk, IPPROTO_IPV6, IPV6_RECVPKTINFO, &opt, sizeof(opt))) {
> + perror("Failed to set IPV6_PKTINFO");
> + exit(EXIT_FAILURE);
> + }
> + opt = 1;
> + if (transparent &&
> + setsockopt(sk, IPPROTO_IPV6, IPV6_TRANSPARENT, &opt, sizeof(opt))) {
> + perror("Failed to set IPV6_TRANSPARENT");
> + exit(EXIT_FAILURE);
> + }
> +
> + memset(&address, 0, sizeof(address));
> + address.sin6_family = AF_INET6;
> + address.sin6_addr = in6addr_any;
> + address.sin6_port = htons(port);
> +
> + if (bind(sk, (struct sockaddr *)&address, sizeof(address))) {
> + perror("Failed to bind");
> + exit(EXIT_FAILURE);
> + }
> +
> + return sk;
> +}
[ ... ]
> +static void wait_and_send_udp(int sk, int sync_fd)
> +{
> + uint8_t data_buff[256], ctrl_buff[256];
> + ssize_t nbytes;
[ ... ]
> + nbytes = recvmsg(sk, &msg, 0);
> + if (nbytes < 0) {
> + perror("Failed to receive");
> + exit(EXIT_FAILURE);
> + }
> +
> + // The "test_foreign_addr_*" tests needs us to reply to the same address
> + // but different port, and also from a different local address. The new
> + // saddr and dport are received in the previous message with the format
> + // "new_source_address new_dest_port"
> + data_buff[nbytes] = '\0';
> + maybe_chg_saddr_dport((char *)data_buff, &msg);
> +
> + sync_wait(sync_fd);
> +
> + // Change the data, but keep the same ancillary data so that we use the
> + // same addresses as we received.
> + iov.iov_base = MESSAGE;
> + iov.iov_len = strlen(MESSAGE);
> +
> + if (sendmsg(sk, &msg, 0) < 0) {
> + /* Contrary to the TCP case, as UDP doesn't keep a "connection",
> + * the test cases where the source address is not configured in
> + * the host return an error here. It is expected, ignore it.
> + */
> + }
> +}
[Severity: Medium]
Does passing the IPV6_PKTINFO control message to sendmsg() in
wait_and_send_udp() bypass the new routing source address check in
ip6_dst_lookup_tail() during the negative IPv6 UDP tests?
In create_sock_ipv6(), the UDP socket is bound to in6addr_any with
IPV6_RECVPKTINFO enabled, and wait_and_send_udp() passes the received
IPV6_PKTINFO control message back into sendmsg(sk, &msg, 0).
In the kernel, udpv6_sendmsg() calls ip6_datagram_send_ctl() when
msg->msg_controllen is non-zero. For a non-transparent socket
(!ipv6_can_nonlocal_bind()), ip6_datagram_send_ctl() validates
src_info->ipi6_addr via ipv6_chk_addr_and_flags() and
ipv6_chk_acast_addr_src(), returning -EINVAL immediately when the address has
been removed, expired, or replaced with FOREIGN_ADDR6.
Because udpv6_sendmsg() aborts during cmsg parsing before calling
ip6_sk_dst_lookup_flow() -> ip6_dst_lookup_tail() -> ip6_is_local_saddr(),
test_remove_addr_ipv6_udp_normal, test_expire_addr_ipv6_udp_normal, and
test_foreign_addr_ipv6_udp_normal will pass even on a kernel without the
ip6_dst_lookup_tail() change. Should the UDP socket instead bind or connect to
the source address so that fl6->saddr is populated from np->saddr and checked
in ip6_dst_lookup_tail()?
> +
> +/* -----------------------------------------------
> + * Functions for the HDRINCL tests:
> + * - Create a RAW socket with the HDRINCL sockopt
> + * - If acting as server, wait for incoming data and print it
> + * - If acting as client, craft a packet with the saddr and daddr from the args
> + * and send it.
> + * ------------------------------------------------
> + */
> +static int create_sock_ipv4_hdrincl(void)
> +{
> + struct sockaddr_in address;
> + int sk, opt;
[ ... ]
> + memset(&address, 0, sizeof(address));
> + address.sin_family = AF_INET;
> + address.sin_addr.s_addr = htonl(INADDR_ANY);
> +
> + if (bind(sk, (struct sockaddr *)&address, sizeof(address))) {
> + perror("Failed to bind");
> + exit(EXIT_FAILURE);
> + }
> +
> + return sk;
> +}
> +
> +static int create_sock_ipv6_hdrincl(void)
> +{
> + struct sockaddr_in6 address;
> + int sk, opt;
[ ... ]
> + memset(&address, 0, sizeof(address));
> + address.sin6_family = AF_INET6;
> + address.sin6_addr = in6addr_any;
> +
> + if (bind(sk, (struct sockaddr *)&address, sizeof(address))) {
> + perror("Failed to bind");
> + exit(EXIT_FAILURE);
> + }
> +
> + return sk;
> +}
[Severity: Medium]
Do the HDRINCL raw socket tests actually exercise the FLOWI_FLAG_ANYSRC check
during route lookup?
Both create_sock_ipv4_hdrincl() and create_sock_ipv6_hdrincl() bind the raw
socket to INADDR_ANY and in6addr_any, and send_or_receive_hdrincl() calls
sendto() without a PKTINFO control message, placing local_addr only inside the
user-crafted packet buffer. In addition, test_remove_addr_hdrincl() and
test_expire_addr_hdrincl() in ip_saddr.sh delete or expire SENDER_ADDR before
starting the client ip_saddr process.
In the kernel, raw_sendmsg() and rawv6_sendmsg() perform the route lookup
(ip_route_output_flow() and ip6_dst_lookup_flow()) before raw_send_hdrinc() or
rawv6_send_hdrinc() copies the IP header from userspace. During that route
lookup, fl4.saddr and fl6.saddr are taken from inet->inet_saddr and np->saddr
(which are 0.0.0.0 and ::), so the route lookup selects the valid permanent
CLI_ADDR4 or CLI_ADDR6 configured on eth0 rather than testing an invalidated
or foreign source address against FLOWI_FLAG_ANYSRC.
Should the raw socket bind to SENDER_ADDR before the address is removed or
expired (or pass local_addr via IP_PKTINFO / IPV6_PKTINFO) so that fl4.saddr
and fl6.saddr hold the invalidated or foreign address when entering route
lookup?
[ ... ]
> +static void send_or_receive_hdrincl(int sk,
> + bool is_ipv6,
> + bool send_msg,
> + union addr46 *local_addr,
> + union addr46 *peer_addr)
> +{
> + uint8_t buff[256];
> + ssize_t msglen;
> +
> + if (send_msg && is_ipv6) {
> + struct sockaddr_in6 dest;
> +
> + memset(&dest, 0, sizeof(dest));
> + dest.sin6_family = AF_INET6;
> + dest.sin6_addr = peer_addr->addr6;
> + msglen = create_packet_ipv6(buff,
> + local_addr->addr6,
> + peer_addr->addr6);
> + if (sendto(sk, buff, msglen, 0, &dest, sizeof(dest)) < 0) {
> + perror("Failed to send");
> + exit(EXIT_FAILURE);
> + }
> + } else if (send_msg && !is_ipv6) {
> + struct sockaddr_in dest;
> +
> + memset(&dest, 0, sizeof(dest));
> + dest.sin_family = AF_INET;
> + dest.sin_addr = peer_addr->addr4;
> + msglen = create_packet_ipv4(buff,
> + local_addr->addr4,
> + peer_addr->addr4);
> + if (sendto(sk, buff, msglen, 0, &dest, sizeof(dest)) < 0) {
[Severity: Low]
Will passing &dest (of type struct sockaddr_in6 * and struct sockaddr_in *)
directly to sendto() without casting to (struct sockaddr *)&dest fail to build
with -Werror=incompatible-pointer-types on non-glibc C libraries such as musl
that do not use a transparent union for struct sockaddr?
[ ... ]
> diff --git a/tools/testing/selftests/net/ip_saddr.sh b/tools/testing/selftests/net/ip_saddr.sh
> new file mode 100755
> index 0000000000000..9345b743afd0b
> --- /dev/null
> +++ b/tools/testing/selftests/net/ip_saddr.sh
> @@ -0,0 +1,575 @@
[ ... ]
> +wait_raw_sock_listen_any_addr() {
> + local ipvx="$1"
> + local ns="$2"
> + local ptrn proto found i
> +
> + if [ "$ipvx" = "ipv4" ]; then
> + ptrn='^00000000:'
> + proto=raw
> + elif [ "$ipvx" = "ipv6" ]; then
> + ptrn='^00000000000000000000000000000000:'
> + proto=raw6
> + else
> + RET=$ksft_fail
> + return 1
> + fi
> + for i in $(seq 10); do
> + found="$(ip netns exec "$ns" cat "/proc/net/$proto" | \
> + awk -vptrn="$ptrn" '$2 ~ ptrn {print "yes"}')"
> + [ "$found" = "yes" ] && return 0
> + sleep 0.1
> + done
> + RET=$ksft_fail
> + return 1
> +}
> +
> +wait_addr_missing() {
> + local ns="$1"
> + local iface="$2"
> + local addr="$3"
> + local timeout="${4:-1}"
> +
> + for i in $(seq $((timeout * 10))); do
> + ip -n "$ns" -br a show dev "$iface" | grep -q "$addr" || return 0
> + sleep 0.1
> + done
> + RET=$ksft_fail
> + return 1
> +}
[Severity: Medium]
When wait_addr_missing() is called inside the command substitution and child
bash -c process in test_remove_addr_tcp_udp() and test_expire_addr_tcp_udp(),
isn't the RET=$ksft_fail assignment lost if the wait times out?
Because ksft_fail is not exported in lib.sh and wait_addr_missing() runs in a
subshell without its return status being checked, a timeout in
wait_addr_missing() will not update RET in the parent shell. In the
transparent socket tests (such as test_expire_addr_ipv4_tcp_transparent), the
subshell will still write to SRV_SYNC_FILE and receive the server's message
while the address is still present, resulting in a false pass.
Also, could wait_raw_sock_listen_any_addr() and wait_addr_missing() reuse
slowwait or busywait from tools/testing/selftests/net/lib.sh instead of open
coding the polling loops?
[ ... ]
> +# Open a TCP/UDP connection to the server, remove the address from the server
> +# and wait for a message from it. As the address is no longer present, the
> +# expectation is not to receive the message, except for transparent sockets.
> +test_remove_addr_tcp_udp() {
> + local test_name="$1"
> + local ipvx="$2"
> + local proto="$3"
> + local sock_type="$4"
> + local srv_msg
> +
> + setup_test_tcp_udp "$ipvx" "$proto" "$sock_type" "$SRV_SYNC_FILE"
> + srv_msg="$(ip netns exec $ns_cli bash -c "
> + exec 3<>'/dev/$proto/${SENDER_ADDR%/*}/$PORT'
> + [ '$proto' = 'udp' ] && echo 'x' >&3
> + ip -n $ns_srv addr del '$SENDER_ADDR' dev eth0
> + wait_addr_missing '$ns_srv' eth0 '$SENDER_ADDR'
> + echo 'x' > '$SRV_SYNC_FILE'
> + timeout 2s cat <&3
> + exec 3<&-
> + ")"
> + set_test_result "$sock_type" "yes" "$srv_msg"
> + cleanup_test
> + log_test "$test_name"
> +}
[Severity: Medium]
Can "ip -n $ns_srv addr del '$SENDER_ADDR' dev eth0" in
test_remove_addr_tcp_udp() race with the delivery of the initial UDP datagram
to the server?
Unlike TCP, where opening /dev/tcp/... blocks until ARP or NDISC resolution
and the three-way handshake complete with wait_and_send_tcp() in ns_srv,
opening /dev/udp/... only performs a local connect() syscall, and
"echo 'x' >&3" returns as soon as the datagram is queued (including while
waiting for ARP or neighbor discovery resolution).
Furthermore, parse_tcp_udp_args() in ip_saddr.c opens SRV_SYNC_FILE with
O_RDWR at startup before calling recvmsg(), so writing to SRV_SYNC_FILE does
not wait for the server to finish recvmsg() either. If SENDER_ADDR is deleted
on ns_srv before ARP/ND resolution or UDP reception completes, the client's
initial datagram is dropped, wait_and_send_udp() remains blocked in recvmsg(),
and test_remove_addr_ipv4_udp_transparent and
test_remove_addr_ipv6_udp_transparent time out and fail (while the normal
variants pass without exercising sendmsg()).
Should wait_and_send_udp() signal the client after recvmsg() succeeds before
the client deletes SENDER_ADDR?
[ ... ]
> diff --git a/tools/testing/selftests/net/loopback_subnet.sh b/tools/testing/selftests/net/loopback_subnet.sh
> new file mode 100755
> index 0000000000000..c846e65f01f3b
> --- /dev/null
> +++ b/tools/testing/selftests/net/loopback_subnet.sh
> @@ -0,0 +1,135 @@
[ ... ]
> +setup_loopback_subnets() {
> + local rt_table="${1:-local}"
> +
> + ip -n $ns_srv route add local "$LO_SUBNET4" dev lo table "$rt_table"
> + ip -n $ns_srv route add local "$LO_SUBNET6" dev lo table "$rt_table"
> + if [[ $rt_table != local && $rt_table != 255 ]]; then
> + ip -n $ns_srv rule add from all lookup "$rt_table"
> + fi
> +}
> +
> +cleanup_loopback_subnets() {
> + local rt_table="${1:-local}"
> +
> + ip -n $ns_srv route del local "$LO_SUBNET4" dev lo table "$rt_table"
> + ip -n $ns_srv route del local "$LO_SUBNET6" dev lo table "$rt_table"
> + if [[ $rt_table != local && $rt_table != 255 ]]; then
> + ip -n $ns_srv rule del from all lookup "$rt_table"
> + fi
> +}
[Severity: Medium]
Aren't setup_loopback_subnets() and cleanup_loopback_subnets() missing "-6"
rule commands for IPv6?
In setup_loopback_subnets(), "ip -n $ns_srv route add local $LO_SUBNET6 ..."
automatically selects AF_INET6 because LO_SUBNET6 is an IPv6 prefix, but
"ip -n $ns_srv rule add from all lookup $rt_table" has no address family
indicator and defaults to IPv4 (AF_INET) only.
Without "ip -6 -n $ns_srv rule add from all lookup $rt_table" (and the
matching "ip -6 ... rule del" in cleanup_loopback_subnets()), IPv6 FIB
lookups in ns_srv never consult RT_TABLE (200), causing
test_loopback_subnet_from_localhost_ipv6_table and
test_loopback_subnet_from_remote_ipv6_table to fail due to a missing FIB rule
rather than testing loopback subnet behavior in custom tables.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028191103.623156-1-ihuguet@riseup.net?part=3
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-29 19:12 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-28 19:10 [PATCH net v3 0/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 1/3] net/ipv6: add ipv6_chk_addr_namespace Íñigo Huguet
2026-10-28 19:10 ` [PATCH net v3 2/3] net/ipv6: don't route packets with unknown source address Íñigo Huguet
2026-10-29 0:02 ` Kuniyuki Iwashima
2026-10-29 6:28 ` Íñigo Huguet
2026-10-29 19:12 ` sashiko-bot
2026-10-28 19:10 ` [PATCH net v3 3/3] selftests: net: add tests for saddr, anycast and loopback subnets Íñigo Huguet
2026-10-29 19:12 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.