Netdev List
 help / color / mirror / Atom feed
* [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; 9+ 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] 9+ 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-11-01 19:49   ` netdev-bot+sashiko
  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, 1 reply; 9+ 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] 9+ 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-11-01 19:49   ` netdev-bot+sashiko
  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; 9+ 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] 9+ 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-11-01 19:49   ` netdev-bot+sashiko
  2 siblings, 1 reply; 9+ 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] 9+ 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-11-01 19:49   ` netdev-bot+sashiko
  1 sibling, 1 reply; 9+ 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] 9+ 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; 9+ 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] 9+ messages in thread

* Re: [PATCH net v3 1/3] net/ipv6: add ipv6_chk_addr_namespace
  2026-10-28 19:10 ` [PATCH net v3 1/3] net/ipv6: add ipv6_chk_addr_namespace Íñigo Huguet
@ 2026-11-01 19:49   ` netdev-bot+sashiko
  0 siblings, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-11-01 19:49 UTC (permalink / raw)
  To: ihuguet
  Cc: ast, andrew+netdev, andrii, daniel, dsahern, davem, eddyz87,
	edumazet, fw, idosch, kuba, memxor, ncardwell, pablo, pabeni,
	shuah, willemdebruijn.kernel, bpf, leitao, coreteam, emil,
	fmancera, ihor.solodrai, jlayton, jolsa, joe, john.fastabend,
	zirajs7, kees, kuniyu, leon.hwang, lirongqing, linux-kselftest,
	martin.lau, netdev, netfilter-devel, phil, tpluszz77, bigeasy,
	horms, song, sdf, willemb, luoxuanqiang, yonghong.song, ihuguet,
	dcaratti

Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.

Low priority / nit picks for your consideration:

- [Low] The commit message says the patch clarifies how ipv6_chk_addr()
  and related functions behave, but two of the new comments describe the…
- [Low] ipv6_chk_addr_namespace() is exported with EXPORT_SYMBOL, but no
  module uses it.

--

net/ipv6: add ipv6_chk_addr_namespace

This patch adds a skip_l3mdev_check argument to ipv6_chk_addr_and_flags()
and a new ipv6_chk_addr_namespace() helper. The helper looks for an address
in every L3 domain of the namespace. The patch also rewrites the comments
that describe how ipv6_chk_addr() and related functions behave when dev is
NULL.

> diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
> index c8c0dd04982d9..b13a5846ec95a 100644
> --- a/net/ipv6/addrconf.c
> +++ b/net/ipv6/addrconf.c

[ ... ]

> @@ -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".
> + */

[Severity: Low]
Is the description of case 1 accurate? The commit message says the patch
clarifies the behaviour, but the match condition in
__ipv6_chk_addr_and_flags() quoted above is looser:

    (skip_dev_check || ndev == dev ||
     !(ifp->scope&(IFA_LINK|IFA_HOST) || strict))

With skip_dev_check == false and strict == 0, a global scope address on any
other device in the same L3 domain also matches. Only link-local or host
scope addresses, or strict == 1, are tied to the exact device.

Existing callers depend on this. ip6_route_info_create() checks prefsrc
with ipv6_chk_addr(net, &cfg->fc_prefsrc, dev, 0). ip6_tnl_rcv_ctl() and
ip6_tnl_xmit_ctl() pass (ldev, false, false, 0, ...).

The new comment on ipv6_chk_addr() below has the same problem ("Check if
'addr' is configured on 'dev'"), because ipv6_chk_addr() passes the
caller's strict value through.

The ipv6_chk_addr_namespace() comment also says "exists in the entire
namespace". It leaves out that tentative addresses are skipped because of
IFA_F_TENTATIVE, while optimistic ones are accepted.

Could these comments mention the strict and scope exception, and the
tentative filtering?

>  int ipv6_chk_addr_and_flags(struct net *net, const struct in6_addr *addr,
>  			    const struct net_device *dev, bool skip_dev_check,

[ ... ]

> +/* 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);

[Severity: Low]
Is this export needed? CONFIG_IPV6 is bool in net/ipv6/Kconfig, so
addrconf.c is always built in.

The only user in the series is ip6_is_local_saddr() in
net/ipv6/ip6_output.c. It is added by the follow-up patch "net/ipv6: don't
route packets with unknown source address", and that file is also always
built in.

Could the EXPORT_SYMBOL be dropped? If a modular user is planned, could it
be EXPORT_SYMBOL_GPL instead?

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261028191103.623156-1-ihuguet%40riseup.net

^ permalink raw reply	[flat|nested] 9+ 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-11-01 19:49   ` netdev-bot+sashiko
  1 sibling, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-11-01 19:49 UTC (permalink / raw)
  To: ihuguet
  Cc: ast, andrew+netdev, andrii, daniel, dsahern, davem, eddyz87,
	edumazet, fw, idosch, kuba, memxor, ncardwell, pablo, pabeni,
	shuah, willemdebruijn.kernel, bpf, leitao, coreteam, emil,
	fmancera, ihor.solodrai, jlayton, jolsa, joe, john.fastabend,
	zirajs7, kees, kuniyu, leon.hwang, lirongqing, linux-kselftest,
	martin.lau, netdev, netfilter-devel, phil, tpluszz77, bigeasy,
	horms, song, sdf, willemb, luoxuanqiang, yonghong.song, ihuguet,
	dcaratti

Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider.

Critical: 0 · High: 1 · Medium: 5 · Low: 1

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [High] ip6_is_local_saddr() in net/ipv6/ip6_output.c declares `struct
  fib6_result fib_result;` without initializing it and passes it to…
- [Medium] The new source-address check lives only in
  ip6_dst_lookup_tail() and is not part of the core IPv6 output route…
- [Medium] The ip6_is_local_saddr() fallback that is supposed to keep
  AnyIP working calls fib6_lookup(net, 0, &fl6, ...) on a freshly zeroed…
- [Medium] net/sctp/ipv6.c:sctp_v6_get_dst() builds its own flowi6
  (memset to zero) and calls ip6_dst_lookup_flow(), but never sets…
- [Medium] The new source check runs only in ip6_dst_lookup_tail(), and
  ip6_sk_dst_lookup_flow() returns the socket's cached dst before…
- [Medium] The new source check runs before the socket takes its route-
  cache cookie, and nothing orders the two against address deletion.
- [Low] The new non-local source check in ip6_dst_lookup_tail() runs
  before the existing IPv4-mapped source check (ip6_output.c:1275-1279,…

--

net/ipv6: don't route packets with unknown source address

This patch adds a source address check to ip6_dst_lookup_tail(). Flows
whose saddr is not configured on the host are rejected with -ENETUNREACH
unless FLOWI_FLAG_ANYSRC is set, and ANYSRC is now set in the IPv6
places where IPv4 sets it. A FIB lookup fallback in ip6_is_local_saddr()
is meant to keep loopback subnets (AnyIP) working.

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

[Severity: High]
Should fib_result be zero-initialized here? The other fib6_lookup()
callers use "struct fib6_result res = {};".

If net->ipv6.fib6_has_custom_rules is set (any ip -6 rule, VRF,
wg-quick and so on), fib6_lookup() goes through fib_rules_lookup(). That
calls ops->suppress after each successful rule action:

fib_rules_lookup()
  fib6_rule_action()
    fib6_rule_action_alt()    /* returns 0, never writes res->rt6 */
  fib6_rule_suppress()

net/ipv6/fib6_rules.c:fib6_rule_suppress() {
	struct fib6_result *res = arg->result;
	struct rt6_info *rt = res->rt6;
	...
	if (!rt)
		return false;

	if (rt->rt6i_idev)
		dev = rt->rt6i_idev->dev;
	...
	if (rt->rt6i_dst.plen <= rule->suppress_prefixlen)
		goto suppress_route;
	...
suppress_route:
	ip6_rt_put_flags(rt, flags);
}

Only __fib6_rule_action() writes res->rt6. Here, whatever was left on
the stack would be dereferenced as an rt6_info pointer. If a rule has
suppress_prefixlen set, that pointer could also be released as a dst.

Any flow without ANYSRC and with a non-configured saddr reaches this
path, for example:

- a socket using IPV6_FREEBIND
- an established TCP socket whose address's valid_lft has expired
- IPV6_PKTINFO with nonlocal bind

CONFIG_INIT_STACK_ALL_ZERO builds would hide the problem.

> +	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;

[Severity: Medium]
Can this fallback find AnyIP local routes that live in a VRF table or in
a policy-routed table?

Only daddr is set here. The caller's flowi6_l3mdev, oif, iif and mark
are all dropped, even though ip6_dst_lookup_tail() has the original fl6
available and passes only &fl6->saddr.

With the VRF l3mdev rule installed, fib6_lookup()->l3mdev_update_flow()
finds no l3mdev because oif and iif are 0. The rule then never matches:

net/l3mdev/l3mdev.c:l3mdev_fib_rule_match() {
	...
	if (!fl->flowi_l3mdev)
		return 0;
	...
}

Tables selected by fwmark, iif or oif rules are skipped the same way.

For example, with "ip -6 route add local 2001:db8::/64 dev lo table
<vrf-table>", a socket in that VRF that replies from 2001:db8::5 would
now get -ENETUNREACH. The commit message presents AnyIP as handled, but
this seems to work only for local routes in the local or main tables.

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

[Severity: Low]
Does this new check shadow the existing v4-mapped check further down in
ip6_dst_lookup_tail(), which came from commits ec5e3b0a1d41 and
00ea1ceebe0d?

	if (ipv6_addr_v4mapped(&fl6->saddr) &&
	    !(ipv6_addr_v4mapped(&fl6->daddr) || ipv6_addr_any(&fl6->daddr))) {
		err = -EAFNOSUPPORT;
		goto out_err_release;
	}

ip6_is_local_saddr() always returns false for ::ffff:a.b.c.d. So any
flow without ANYSRC that has a v4-mapped saddr now gets -ENETUNREACH,
and IPSTATS_MIB_OUTNOROUTES is incremented even though a route was
found.

For example, a dual-stack UDP socket bound to ::ffff:127.0.0.1 that
calls sendto() to a native IPv6 peer used to get EAFNOSUPPORT and now
gets ENETUNREACH. A v4-mapped saddr with a v4-mapped or :: daddr, which
used to be allowed, would now be rejected too.

[Severity: Medium]
Is SCTP missing an inet_sk_flowi_flags() update like the other
protocols got in this patch?

sctp_v6_get_dst() in net/sctp/ipv6.c builds its own flow and never sets
flowi6_flags:

net/sctp/ipv6.c:sctp_v6_get_dst() {
	...
	memset(&_fl, 0, sizeof(_fl));
	fl6->daddr = daddr->v6.sin6_addr;
	...
	dst = ip6_dst_lookup_flow(sock_net(sk), sk, fl6, final_p);
	...
		bdst = ip6_dst_lookup_flow(sock_net(sk), sk, fl6, final_p);
	...
}

SCTP does allow binding to non-local addresses:

net/sctp/ipv6.c:sctp_v6_available() {
	...
	res = ipv6_can_nonlocal_bind(net, &sp->inet) ||
	      ipv6_chk_addr(net, in6, dev, 0);
	...
}

Take an SCTPv6 socket with IPV6_TRANSPARENT that is bound to a
non-local address. Both the first lookup and each bind-address lookup in
the loop would now fail with -ENETUNREACH, leaving t->dst NULL.

IPv4 SCTP (sctp_v4_get_dst()) does not set ANYSRC either, so this
matches IPv4. It is still new breakage for IPv6 SCTP TPROXY users.

[Severity: Medium]
Can the socket dst cache bypass this check?

ip6_sk_dst_lookup_flow() returns the cached dst before it reaches
ip6_dst_lookup_tail():

net/ipv6/ip6_output.c:ip6_sk_dst_lookup_flow() {
	struct dst_entry *dst = sk_dst_check(sk, inet6_sk(sk)->dst_cookie);

	dst = ip6_sk_dst_check(sk, dst, fl6);
	if (dst)
		return dst;
	...
}

ip6_sk_dst_check() compares fl6->saddr only under
CONFIG_IPV6_SUBTREES. Otherwise it checks just daddr and oif.

Consider a connected UDPv6 socket with IPV6_FREEBIND, or a host with
ip_nonlocal_bind=1. ip6_datagram_send_ctl() copies any IPV6_PKTINFO
ipi6_addr into fl6->saddr, because ipv6_can_nonlocal_bind() is true.

udpv6_sendmsg() clears connected when cmsgs are present, but that only
stops the dst from being stored. The dst cached by connect() is still
returned, and the packet goes out with an arbitrary non-local source.
The same send without a cached dst would be rejected with -ENETUNREACH.

IPv4 udp_sendmsg() clears connected before sk_dst_check() when cmsgs
are present, so its core check always runs.

[Severity: Medium]
Is there a window where an address deletion is missed and the stale dst
then stays cached?

The check here runs before the socket samples its route cookie in
ip6_dst_store(), and nothing orders the two against ipv6_del_addr():

CPU0 (socket lock, e.g. inet6_csk_route_socket())
ip6_dst_lookup_tail()
  ip6_is_local_saddr()            /* address still found */

CPU1 (RTNL)
ipv6_del_addr()
  hlist_del_init_rcu(&ifp->addr_lst)
  __ipv6_ifa_notify(RTM_DELADDR)
    rt_genid_bump_ipv6(net)       /* flushes fn_sernum */

CPU0
ip6_dst_store()
  rt6_get_cookie()
    fib6_get_cookie_safe()        /* reads the post-flush sernum */

For routes without a nexthop object (rt->sernum == 0), the cached dst
now carries a current cookie. Later calls through __sk_dst_check() ->
ip6_dst_check() accept it, and ip6_dst_lookup_tail() is not run again
for that socket. Packets would keep going out with the removed source
until some unrelated routing change flushes the trees again. Routes that
use nexthop objects are not affected.

>  #ifdef CONFIG_IPV6_OPTIMISTIC_DAD
>  	/*
>  	 * Here if the dst entry we've looked up

[ ... ]

> diff --git a/net/ipv6/netfilter.c b/net/ipv6/netfilter.c
> index a7025ec870359..716fc6e246de9 100644
> --- a/net/ipv6/netfilter.c
> +++ b/net/ipv6/netfilter.c

[ ... ]

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

[Severity: Medium]
Does this ANYSRC assignment have any effect? This flow goes straight to
ip6_route_output(), and in net/ipv6 the flag is read only by the new
check in ip6_dst_lookup_tail(). xfrm_decode_session() also rebuilds fl6
afterwards.

This points to a wider gap: the new check is not in the core IPv6
output lookup, so direct ip6_route_output() callers skip it. In IPv4 the
check sits in ip_route_output_key_hash_rcu() and covers every output
lookup, including __xfrm4_dst_lookup().

On the IPv6 side, xfrm6_dst_lookup() copies the SA's saddr and routes it
without any source check:

net/ipv6/xfrm6_policy.c:xfrm6_dst_lookup() {
	...
	if (params->saddr)
		memcpy(&fl6.saddr, params->saddr, sizeof(fl6.saddr));
	...
	dst = ip6_route_output(params->net, NULL, &fl6);
	...
}

So IPsec tunnel-mode outer headers would still go out with a removed or
expired local address. Other direct ip6_route_output() users
(ip6_route_me_harder(), collect_md tunnels, seg6/rpl) are also
unchecked.

This hunk also clears fl6.saddr for non-unicast source types (::,
v4-mapped, multicast) before the reroute lookup. The commit message does
not mention that change.

Given this, is the statement in the commit message accurate?

  This aligns the behaviour with the IPv4 stack.

>  	if (sk && sk->sk_bound_dev_if)
>  		fl6.flowi6_oif = sk->sk_bound_dev_if;
>  	else if (strict)

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261028191103.623156-1-ihuguet%40riseup.net

^ permalink raw reply	[flat|nested] 9+ 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-11-01 19:49   ` netdev-bot+sashiko
  0 siblings, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-11-01 19:49 UTC (permalink / raw)
  To: ihuguet
  Cc: ast, andrew+netdev, andrii, daniel, dsahern, davem, eddyz87,
	edumazet, fw, idosch, kuba, memxor, ncardwell, pablo, pabeni,
	shuah, willemdebruijn.kernel, bpf, leitao, coreteam, emil,
	fmancera, ihor.solodrai, jlayton, jolsa, joe, john.fastabend,
	zirajs7, kees, kuniyu, leon.hwang, lirongqing, linux-kselftest,
	martin.lau, netdev, netfilter-devel, phil, tpluszz77, bigeasy,
	horms, song, sdf, willemb, luoxuanqiang, yonghong.song, ihuguet,
	dcaratti

Thank you for your contribution! Sashiko AI review found 11 potential
issue(s) to consider.

Critical: 0 · High: 0 · Medium: 4 · Low: 7

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [Medium] The IPv6 '*_table' tests in loopback_subnet.sh are set up
  wrong and can never pass.
- [Medium] ip_saddr.sh can hang indefinitely if the ./ip_saddr server
  helper is not running.
- [Medium] selftests: net: ip_saddr hdrincl tests never exercise
  FLOWI_FLAG_ANYSRC check
- [Medium] selftests: net: ip_saddr IPV6_PKTINFO bypasses routing saddr
  check in UDPv6 tests
- [Low] Two error messages in tools/testing/selftests/net/ip_saddr.c name
  the wrong argument or socket option.
- [Low] In test_remove_addr_tcp_udp() and test_expire_addr_tcp_udp(),
  wait_addr_missing() runs inside an `ip netns exec $ns_cli bash -c…
- [Low] test_foreign_addr_udp() (ip_saddr.sh:401) finds the client's UDP
  port with `ss -uanp | grep "pid=$BASHPID,fd=3"`.
- [Low] parse_tcp_udp_args() uses 0 to mean 'no sync file' (`*out_sync_fd
  = 0;`), and sync_wait() checks `if (sync_fd && read(...))`.
- [Low] parse_hdrincl_args() ignores the return value of inet_pton().
- [Low] In test_expire_addr_*_{tcp,udp}_*, setup_test_tcp_udp() adds
  SENDER_ADDR with `preferred_lft 1 valid_lft 1` before the client has…
- [Low] selftests: net: missing sockaddr cast in
  send_or_receive_hdrincl()

--

selftests: net: add tests for saddr, anycast and loopback subnets

This adds selftests for source address validation of outgoing IPv4/IPv6
TCP, UDP and HDRINCL raw packets: ip_saddr.sh drives a C helper,
ip_saddr.c. It also adds ipv6_anycast.sh for subnet-router anycast and
loopback_subnet.sh for AnyIP loopback subnets. The loopback cases that
use a non-local table are marked xfail.

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

[Severity: Low]
This isn't a bug, but this message says arg3 while the code checks
argv[4]. parse_gral_args() calls argv[1] arg1, so argv[3] (the port)
is arg3. Should this say arg4?

There is a similar mismatch in create_sock_ipv6():

	if (setsockopt(sk, IPPROTO_IPV6, IPV6_RECVPKTINFO, &opt, sizeof(opt))) {
		perror("Failed to set IPV6_PKTINFO");

The code sets IPV6_RECVPKTINFO, but the message names IPV6_PKTINFO.

> +		exit(EXIT_FAILURE);
> +	}
> +
> +	*out_sync_fd = 0;

[Severity: Low]
The shipped script can't trigger this, but is 0 a safe "no sync file"
value? sync_wait() later does:

	if (sync_fd && read(sync_fd, buff, sizeof(buff) - 1) < 0) {

If the helper starts with stdin closed, open(argv[5]) could return fd 0,
and the FIFO wait would be skipped without any error.

ip_saddr.sh always starts the helper as a background job, so stdin is
/dev/null and fd 0 is never free. This only matters when the helper is
run by hand. Using -1 as the unset value would avoid the ambiguity.

> +	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);
> +		}
> +	}
> +}

[ ... ]

> +	*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);
> +	}

[Severity: Low]
The inet_pton() return values are ignored here. If argv[3] or argv[4] is
not a valid address for the chosen family, would the uninitialized
local_addr and peer_addr unions from main() end up in the crafted IP
header and in the sendto() destination?

Every caller in ip_saddr.sh passes valid, hard-coded addresses, so this
only affects manual runs of the helper.

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

[ ... ]

> +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'

[Severity: Medium]
Do the hdrincl variants ever reach the source address check they are
meant to cover? create_sock_ipv4_hdrincl() and
create_sock_ipv6_hdrincl() bind the raw socket to INADDR_ANY and
in6addr_any. send_or_receive_hdrincl() then calls sendto() without any
control message, so the sender address only exists in the crafted
header inside buff.

raw_sendmsg() and rawv6_sendmsg() do the route lookup before
raw_send_hdrinc() and rawv6_send_hdrinc() copy that header in. The
flow's saddr comes from inet->inet_saddr / np->saddr, or from a
PKTINFO cmsg if one is passed. Here it is unspecified.

The lookup therefore selects an address that is still configured, such
as CLI_ADDR. ip_route_output_key_hash_rcu() and ip6_dst_lookup_tail()
never see the removed, expired or foreign address.

As a result test_remove_addr_*_hdrincl, test_expire_addr_*_hdrincl and
test_foreign_addr_*_hdrincl pass whether or not FLOWI_FLAG_ANYSRC is
set for IP_HDRINCL and IPV6_HDRINCL sockets.

The previous patch changes rawv6_sendmsg() to set
fl6.flowi6_flags = inet_sk_flowi_flags(sk). That change is not covered
at all, and these tests would still pass if it were dropped.

Would it work to bind the client raw socket to SENDER_ADDR before the
address is removed or expires? Another option is to pass the sender
address in an IP_PKTINFO / IPV6_PKTINFO cmsg. For IPv6 the cmsg
approach would also need something like IPV6_FREEBIND, because
ip6_datagram_send_ctl() rejects a non-local ipi6_addr otherwise.

[Severity: Low]
Is a wait_addr_missing() timeout lost here? The function runs in the
bash -c child shell, and it reports failure only through RET and its
return status:

	RET=$ksft_fail
	return 1

RET is set only in the child. ksft_fail isn't exported, so it expands to
an empty string there. The return status is ignored, and the script goes
on to write the FIFO.

If the address is still present after the timeout, could the transparent
variants report OK even though the address was never invalidated? For
those variants set_test_result() only checks that a message arrived.

The call in test_expire_addr_tcp_udp() has the same problem. The hdrincl
variants call wait_addr_missing() in the main shell, so they are not
affected.

> +                        echo 'x' > '$SRV_SYNC_FILE'

[Severity: Medium]
Can this hang the whole script if the ./ip_saddr helper is not running?

Opening a FIFO for writing blocks until something opens it for reading.
The only process that ever opens $SRV_SYNC_FILE is the helper, in
parse_tcp_udp_args():

	*out_sync_fd = open(argv[5], O_RDWR);

setup_test_tcp_udp() doesn't check that the helper started. Also,
wait_local_port_listen() in lib.sh just leaves its loop without
returning a failure:

	for i in $(seq 10); do
		if ip netns exec "${listener_ns}" awk '{print $2" "$4}' \
		   /proc/net/"${protocol}"* | grep -q "${pattern}"; then
			break
		fi
		sleep 0.1
	done

The helper can exit early, for example when it wasn't built, or when
socket, setsockopt, bind, listen, accept or recvmsg fails and it calls
exit(EXIT_FAILURE). In that case this echo blocks in open() forever,
because timeout 2s only wraps the cat that follows.

cleanup_test and log_test then never run. The remaining tests are lost
until the kselftest runner timeout kills the script.

test_expire_addr_tcp_udp() has the same write.

Would it help to keep the FIFO open in the script (for example
exec 4<>"$SRV_SYNC_FILE" in setup()), wrap the write in timeout, and
check that the helper actually started?

> +                        timeout 2s cat <&3
> +                        exec 3<&-
> +                ")"

[ ... ]

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

[Severity: Low]
Can the 1 second lifetime run out before the client connects or sends
its UDP trigger? setup_test_tcp_udp() adds the address as its last step:

	ip -n $ns_srv addr add "$SENDER_ADDR" dev eth0 $nodad $addr_args

The lifetime starts counting at that point. The client still has to run
ip netns exec, start bash -c, and do exec 3<>/dev/$proto/... before its
SYN or datagram reaches the server.

On a slow or heavily loaded machine, the client's packet could arrive
after the address is gone and be dropped. The normal variants would then
pass without exercising the saddr check, and the transparent variants
would fail for unrelated reasons.

Would it be more robust to add the address with a long lifetime and
shorten it with ip addr change ... valid_lft 1 once the connection is up
and the server has received the trigger?

The remove variants have a similar but much smaller gap. The address is
deleted right after the UDP trigger is sent, with no acknowledgement
from the server.

> +	srv_msg="$(ip netns exec $ns_cli bash -c "
> +                        exec 3<>'/dev/$proto/${SENDER_ADDR%/*}/$PORT'
> +                        [ '$proto' = 'udp' ] && echo 'x' >&3

[ ... ]

> +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/.*://')\"

[Severity: Low]
Does $BASHPID here refer to the shell that owns fd 3? Each part of the
pipeline is forked before its words are expanded. So $BASHPID becomes
the PID of the process that runs grep, not the bash -c shell that
opened the socket.

The match works only because grep inherits fd 3 and ss -p happens to
see grep's process during its /proc scan. If it doesn't, cli_port is
empty and maybe_chg_saddr_dport() ends up doing:

	peer_addr->sin_port = htons(atoi(portstr));

with an empty portstr. The reply then goes to port 0 and sendmsg()
fails. The transparent foreign tests would fail spuriously, and the
normal ones would pass without testing anything.

Would using $$ (the PID of the bash -c shell), or filtering on the
connected peer address, find the socket reliably?

> +                        echo \"${FOREIGN_ADDR%/*} \$cli_port\" >/dev/udp/${SENDER_ADDR%/*}/$PORT

[ ... ]

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

[Severity: Medium]
Does this create an IPv6 rule? With no -6 and a "from all" selector, ip
rule defaults to AF_INET, so only an IPv4 rule pointing at table 200 is
added.

The IPv6 local route for $LO_SUBNET6 goes into table 200 just above.
fib6_rules_net_init() only installs default AF_INET6 rules for the
local and main tables, so no IPv6 lookup reaches table 200. That means
test_loopback_subnet_from_localhost_ipv6_table and
test_loopback_subnet_from_remote_ipv6_table cannot pass on any kernel.

cleanup_loopback_subnets() deletes only the IPv4 rule in the same way.

The commit message says:

    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.

The IPv6 variants never reach the case described there. With xfail, a
real IPv6 regression in a non-local table would still be reported as an
expected failure. These tests would also keep failing after the stated
limitation is fixed.

The previous patch's IPv6 source address check falls back to
fib6_lookup(), which follows policy rules. That makes this path relevant
to the main patch.

Would adding ip -n $ns_srv -6 rule add from all lookup "$rt_table", plus
the matching -6 rule del, make the IPv6 table tests meaningful? Other
selftests in this directory, such as fib_rule_tests.sh and
fcnal-test.sh, pass -6 explicitly for IPv6 rules.

> +        fi
> +}

[ ... ]

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

[Severity: Low]
Both sendto() calls in send_or_receive_hdrincl() pass &dest without a
cast. One is a struct sockaddr_in6 * and the other a
struct sockaddr_in *. The bind() calls in this file use the cast.

This relies on glibc declaring the address argument as a transparent
union, which it only does when __USE_GNU is in effect. Otherwise, for
example with musl, sendto() takes a const struct sockaddr *.

This then becomes an incompatible-pointer-types diagnostic, which
GCC 14 and newer treat as an error by default. Could both calls cast to
(struct sockaddr *)&dest like the bind() calls do?

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

[Severity: Medium]
Does the IPv6 normal UDP path ever reach the route lookup?
wait_and_send_udp() sends the reply with the IPV6_PKTINFO cmsg it
received, after maybe_chg_saddr_dport() may have rewritten ipi6_addr.

Because msg_controllen is non-zero, udpv6_sendmsg() first runs
ip6_datagram_send_ctl(). For a socket that can't do nonlocal binds,
that function checks ipi6_addr with ipv6_chk_addr_and_flags() and
ipv6_chk_acast_addr_src(). It returns -EINVAL when the address is not
configured.

This affects test_remove_addr_ipv6_udp_normal,
test_expire_addr_ipv6_udp_normal and test_foreign_addr_ipv6_udp_normal.
In each of them, sendmsg() fails during cmsg parsing, before
ip6_sk_dst_lookup_flow() and ip6_dst_lookup_tail() are reached.

The error is ignored here. So these tests pass the same way on a kernel
without the previous patch, and the new IPv6 source address check is
never exercised.

The IPv4 variants don't seem to have this problem. ip_cmsg_send() takes
ipi_spec_dst for IP_PKTINFO without validating it, and the address goes
on to the route lookup.

Could the IPv6 normal remove and expire variants send without the cmsg,
for example with the socket bound to SENDER_ADDR6 while it still
exists? The source would then come from np->saddr. The foreign variant
would probably need a different approach.

Printing the sendmsg() errno would also make it clearer which check
rejected the packet.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261028191103.623156-1-ihuguet%40riseup.net

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-11-01 19:49 UTC | newest]

Thread overview: 9+ 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-11-01 19:49   ` netdev-bot+sashiko
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-11-01 19:49   ` netdev-bot+sashiko
2026-10-28 19:10 ` [PATCH net v3 3/3] selftests: net: add tests for saddr, anycast and loopback subnets Íñigo Huguet
2026-11-01 19:49   ` netdev-bot+sashiko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox