* [PATCH net-next 01/13] amt: create an AF_INET6 encapsulation socket for an IPv6 outer address
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-09 12:24 ` [PATCH net-next 02/13] amt: send the Relay Advertisement over IPv6 Omar Ramadan
` (11 subsequent siblings)
12 siblings, 0 replies; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
The amt device binds one AF_INET UDP socket on the relay port, so a
relay and its gateways can only talk AMT over IPv4. RFC 7450 defines the
protocol over either IP version: the Relay Advertisement carries an IPv4
or an IPv6 relay address, and the relay answers in the IP version of the
Discovery (s5.1.2.5), so an IPv6-only access network needs an IPv6 outer
transport to reach the relay at all.
Add the outer IPv6 local address to struct amt_dev, and amt_v6(), which
reports the device's outer family from it. The inner family is
independent of the outer one: the bool v6 arguments keep selecting IGMP
or MLD, and a device of either outer family carries both IPv4 and IPv6
multicast. amt_create_sock() binds an AF_INET6 socket for an IPv6
device. The socket sends and verifies the UDP checksum, which RFC 8200
s8.1 makes mandatory over IPv6, and it is V6ONLY, so that it does not
also claim the IPv4 wildcard port, which an IPv4 amt device in the same
netns may hold.
The socket is also bound to the underlying link, IFLA_AMT_LINK. The
IPv4 paths pass that link as the output interface, and an IPv4 route
lookup then only uses routes through it. An IPv6 lookup that has a
source address, as every AMT send does, treats the output interface as
a preference unless the socket is bound to a device, so without the
binding the outer IPv6 traffic could leave through another link while
the device's MTU and headroom are derived from this one. The binding
also keeps packets that arrive on other links away from the socket. A
link-local address is unique only on its link, and relay tunnels are
keyed on the gateway's address, so two gateways on different links
with the same link-local address would otherwise share a tunnel, and
the replies would go out the wrong link.
The new socket also hands ICMPv6 errors to amt_err_lookup(). This relies
on amt_parse_type() pulling the AMT header behind the transport header,
as "amt: pull the AMT header behind the transport header in
amt_parse_type()" (eb0c18404c89) makes it do, since on the error path
skb->data points at the quoted IPv6 header.
Nothing sets local_ipv6 until the netlink attribute added at the end of
this series, once every path can use it. No functional change.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 34 ++++++++++++++++++++++++++++------
include/net/amt.h | 2 ++
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 81d26ef..423ed77 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -80,6 +80,14 @@ static struct in6_addr mld2_all_node = MLD2_ALL_NODE_INIT;
static struct mld2_grec mldv2_zero_grec;
#endif
+/* The outer transport family is fixed when the link is created: an IPv6
+ * local address selects IPv6, otherwise the device runs over IPv4.
+ */
+static bool amt_v6(const struct amt_dev *amt)
+{
+ return IS_ENABLED(CONFIG_IPV6) && !ipv6_addr_any(&amt->local_ipv6);
+}
+
static void __amt_source_gc_work(void)
{
struct amt_source_node *snode;
@@ -2984,19 +2992,33 @@ drop:
return 0;
}
-static struct sock *amt_create_sock(struct net *net, __be16 port)
+static struct sock *amt_create_sock(const struct amt_dev *amt)
{
struct udp_port_cfg udp_conf;
struct socket *sock;
int err;
memset(&udp_conf, 0, sizeof(udp_conf));
- udp_conf.family = AF_INET;
- udp_conf.local_ip.s_addr = htonl(INADDR_ANY);
+ if (amt_v6(amt)) {
+ /* Bound to :: by the memset, and to the underlying link, so
+ * that IPv6 route lookups are strict about the output
+ * interface, as IPv4 ones are, and a link-local peer is
+ * unique. V6ONLY leaves the IPv4 wildcard port to an IPv4
+ * amt device.
+ */
+ udp_conf.family = AF_INET6;
+ udp_conf.bind_ifindex = amt->stream_dev->ifindex;
+ udp_conf.use_udp6_tx_checksums = true;
+ udp_conf.use_udp6_rx_checksums = true;
+ udp_conf.ipv6_v6only = true;
+ } else {
+ udp_conf.family = AF_INET;
+ udp_conf.local_ip.s_addr = htonl(INADDR_ANY);
+ }
- udp_conf.local_udp_port = port;
+ udp_conf.local_udp_port = amt->relay_port;
- err = udp_sock_create(net, &udp_conf, &sock);
+ err = udp_sock_create(amt->net, &udp_conf, &sock);
if (err < 0)
return ERR_PTR(err);
@@ -3008,7 +3030,7 @@ static int amt_socket_create(struct amt_dev *amt)
struct udp_tunnel_sock_cfg tunnel_cfg;
struct sock *sk;
- sk = amt_create_sock(amt->net, amt->relay_port);
+ sk = amt_create_sock(amt);
if (IS_ERR(sk))
return PTR_ERR(sk);
diff --git a/include/net/amt.h b/include/net/amt.h
index 2846dde..8df7d43 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -344,6 +344,8 @@ struct amt_dev {
__be16 gw_port;
/* Outer local ip */
__be32 local_ip;
+ /* Outer local IPv6 address, :: unless the outer transport is IPv6 */
+ struct in6_addr local_ipv6;
/* Outer remote ip */
__be32 remote_ip;
/* Outer discovery ip */
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* [PATCH net-next 02/13] amt: send the Relay Advertisement over IPv6
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
2026-10-09 12:24 ` [PATCH net-next 01/13] amt: create an AF_INET6 encapsulation socket for an IPv6 outer address Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 03/13] amt: key relay tunnels on a union amt_addr endpoint Omar Ramadan
` (10 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
A gateway finds its relay with a Relay Discovery, and the relay answers
with a Relay Advertisement carrying the address the gateway should use
from then on. RFC 7450 s5.1.2 defines two forms of the Advertisement:
the same fixed header and nonce followed by either a 4-byte IPv4 or a
16-byte IPv6 relay address. A gateway tells the two apart by the length
of the UDP datagram, and the relay answers in the IP version of the
Discovery (s5.1.2.5), so an amt relay on an IPv6 outer transport has to
answer with the 24-byte IPv6 form.
Add struct amt_header_advertisement_v6 for that form and
amt_send_advertisement_v6(), which fills it on the stack and sends it
with a new amt_send_ctrl_v6() helper. The helper routes with
ip6_dst_lookup_flow() through amt_route6(), which the later IPv6 senders
share, and sends with udp_tunnel6_xmit_skb(), which builds the UDP and
IPv6 headers and fills in the UDP checksum that RFC 8200 s8.1 makes
mandatory over IPv6. Like the IPv4 control messages, it marks the skb
TC_PRIO_CONTROL, uses AMT_TOS as the traffic class and passes no netdev,
so control traffic stays out of the amt device's tunnel stats. It holds
rcu_read_lock_bh(): udp_tunnel6_xmit_skb() reaches ip6tunnel_xmit(),
which without PREEMPT_RT counts xmit recursion per CPU with
__this_cpu_inc() and __this_cpu_dec(), and the gateway's messages, added
later in this series, are sent from process context.
amt_discovery_handler() answers an IPv6 device's Discovery with the
IPv6 form, sent back to the outer source of the Discovery. RFC 7450
s5.1.2 makes the source of the Advertisement the destination of the
Discovery, the Relay Discovery Address, so amt_send_ctrl_v6() and
amt_route6() take the source address from their caller. The socket is
bound to ::, so a Discovery sent to an anycast or secondary address
reaches the relay, and a gateway accepts only an Advertisement whose
source is the address it sent the Discovery to; answering from
local_ipv6 would leave such a gateway stuck in discovery. The relay
address carried in the message stays local_ipv6. The same socket also
receives a Discovery sent to a multicast group the host has joined,
such as ff02::1, and a multicast address may not be a source (RFC 4291
s2.7), so a Discovery with a multicast destination is dropped rather
than answered by every relay on the link with an invalid packet.
The IPv4 Advertisement is still sent from local_ip: changing the source
of an existing IPv4 relay's replies is a fix of its own for net, and
nothing in this series depends on it.
No functional change: amt_v6() is still false for every device.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++
include/net/amt.h | 10 +++++
2 files changed, 120 insertions(+)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 423ed77..a550f84 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -609,6 +609,78 @@ static void amt_update_relay_status(struct amt_tunnel_list *tunnel,
spin_unlock_bh(&tunnel->lock);
}
+static struct dst_entry *amt_route6(struct amt_dev *amt, struct sock *sk,
+ const struct in6_addr *saddr,
+ const struct in6_addr *daddr,
+ __be16 sport, __be16 dport)
+{
+ struct flowi6 fl6;
+
+ memset(&fl6, 0, sizeof(fl6));
+ fl6.flowi6_oif = amt->stream_dev->ifindex;
+ fl6.flowi6_proto = IPPROTO_UDP;
+ fl6.daddr = *daddr;
+ fl6.saddr = *saddr;
+ fl6.fl6_dport = dport;
+ fl6.fl6_sport = sport;
+
+ return ip6_dst_lookup_flow(amt->net, sk, &fl6, NULL);
+}
+
+/* Send an AMT control message from @saddr over the IPv6 outer transport.
+ * Returns 0 once the message is handed to the IPv6 stack.
+ */
+static int amt_send_ctrl_v6(struct amt_dev *amt, const struct in6_addr *saddr,
+ const struct in6_addr *daddr,
+ __be16 sport, __be16 dport,
+ const void *msg, unsigned int len)
+{
+ struct dst_entry *dst;
+ struct sk_buff *skb;
+ struct sock *sk;
+ int hlen, err;
+
+ /* Without PREEMPT_RT, ip6tunnel_xmit() counts xmit recursion per
+ * CPU, so BH must be off even when this is called from process
+ * context.
+ */
+ rcu_read_lock_bh();
+ sk = rcu_dereference_bh(amt->sk);
+ if (!sk || !netif_running(amt->stream_dev) ||
+ !netif_running(amt->dev)) {
+ err = -ENETDOWN;
+ goto out;
+ }
+
+ dst = amt_route6(amt, sk, saddr, daddr, sport, dport);
+ if (IS_ERR(dst)) {
+ DEV_STATS_INC(amt->dev, tx_errors);
+ err = PTR_ERR(dst);
+ goto out;
+ }
+
+ hlen = LL_RESERVED_SPACE(amt->dev) + sizeof(struct ipv6hdr) +
+ sizeof(struct udphdr);
+ skb = netdev_alloc_skb_ip_align(amt->dev, hlen + len +
+ amt->dev->needed_tailroom);
+ if (!skb) {
+ dst_release(dst);
+ DEV_STATS_INC(amt->dev, tx_errors);
+ err = -ENOMEM;
+ goto out;
+ }
+
+ skb_reserve(skb, hlen);
+ skb_put_data(skb, msg, len);
+ skb->priority = TC_PRIO_CONTROL;
+ udp_tunnel6_xmit_skb(dst, sk, skb, NULL, saddr, daddr, AMT_TOS,
+ ip6_dst_hoplimit(dst), 0, sport, dport, false, 0);
+ err = 0;
+out:
+ rcu_read_unlock_bh();
+ return err;
+}
+
static void amt_send_discovery(struct amt_dev *amt)
{
struct amt_header_discovery *amtd;
@@ -2685,6 +2757,24 @@ out:
rcu_read_unlock();
}
+/* The IPv6 form of amt_send_advertisement(), carrying the relay's IPv6
+ * address. It is sent from @saddr, the address the Discovery was sent to.
+ */
+static void amt_send_advertisement_v6(struct amt_dev *amt, __be32 nonce,
+ const struct in6_addr *saddr,
+ const struct in6_addr *daddr,
+ __be16 dport)
+{
+ struct amt_header_advertisement_v6 amta = {
+ .hdr.type = AMT_MSG_ADVERTISEMENT,
+ .hdr.nonce = nonce,
+ .ip6 = amt->local_ipv6,
+ };
+
+ amt_send_ctrl_v6(amt, saddr, daddr, amt->relay_port, dport,
+ &amta, sizeof(amta));
+}
+
static bool amt_discovery_handler(struct amt_dev *amt, struct sk_buff *skb)
{
struct amt_header_discovery *amtd;
@@ -2701,6 +2791,26 @@ static bool amt_discovery_handler(struct amt_dev *amt, struct sk_buff *skb)
if (amtd->reserved || amtd->version)
return true;
+ /* The Advertisement takes the form of the outer IP version, and
+ * RFC 7450 s5.1.2 sources it from the address the Discovery was
+ * sent to, which may be an anycast Relay Discovery Address rather
+ * than local_ipv6.
+ */
+ if (amt_v6(amt)) {
+ const struct ipv6hdr *ip6h = ipv6_hdr(skb);
+
+ /* The socket bound to :: also receives a Discovery sent to
+ * a group, and a multicast address can never be a source
+ * (RFC 4291 s2.7), so such a Discovery is not answered.
+ */
+ if (ipv6_addr_is_multicast(&ip6h->daddr))
+ return true;
+
+ amt_send_advertisement_v6(amt, amtd->nonce, &ip6h->daddr,
+ &ip6h->saddr, udph->source);
+ return false;
+ }
+
amt_send_advertisement(amt, amtd->nonce, iph->saddr, udph->source);
return false;
diff --git a/include/net/amt.h b/include/net/amt.h
index 8df7d43..921944b 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -133,6 +133,16 @@ struct amt_header_advertisement {
__be32 ip4;
} __packed;
+/* The IPv6 form of the Relay Advertisement (RFC 7450 s5.1.2): the header
+ * and nonce, laid out as in a Discovery, then a 16-byte relay address. A
+ * gateway tells the two forms apart by the UDP datagram length
+ * (s5.1.2.5), not by a field in the message, so it is a type of its own.
+ */
+struct amt_header_advertisement_v6 {
+ struct amt_header_discovery hdr;
+ struct in6_addr ip6;
+} __packed;
+
struct amt_header_request {
#if defined(__LITTLE_ENDIAN_BITFIELD)
u32 type:4,
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 02/13] amt: send the Relay Advertisement over IPv6
2026-10-09 12:24 ` [PATCH net-next 02/13] amt: send the Relay Advertisement over IPv6 Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
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] amt_send_ctrl_v6() in drivers/net/amt.c never counts a transmit
failure after the route lookup.
- [Low] amt_route6() builds the flowi6 for ip6_dst_lookup_flow() with
memset() and never sets fl6.flowlabel.
- [Low] The new IPv6 branch of amt_discovery_handler() rejects a
Discovery whose outer destination is multicast, so that it does not…
--
amt: send the Relay Advertisement over IPv6
This adds the 24-byte IPv6 form of the Relay Advertisement. It also adds a
new amt_send_ctrl_v6() helper, which routes with ip6_dst_lookup_flow() via
amt_route6() and sends with udp_tunnel6_xmit_skb(). On an IPv6 device,
amt_discovery_handler() answers a Discovery from the Discovery's destination
address, and drops a Discovery whose destination is multicast.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index 423ed7745fc7d..a550f84e86344 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
> @@ -609,6 +609,78 @@ static void amt_update_relay_status(struct amt_tunnel_list *tunnel,
> spin_unlock_bh(&tunnel->lock);
> }
>
> +static struct dst_entry *amt_route6(struct amt_dev *amt, struct sock *sk,
> + const struct in6_addr *saddr,
> + const struct in6_addr *daddr,
> + __be16 sport, __be16 dport)
> +{
> + struct flowi6 fl6;
> +
> + memset(&fl6, 0, sizeof(fl6));
> + fl6.flowi6_oif = amt->stream_dev->ifindex;
> + fl6.flowi6_proto = IPPROTO_UDP;
> + fl6.daddr = *daddr;
> + fl6.saddr = *saddr;
> + fl6.fl6_dport = dport;
> + fl6.fl6_sport = sport;
[Severity: Low]
fl6.flowlabel stays at zero here, so the route lookup uses DSCP 0.
amt_send_ctrl_v6() then sends the packet with AMT_TOS (0xc0, DSCP 48) as
the traffic class. Would a DSCP-based IPv6 policy rule match this traffic?
fib6_rule_match() in net/ipv6/fib6_rules.c checks the flow key:
if ((r->dscp ^ ip6_dscp(fl6->flowlabel)) & r->dscp_mask)
return 0;
A rule like "ip -6 rule add dscp 48 table X" would therefore not match. The
Advertisement could then be routed by another table, or fail to route,
even though the packet on the wire carries DSCP 48. The later IPv6 senders
in the series share amt_route6() and would behave the same way.
udp_tunnel6_dst_lookup() in net/ipv6/ip6_udp_tunnel.c builds the key from
the traffic class it sends:
fl6.flowlabel = ip6_make_flowinfo(dsfield, key->label);
Should amt_route6() do the same with AMT_TOS? The existing IPv4 amt
control senders have the same mismatch: they pass tos 0 to
ip_route_output_ports() and then write iph->tos = AMT_TOS.
> +
> + return ip6_dst_lookup_flow(amt->net, sk, &fl6, NULL);
> +}
> +
> +/* Send an AMT control message from @saddr over the IPv6 outer transport.
> + * Returns 0 once the message is handed to the IPv6 stack.
> + */
> +static int amt_send_ctrl_v6(struct amt_dev *amt, const struct in6_addr *saddr,
> + const struct in6_addr *daddr,
> + __be16 sport, __be16 dport,
> + const void *msg, unsigned int len)
> +{
[ ... ]
> + skb_reserve(skb, hlen);
> + skb_put_data(skb, msg, len);
> + skb->priority = TC_PRIO_CONTROL;
> + udp_tunnel6_xmit_skb(dst, sk, skb, NULL, saddr, daddr, AMT_TOS,
> + ip6_dst_hoplimit(dst), 0, sport, dport, false, 0);
> + err = 0;
[Severity: Low]
Do output-path drops go uncounted in tx_errors here? dev is passed as NULL,
and ip6tunnel_xmit() in include/net/ip6_tunnel.h only looks at the
ip6_local_out() result when dev is set:
err = ip6_local_out(skb_dst_dev_net(skb), sk, skb);
if (dev) {
if (unlikely(net_xmit_eval(err)))
pkt_len = -1;
iptunnel_xmit_stats(dev, pkt_len);
}
err is then set to 0 here whatever happened.
The IPv4 sender of the same message, amt_send_advertisement(), counts
these drops:
err = ip_local_out(amt->net, sk, skb);
if (unlikely(net_xmit_eval(err)))
amt->dev->stats.tx_errors++;
So these drops raise tx_errors on an IPv4 relay but not on an IPv6 one: a
netfilter OUTPUT drop, a neighbour or qdisc drop, or the ip6tunnel_xmit()
recursion-limit drop. On IPv6 only route lookup and skb allocation failures
are counted.
amt_send_discovery_v6() and amt_send_request_v6(), added later in the
series, reuse this helper. They would also get err == 0 for every packet
that reaches udp_tunnel6_xmit_skb().
The commit message says the NULL netdev is deliberate, to keep control
traffic out of the tunnel stats. Could the ip6_local_out() result still be
checked here so tx_errors behaves the same as on IPv4?
> +out:
> + rcu_read_unlock_bh();
> + return err;
> +}
[ ... ]
> @@ -2701,6 +2791,26 @@ static bool amt_discovery_handler(struct amt_dev *amt, struct sk_buff *skb)
> if (amtd->reserved || amtd->version)
> return true;
>
[ ... ]
> + if (amt_v6(amt)) {
> + const struct ipv6hdr *ip6h = ipv6_hdr(skb);
> +
> + /* The socket bound to :: also receives a Discovery sent to
> + * a group, and a multicast address can never be a source
> + * (RFC 4291 s2.7), so such a Discovery is not answered.
> + */
> + if (ipv6_addr_is_multicast(&ip6h->daddr))
> + return true;
> +
> + amt_send_advertisement_v6(amt, amtd->nonce, &ip6h->daddr,
> + &ip6h->saddr, udph->source);
[Severity: Low]
What happens when the Discovery's outer source is ::? Only the destination
is checked, so ip6h->saddr becomes the Advertisement's destination without
any check.
ip6_rcv_core() drops a multicast or loopback source, but not an
unspecified one. UDPv6 delivery to the relay socket bound to :: does not
check it either. The path would be:
amt_discovery_handler()
amt_send_advertisement_v6(..., daddr = ::, ...)
amt_send_ctrl_v6()
amt_route6() fl6.daddr = ::
ip6_dst_lookup_flow()
udp_tunnel6_xmit_skb() outer daddr = ::
ip6_dst_lookup_tail() treats a zero daddr as the default router. So the
lookup succeeds whenever stream_dev has a default route, and the relay
sends a packet with destination ::, which RFC 4291 s2.5.2 forbids.
Should a Discovery with ipv6_addr_any(&ip6h->saddr) be dropped too, like
the multicast destination case?
> + return false;
> + }
> +
> amt_send_advertisement(amt, amtd->nonce, iph->saddr, udph->source);
>
> return false;
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 03/13] amt: key relay tunnels on a union amt_addr endpoint
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
2026-10-09 12:24 ` [PATCH net-next 01/13] amt: create an AF_INET6 encapsulation socket for an IPv6 outer address Omar Ramadan
2026-10-09 12:24 ` [PATCH net-next 02/13] amt: send the Relay Advertisement over IPv6 Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 04/13] amt: send the Membership Query over IPv6 Omar Ramadan
` (9 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
A relay keeps one struct amt_tunnel_list per gateway and finds it by
the gateway's IPv4 address, stored as __be32 ip4 and compared with
ip_hdr(skb)->saddr. On an IPv6 outer transport that reads bytes 4-7 of
the IPv6 source address as an IPv4 address, and the field cannot hold
the gateway's address anyway.
Store the endpoint as a union amt_addr, which group and source nodes
already use, and move the union above struct amt_tunnel_list so the
tunnel can embed it. Its ip6 member is no longer under CONFIG_IPV6, so
code that only copies, compares or prints an endpoint compiles without
an #if and relies on amt_v6() being false. The tunnel's endpoint grows
from 4 to 16 bytes on every kernel; a kernel without IPv6 also pays 12
more bytes per source node and 24 per group node, which it already
zeroes. The tunnel needs no family of its own: the relay's socket is
bound to one family, so every tunnel has the device's family, which
amt_v6() reports.
amt_outer_saddr() copies the outer source address of a received message
by value into a zeroed union amt_addr. amt_request_handler() takes that
snapshot once and matches and records tunnels with it, comparing with
the existing amt_addr_equal(); the union is zero-padded for IPv4, so
one memcmp() serves both families. As before, a tunnel is matched on
the address alone. When the tunnel limit is reached, an IPv6 device
answers with an ICMPv6 destination unreachable instead of an ICMP one.
It is sent with skb->dev set to the underlying link: amt_rcv() has made
it the amt device, and icmp6_send() routes an error to a link-local
source through skb->dev, where amt_dev_xmit() would drop it.
The response MAC becomes one siphash() over the packed {address, port,
nonce} tuple instead of siphash_3u32() over the IPv4 address. Only the
relay computes and checks the MAC; the gateway echoes it verbatim, so it
cannot tell that the value for an IPv4 endpoint changed.
The IPv4 senders and the relay status debug message are converted to
tunnel->addr.ip4, and the debug message prints an IPv6 endpoint with
%pI6c. No functional change: amt_v6() is still false for every device.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 73 ++++++++++++++++++++++++++++++++++++-----------
include/net/amt.h | 18 ++++++------
2 files changed, 66 insertions(+), 25 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index a550f84..eaa5637 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -88,6 +88,19 @@ static bool amt_v6(const struct amt_dev *amt)
return IS_ENABLED(CONFIG_IPV6) && !ipv6_addr_any(&amt->local_ipv6);
}
+/* Copy the outer source address of a received message by value, so that
+ * the caller may pull the skb afterwards.
+ */
+static void amt_outer_saddr(const struct amt_dev *amt,
+ const struct sk_buff *skb, union amt_addr *addr)
+{
+ memset(addr, 0, sizeof(*addr));
+ if (amt_v6(amt))
+ addr->ip6 = ipv6_hdr(skb)->saddr;
+ else
+ addr->ip4 = ip_hdr(skb)->saddr;
+}
+
static void __amt_source_gc_work(void)
{
struct amt_source_node *snode;
@@ -594,10 +607,16 @@ static void __amt_update_relay_status(struct amt_tunnel_list *tunnel,
{
if (validate && tunnel->status >= status)
return;
- netdev_dbg(tunnel->amt->dev,
- "Update Tunnel(IP = %pI4, PORT = %u) status %s -> %s",
- &tunnel->ip4, ntohs(tunnel->source_port),
- status_str[tunnel->status], status_str[status]);
+ if (amt_v6(tunnel->amt))
+ netdev_dbg(tunnel->amt->dev,
+ "Update Tunnel(IP = %pI6c, PORT = %u) status %s -> %s",
+ &tunnel->addr.ip6, ntohs(tunnel->source_port),
+ status_str[tunnel->status], status_str[status]);
+ else
+ netdev_dbg(tunnel->amt->dev,
+ "Update Tunnel(IP = %pI4, PORT = %u) status %s -> %s",
+ &tunnel->addr.ip4, ntohs(tunnel->source_port),
+ status_str[tunnel->status], status_str[status]);
tunnel->status = status;
}
@@ -1173,12 +1192,12 @@ static void amt_send_multicast_data(struct amt_dev *amt,
memset(&fl4, 0, sizeof(struct flowi4));
fl4.flowi4_oif = amt->stream_dev->ifindex;
- fl4.daddr = tunnel->ip4;
+ fl4.daddr = tunnel->addr.ip4;
fl4.saddr = amt->local_ip;
fl4.flowi4_proto = IPPROTO_UDP;
rt = ip_route_output_key(amt->net, &fl4);
if (IS_ERR(rt)) {
- netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->ip4);
+ netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->addr.ip4);
kfree_skb(skb);
return;
}
@@ -1228,13 +1247,13 @@ static bool amt_send_membership_query(struct amt_dev *amt,
skb_reset_inner_headers(skb);
memset(&fl4, 0, sizeof(struct flowi4));
fl4.flowi4_oif = amt->stream_dev->ifindex;
- fl4.daddr = tunnel->ip4;
+ fl4.daddr = tunnel->addr.ip4;
fl4.saddr = amt->local_ip;
fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
fl4.flowi4_proto = IPPROTO_UDP;
rt = ip_route_output_key(amt->net, &fl4);
if (IS_ERR(rt)) {
- netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->ip4);
+ netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->addr.ip4);
return true;
}
@@ -2585,7 +2604,7 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
skb_reset_network_header(skb);
list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
- if (tunnel->ip4 == saddr) {
+ if (tunnel->addr.ip4 == saddr) {
if ((nonce == tunnel->nonce &&
response_mac == tunnel->mac)) {
mod_delayed_work(amt_wq, &tunnel->gc_wq,
@@ -2818,18 +2837,23 @@ static bool amt_discovery_handler(struct amt_dev *amt, struct sk_buff *skb)
static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
{
+ struct {
+ union amt_addr addr;
+ __be16 port;
+ __be32 nonce;
+ } __packed mac_in;
struct amt_header_request *amtrh;
struct amt_tunnel_list *tunnel;
unsigned long long key;
+ union amt_addr saddr;
struct udphdr *udph;
- struct iphdr *iph;
u64 mac;
int i;
if (!pskb_may_pull(skb, sizeof(*udph) + sizeof(*amtrh)))
return true;
- iph = ip_hdr(skb);
+ amt_outer_saddr(amt, skb, &saddr);
udph = udp_hdr(skb);
amtrh = (struct amt_header_request *)(udp_hdr(skb) + 1);
@@ -2837,12 +2861,24 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
return true;
list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
- if (tunnel->ip4 == iph->saddr)
+ if (amt_addr_equal(&tunnel->addr, &saddr))
goto send;
spin_lock_bh(&amt->lock);
if (amt->nr_tunnels >= amt->max_tunnels) {
spin_unlock_bh(&amt->lock);
+ if (amt_v6(amt)) {
+ /* amt_rcv() made skb->dev the amt device, but
+ * icmp6_send() routes an error to a link-local
+ * source through skb->dev, and the amt device
+ * drops it. Send it through the underlying link.
+ */
+ skb->dev = amt->stream_dev;
+ icmpv6_ndo_send(skb, ICMPV6_DEST_UNREACH,
+ ICMPV6_ADDR_UNREACH, 0);
+ skb->dev = amt->dev;
+ return true;
+ }
icmp_ndo_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0);
return true;
}
@@ -2856,7 +2892,7 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
}
tunnel->source_port = udph->source;
- tunnel->ip4 = iph->saddr;
+ tunnel->addr = saddr;
memcpy(&key, &tunnel->key, sizeof(unsigned long long));
tunnel->amt = amt;
@@ -2876,10 +2912,13 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
send:
tunnel->nonce = amtrh->nonce;
- mac = siphash_3u32((__force u32)tunnel->ip4,
- (__force u32)tunnel->source_port,
- (__force u32)tunnel->nonce,
- &tunnel->key);
+ /* The MAC is opaque to the gateway, which only echoes it, so one
+ * siphash over the zero-padded endpoint serves both families.
+ */
+ mac_in.addr = tunnel->addr;
+ mac_in.port = tunnel->source_port;
+ mac_in.nonce = tunnel->nonce;
+ mac = siphash(&mac_in, sizeof(mac_in), &tunnel->key);
tunnel->mac = mac >> 16;
if (!netif_running(amt->dev) || !netif_running(amt->stream_dev))
diff --git a/include/net/amt.h b/include/net/amt.h
index 921944b..77d17fc 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -241,6 +241,14 @@ struct amt_relay_headers {
};
} __packed;
+/* ip6 is present without CONFIG_IPV6 too, so that code which only copies
+ * or compares addresses needs no #if; amt_v6() is then false.
+ */
+union amt_addr {
+ __be32 ip4;
+ struct in6_addr ip6;
+};
+
struct amt_tunnel_list {
struct list_head list;
/* Protect All resources under an amt_tunne_list */
@@ -251,7 +259,8 @@ struct amt_tunnel_list {
enum amt_status status;
struct delayed_work gc_wq;
__be16 source_port;
- __be32 ip4;
+ /* Gateway endpoint, in the device's outer family */
+ union amt_addr addr;
__be32 nonce;
siphash_key_t key;
u64 mac:48,
@@ -260,13 +269,6 @@ struct amt_tunnel_list {
struct hlist_head groups[];
};
-union amt_addr {
- __be32 ip4;
-#if IS_ENABLED(CONFIG_IPV6)
- struct in6_addr ip6;
-#endif
-};
-
/* RFC 3810
*
* When the router is in EXCLUDE mode, the router state is represented
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 03/13] amt: key relay tunnels on a union amt_addr endpoint
2026-10-09 12:24 ` [PATCH net-next 03/13] amt: key relay tunnels on a union amt_addr endpoint Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
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] In this commit, amt_update_handler() (drivers/net/amt.c) still
finds the tunnel with `saddr = ip_hdr(skb)->saddr` (line 2588) and…
- [Low] In this commit, amt_send_multicast_data() (line 1195) and
amt_send_membership_query() (line 1250) in drivers/net/amt.c always…
Pre-existing issues:
- [Medium] amt_request_handler() in drivers/net/amt.c searches
amt->tunnel_list under RCU only (lines 2863-2865).
- [Medium] In amt_request_handler() (drivers/net/amt.c), the new tunnel
is published with list_add_tail_rcu() at line 2905 before `tunnel->key…
--
amt: key relay tunnels on a union amt_addr endpoint
This commit replaces the IPv4-only __be32 ip4 endpoint in struct
amt_tunnel_list with a union amt_addr, and makes the union's ip6 member
unconditional. amt_request_handler() now records and matches tunnels
through a new amt_outer_saddr() helper and amt_addr_equal(), sends ICMPv6
when the tunnel limit is hit on an IPv6 device, and computes the response
MAC with siphash() over a packed {address, port, nonce} tuple. The commit
message states there is no functional change because amt_v6() is still
false for every device.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index a550f84e86344..eaa5637674694 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
[ ... ]
> @@ -1173,12 +1192,12 @@ static void amt_send_multicast_data(struct amt_dev *amt,
>
> memset(&fl4, 0, sizeof(struct flowi4));
> fl4.flowi4_oif = amt->stream_dev->ifindex;
> - fl4.daddr = tunnel->ip4;
> + fl4.daddr = tunnel->addr.ip4;
> fl4.saddr = amt->local_ip;
> fl4.flowi4_proto = IPPROTO_UDP;
> rt = ip_route_output_key(amt->net, &fl4);
[ ... ]
> @@ -1228,13 +1247,13 @@ static bool amt_send_membership_query(struct amt_dev *amt,
> skb_reset_inner_headers(skb);
> memset(&fl4, 0, sizeof(struct flowi4));
> fl4.flowi4_oif = amt->stream_dev->ifindex;
> - fl4.daddr = tunnel->ip4;
> + fl4.daddr = tunnel->addr.ip4;
> fl4.saddr = amt->local_ip;
[Severity: Low]
include/net/amt.h now documents tunnel->addr like this:
/* Gateway endpoint, in the device's outer family */
However, amt_send_multicast_data() and amt_send_membership_query() still
always route over IPv4 with fl4.daddr = tunnel->addr.ip4.
On a device where amt_v6() is true, would these send to an IPv4 address
built from the first 4 bytes of the gateway's IPv6 address?
This can't happen at this commit, because nothing assigns
amt->local_ipv6 yet, so amt_v6() is false for every device.
Later in the series, "amt: send the Membership Query over IPv6" and
"amt: forward multicast data over IPv6" switch both senders to
amt_udp_xmit(), which branches on amt_v6(). Both patches land before the
netlink attribute that can set local_ipv6.
[ ... ]
> @@ -2585,7 +2604,7 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
> skb_reset_network_header(skb);
>
> list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
> - if (tunnel->ip4 == saddr) {
> + if (tunnel->addr.ip4 == saddr) {
[Severity: Low]
amt_request_handler() now records the endpoint with amt_outer_saddr() and
matches it with amt_addr_equal(). amt_update_handler() still reads the
outer source as IPv4:
saddr = ip_hdr(skb)->saddr;
...
if (tunnel->addr.ip4 == saddr) {
On an IPv6 relay, wouldn't this compare bytes 0-3 of the stored IPv6
address with bytes 4-7 of the received IPv6 source? That would drop
every Membership Update.
As with the senders, this can't happen at this commit, because amt_v6()
is always false.
The later patch "amt: match the Membership Update tunnel by outer family"
converts amt_update_handler() to amt_outer_saddr() and amt_addr_equal().
That happens before local_ipv6 becomes settable.
[ ... ]
> @@ -2837,12 +2861,24 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
> return true;
>
> list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
> - if (tunnel->ip4 == iph->saddr)
> + if (amt_addr_equal(&tunnel->addr, &saddr))
> goto send;
>
> spin_lock_bh(&amt->lock);
[Severity: Medium]
This isn't a bug introduced by this patch, but this lookup runs under RCU
only. On a miss, the new tunnel is added under amt->lock without
searching the list again:
spin_lock_bh(&amt->lock);
...
list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
amt_rcv() runs as a UDP encap_rcv in softirq. Two Requests from the same
gateway address can therefore be handled at the same time on different
CPUs, for example when different source ports are steered to different
RX queues.
Can both miss the RCU lookup, so that each adds an amt_tunnel_list entry
for the same endpoint?
Later lookups stop at the first match, so the second entry would never
be found. It would still hold a max_tunnels slot and a pending gc_wq
until amt_tunnel_expire() removes it.
The Update that answers the Query sent from the duplicate could also be
checked against the first entry's nonce and MAC and rejected.
This patch only changes the comparison to amt_addr_equal().
[ ... ]
> @@ -2876,10 +2912,13 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
>
> send:
> tunnel->nonce = amtrh->nonce;
> - mac = siphash_3u32((__force u32)tunnel->ip4,
> - (__force u32)tunnel->source_port,
> - (__force u32)tunnel->nonce,
> - &tunnel->key);
> + /* The MAC is opaque to the gateway, which only echoes it, so one
> + * siphash over the zero-padded endpoint serves both families.
> + */
> + mac_in.addr = tunnel->addr;
> + mac_in.port = tunnel->source_port;
> + mac_in.nonce = tunnel->nonce;
> + mac = siphash(&mac_in, sizeof(mac_in), &tunnel->key);
[Severity: Medium]
This isn't a bug introduced by this patch, but amt_request_handler()
publishes the new tunnel before it sets the key:
list_add_tail_rcu(&tunnel->list, &amt->tunnel_list);
tunnel->key = amt->key;
Another CPU running amt_request_handler() can match this address under
RCU, jump to send:, and read tunnel->key here without holding amt->lock.
Could it compute tunnel->mac with the all-zero key left by kzalloc(), or
with a partly written key?
If that CPU's nonce and MAC stores end up as the final values, the
response MAC would come from a predictable key.
This patch reads the same key as before. It only replaces siphash_3u32()
with siphash() over the packed tuple.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 04/13] amt: send the Membership Query over IPv6
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (2 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 03/13] amt: key relay tunnels on a union amt_addr endpoint Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 05/13] amt: match the Membership Update tunnel by outer family Omar Ramadan
` (8 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
After the Request, a relay answers each gateway with a Membership Query
through amt_send_membership_query(), which routes and transmits only
over IPv4.
Rather than adding an IPv6 twin of the function, move its routing and
transmit step into amt_udp_xmit(). Given an skb that already carries
the AMT message, a union amt_addr destination and the port pair, it
sends over the device's family: ip_route_output_key() and
udp_tunnel_xmit_skb() for IPv4, as before, and amt_route6() and
udp_tunnel6_xmit_skb(), which fills in the mandatory IPv6 UDP checksum,
for IPv6. Both use AMT_TOS, as the IPv4 path and the IPv6 control
messages do. udp_tunnel_xmit_skb() scrubs the skb in iptunnel_xmit(),
but udp_tunnel6_xmit_skb() does not, so the IPv6 path calls
skb_scrub_packet() itself. Otherwise the conntrack entry and extensions
of the inner packet would stay on the outer one, which conntrack would
then pass as an already tracked flow.
amt_send_membership_query() sizes its headroom for the outer family
with amt_ip_hlen(), builds the AMT header and hands the skb to
amt_udp_xmit(). The route lookup now runs after the header push; on a
route failure the caller still frees the skb. The relay's General
Query, which amt_send_igmp_gq() and amt_send_mld_gq() now send directly
through amt_send_membership_query(), takes the IPv6 path with no change
to those functions.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 94 ++++++++++++++++++++++++++++++++++-------------
1 file changed, 68 insertions(+), 26 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index eaa5637..5e8a74c 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -88,6 +88,11 @@ static bool amt_v6(const struct amt_dev *amt)
return IS_ENABLED(CONFIG_IPV6) && !ipv6_addr_any(&amt->local_ipv6);
}
+static unsigned int amt_ip_hlen(const struct amt_dev *amt)
+{
+ return amt_v6(amt) ? sizeof(struct ipv6hdr) : sizeof(struct iphdr);
+}
+
/* Copy the outer source address of a received message by value, so that
* the caller may pull the skb afterwards.
*/
@@ -1098,6 +1103,65 @@ static void amt_req_work(struct work_struct *work)
msecs_to_jiffies(100));
}
+/* Route an AMT-encapsulated skb to @daddr and send it over the device's
+ * outer family. The caller has already pushed the AMT header.
+ */
+static int amt_udp_xmit(struct amt_dev *amt, struct sock *sk,
+ struct sk_buff *skb, const union amt_addr *daddr,
+ __be16 sport, __be16 dport)
+{
+ struct rtable *rt;
+ struct flowi4 fl4;
+
+ if (amt_v6(amt)) {
+ struct dst_entry *dst;
+
+ dst = amt_route6(amt, sk, &amt->local_ipv6, &daddr->ip6,
+ sport, dport);
+ if (IS_ERR(dst)) {
+ netdev_dbg(amt->dev, "no route to %pI6c\n",
+ &daddr->ip6);
+ return PTR_ERR(dst);
+ }
+ /* iptunnel_xmit() scrubs the IPv4 tunnel skb, but
+ * udp_tunnel6_xmit_skb() does not, so drop the inner
+ * packet's conntrack and extensions here. Links cannot
+ * cross netns, so this is never xnet.
+ */
+ skb_scrub_packet(skb, false);
+ udp_tunnel6_xmit_skb(dst, sk, skb, amt->dev, &amt->local_ipv6,
+ &daddr->ip6, AMT_TOS,
+ ip6_dst_hoplimit(dst), 0, sport, dport,
+ false, 0);
+ return 0;
+ }
+
+ memset(&fl4, 0, sizeof(struct flowi4));
+ fl4.flowi4_oif = amt->stream_dev->ifindex;
+ fl4.daddr = daddr->ip4;
+ fl4.saddr = amt->local_ip;
+ fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
+ fl4.flowi4_proto = IPPROTO_UDP;
+ rt = ip_route_output_key(amt->net, &fl4);
+ if (IS_ERR(rt)) {
+ netdev_dbg(amt->dev, "no route to %pI4\n", &daddr->ip4);
+ return PTR_ERR(rt);
+ }
+
+ udp_tunnel_xmit_skb(rt, sk, skb,
+ fl4.saddr,
+ fl4.daddr,
+ AMT_TOS,
+ ip4_dst_hoplimit(&rt->dst),
+ 0,
+ sport,
+ dport,
+ false,
+ false,
+ 0);
+ return 0;
+}
+
static bool amt_send_membership_update(struct amt_dev *amt,
struct sk_buff *skb,
bool v6)
@@ -1230,8 +1294,6 @@ static bool amt_send_membership_query(struct amt_dev *amt,
bool v6)
{
struct amt_header_membership_query *amtmq;
- struct rtable *rt;
- struct flowi4 fl4;
struct sock *sk;
int err;
@@ -1240,23 +1302,11 @@ static bool amt_send_membership_query(struct amt_dev *amt,
return true;
err = skb_cow_head(skb, LL_RESERVED_SPACE(amt->dev) + sizeof(*amtmq) +
- sizeof(struct iphdr) + sizeof(struct udphdr));
+ amt_ip_hlen(amt) + sizeof(struct udphdr));
if (err)
return true;
skb_reset_inner_headers(skb);
- memset(&fl4, 0, sizeof(struct flowi4));
- fl4.flowi4_oif = amt->stream_dev->ifindex;
- fl4.daddr = tunnel->addr.ip4;
- fl4.saddr = amt->local_ip;
- fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
- fl4.flowi4_proto = IPPROTO_UDP;
- rt = ip_route_output_key(amt->net, &fl4);
- if (IS_ERR(rt)) {
- netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->addr.ip4);
- return true;
- }
-
amtmq = skb_push(skb, sizeof(*amtmq));
amtmq->version = 0;
amtmq->type = AMT_MSG_MEMBERSHIP_QUERY;
@@ -1270,17 +1320,9 @@ static bool amt_send_membership_query(struct amt_dev *amt,
skb_set_inner_protocol(skb, htons(ETH_P_IP));
else
skb_set_inner_protocol(skb, htons(ETH_P_IPV6));
- udp_tunnel_xmit_skb(rt, sk, skb,
- fl4.saddr,
- fl4.daddr,
- AMT_TOS,
- ip4_dst_hoplimit(&rt->dst),
- 0,
- amt->relay_port,
- tunnel->source_port,
- false,
- false,
- 0);
+ if (amt_udp_xmit(amt, sk, skb, &tunnel->addr, amt->relay_port,
+ tunnel->source_port))
+ return true;
amt_update_relay_status(tunnel, AMT_STATUS_SENT_QUERY, true);
return false;
}
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 04/13] amt: send the Membership Query over IPv6
2026-10-09 12:24 ` [PATCH net-next 04/13] amt: send the Membership Query over IPv6 Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 0
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 branch of amt_udp_xmit() routes Membership Queries
without a traffic class but sends them with AMT_TOS.
--
amt: send the Membership Query over IPv6
This patch moves the route and transmit step of
amt_send_membership_query() into a new helper, amt_udp_xmit(). The helper
sends over the device's outer family: ip_route_output_key() and
udp_tunnel_xmit_skb() for IPv4, and amt_route6() and
udp_tunnel6_xmit_skb() for IPv6. The relay's General Query now uses the
IPv6 path when the device has an IPv6 outer transport.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index eaa5637674694..5e8a74cc0700e 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
[ ... ]
> @@ -1098,6 +1103,65 @@ static void amt_req_work(struct work_struct *work)
> msecs_to_jiffies(100));
> }
>
> +/* Route an AMT-encapsulated skb to @daddr and send it over the device's
> + * outer family. The caller has already pushed the AMT header.
> + */
> +static int amt_udp_xmit(struct amt_dev *amt, struct sock *sk,
> + struct sk_buff *skb, const union amt_addr *daddr,
> + __be16 sport, __be16 dport)
> +{
> + struct rtable *rt;
> + struct flowi4 fl4;
> +
> + if (amt_v6(amt)) {
> + struct dst_entry *dst;
> +
> + dst = amt_route6(amt, sk, &amt->local_ipv6, &daddr->ip6,
> + sport, dport);
[ ... ]
> + skb_scrub_packet(skb, false);
> + udp_tunnel6_xmit_skb(dst, sk, skb, amt->dev, &amt->local_ipv6,
> + &daddr->ip6, AMT_TOS,
> + ip6_dst_hoplimit(dst), 0, sport, dport,
> + false, 0);
[Severity: Medium]
Should the IPv6 route lookup also use AMT_TOS? The packet goes out with
AMT_TOS as its traffic class, but the route is looked up with a traffic
class of 0.
amt_route6() clears the flowi6 and never sets the flowlabel:
drivers/net/amt.c:amt_route6() {
memset(&fl6, 0, sizeof(fl6));
fl6.flowi6_oif = amt->stream_dev->ifindex;
fl6.flowi6_proto = IPPROTO_UDP;
fl6.daddr = *daddr;
fl6.saddr = *saddr;
fl6.fl6_dport = dport;
fl6.fl6_sport = sport;
return ip6_dst_lookup_flow(amt->net, sk, &fl6, NULL);
}
IPv6 policy routing takes the DSCP from fl6->flowlabel in
fib6_rule_match():
if ((r->dscp ^ ip6_dscp(fl6->flowlabel)) & r->dscp_mask)
return 0;
So a rule such as "ip -6 rule add dsfield 0xc0 table X" would never match
these Membership Queries. The lookup could then use a different table or
fail.
The IPv4 branch of the same helper does pass the DSCP into the lookup:
fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
udp_tunnel6_dst_lookup() also puts the dsfield into the lookup:
fl6.flowlabel = ip6_make_flowinfo(dsfield, key->label);
The commit message says:
Both use AMT_TOS, as the IPv4 path and the IPv6 control
messages do.
That is true for the transmitted header, but not for route selection.
amt_send_ctrl_v6() (Advertisement, Discovery, Request) has the same
mismatch, since it also routes through amt_route6(). The mismatch is
still there at the end of the series.
Could amt_route6() set something like
fl6.flowlabel = ip6_make_flowinfo(AMT_TOS, 0) so the lookup matches the
header that is sent?
> + return 0;
> + }
> +
> + memset(&fl4, 0, sizeof(struct flowi4));
> + fl4.flowi4_oif = amt->stream_dev->ifindex;
> + fl4.daddr = daddr->ip4;
> + fl4.saddr = amt->local_ip;
> + fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
> + fl4.flowi4_proto = IPPROTO_UDP;
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 05/13] amt: match the Membership Update tunnel by outer family
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (3 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 04/13] amt: send the Membership Query over IPv6 Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 06/13] amt: forward multicast data over IPv6 Omar Ramadan
` (7 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
amt_update_handler() finds the gateway's tunnel by comparing
tunnel->addr.ip4 with ip_hdr(skb)->saddr, which on an IPv6 outer header
reads bytes 4-7 of the source address as an IPv4 address, so an IPv6
relay never accepts a Membership Update.
Take the outer source with amt_outer_saddr() on entry, by value, before
pskb_may_pull() and iptunnel_pull_header() can reallocate the header.
Match the tunnel with amt_addr_equal(), as amt_request_handler() does,
so the tunnel a Request created is the one its Updates find in both
families. The inner report parsing is unchanged.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 5e8a74c..b977b00 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -2620,14 +2620,14 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
{
struct amt_header_membership_update *amtmu;
struct amt_tunnel_list *tunnel;
+ union amt_addr saddr;
struct ethhdr *eth;
struct iphdr *iph;
int len, hdr_size;
u64 response_mac;
- __be32 saddr;
__be32 nonce;
- saddr = ip_hdr(skb)->saddr;
+ amt_outer_saddr(amt, skb, &saddr);
hdr_size = sizeof(*amtmu) + sizeof(struct udphdr);
if (!pskb_may_pull(skb, hdr_size))
@@ -2646,7 +2646,7 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
skb_reset_network_header(skb);
list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
- if (tunnel->addr.ip4 == saddr) {
+ if (amt_addr_equal(&tunnel->addr, &saddr)) {
if ((nonce == tunnel->nonce &&
response_mac == tunnel->mac)) {
mod_delayed_work(amt_wq, &tunnel->gc_wq,
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 05/13] amt: match the Membership Update tunnel by outer family
2026-10-09 12:24 ` [PATCH net-next 05/13] amt: match the Membership Update tunnel by outer family Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
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 "an IPv6 relay never accepts a Membership
Update", as if this were a live, user-visible failure.
Pre-existing issues:
- [High] amt_update_handler() can re-arm the delayed work of a tunnel
that amt_tunnel_expire() is already freeing, leading to use-after-free…
--
amt: match the Membership Update tunnel by outer family
This patch changes amt_update_handler() to read the outer source address
with amt_outer_saddr() instead of ip_hdr(skb)->saddr. It also matches the
tunnel with amt_addr_equal(), as amt_request_handler() does, so the lookup
works for both outer address families.
> amt_update_handler() finds the gateway's tunnel by comparing
> tunnel->addr.ip4 with ip_hdr(skb)->saddr, which on an IPv6 outer header
> reads bytes 4-7 of the source address as an IPv4 address, so an IPv6
> relay never accepts a Membership Update.
[Severity: Low]
Can an IPv6 relay exist at this point in the series? amt_v6() is:
static bool amt_v6(const struct amt_dev *amt)
{
return IS_ENABLED(CONFIG_IPV6) && !ipv6_addr_any(&amt->local_ipv6);
}
Nothing assigns amt->local_ipv6 at this commit. The first assignment is
in amt_newlink(), added later by "amt: add netlink attributes for an IPv6
outer transport".
Until then, amt_outer_saddr() always takes the IPv4 path. That makes
amt_addr_equal() in amt_update_handler() give the same result as the old
tunnel->addr.ip4 == saddr comparison.
This isn't a bug, but could the commit message say that nothing changes
until the netlink attribute is added? The first patch of the series does
this.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index 5e8a74cc0700e..b977b00a6fd55 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
[ ... ]
> @@ -2646,7 +2646,7 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
> skb_reset_network_header(skb);
>
> list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
> - if (tunnel->addr.ip4 == saddr) {
> + if (amt_addr_equal(&tunnel->addr, &saddr)) {
> if ((nonce == tunnel->nonce &&
> response_mac == tunnel->mac)) {
> mod_delayed_work(amt_wq, &tunnel->gc_wq,
[Severity: High]
This is a pre-existing issue and this patch did not introduce it. Can
this mod_delayed_work() re-arm the gc_wq of a tunnel that
amt_tunnel_expire() is already freeing?
amt_update_handler() is called from amt_rcv() under rcu_read_lock_bh().
It does not hold amt->lock or tunnel->lock, and it does not check whether
the tunnel is being torn down.
amt_tunnel_expire() runs from that same gc_wq:
drivers/net/amt.c:amt_tunnel_expire() {
spin_lock_bh(&amt->lock);
rcu_read_lock();
list_del_rcu(&tunnel->list);
amt->nr_tunnels--;
amt_clear_groups(tunnel);
rcu_read_unlock();
spin_unlock_bh(&amt->lock);
kfree_rcu(tunnel, rcu);
}
Consider this ordering for a tunnel T:
CPU1 (amt_wq)
amt_tunnel_expire(T) starts, T->gc_wq is no longer pending
CPU2 (amt_rcv, rcu_read_lock_bh held)
amt_update_handler()
list_for_each_entry_rcu() finds T before list_del_rcu()
mod_delayed_work(amt_wq, &T->gc_wq, ...) arms T->gc_wq.timer
CPU1
list_del_rcu(&T->list);
kfree_rcu(T, rcu);
After CPU2 leaves its RCU section, the grace period ends and T is freed.
T->gc_wq.timer is still queued at that point.
When the timer fires, amt_tunnel_expire() runs on the freed tunnel. It
calls list_del_rcu() a second time, decrements nr_tunnels again, and
calls kfree_rcu() a second time.
The report: path has a similar problem. It takes tunnel->lock and calls
amt_igmp_report_handler() or amt_mld_report_handler(), which can add
group nodes to T after amt_clear_groups() has already run. Wouldn't those
nodes be left with live timers that still reference T after it is freed?
A remote gateway can trigger this. It only needs a valid nonce and
response MAC, and the timing is predictable because gc_wq fires 3 * GMI
after the last Update.
IPv4 relays could already hit this. Once the series is complete, this
patch makes the same path reachable for IPv6 relays. The code is still
the same at the end of the series.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 06/13] amt: forward multicast data over IPv6
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (4 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 05/13] amt: match the Membership Update tunnel by outer family Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 07/13] amt: size the encapsulation headroom by the outer IP version Omar Ramadan
` (6 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
amt_send_multicast_data() routes and transmits the relay's copy of a
multicast packet only over IPv4, so an IPv6 relay could establish
tunnels but not deliver traffic through them.
Size the copy's headroom for the outer family and send it through
amt_udp_xmit(), whose new @data argument marks Multicast Data, still
routed without a DSCP. A copy that cannot be sent is now counted in
tx_dropped and freed with a specific drop reason; before, it was freed
with SKB_DROP_REASON_NOT_SPECIFIED and not counted, as amt_dev_xmit()
returns NETDEV_TX_OK for the packet.
RFC 7450 forbids a Fragment header on Multicast Data sent over IPv6
(s5.3.3.6.3.2) and wants an IPv6 payload above the tunnel MTU dropped,
with a Packet Too Big to its source (s5.3.3.6.2.2). ip6_fragment() would
drop it, but send the Packet Too Big to the relay itself. Instead,
amt_tmtu_exceeded() compares the payload with the route's MTU less the
outer headers, per tunnel since each gateway has its own route, and
sends the Packet Too Big through icmpv6_ndo_send(), as ip6_tunnel does,
with at least IPV6_MIN_MTU, as a source ignores less. Packet Too Big is
exempt from the ICMPv6 rate limit unless the icmpv6_ratemask sysctl
includes it, so a packet draws one for each gateway with a smaller path
MTU. skb_tunnel_check_pmtu() is not used: with reply false it sends
nothing. A GSO skb, which a UDP_SEGMENT source sends once transmit
checksum offload is on, is judged by its segments, and marking it as a
UDP tunnel packet would leave the inner UDP header out of them, so the
marking moves here from the caller, after the check.
The tunnel MTU follows the route's path MTU, as on every IPv6 UDP
tunnel. RFC 7450 s5.3.3.6.1 asks for a way to turn that off, but IPv6
has none: udpv6_err() lowers the route MTU before it looks up a socket,
and "mtu lock" does not stop it. A forged Packet Too Big can lower the
route MTU to IPV6_MIN_MTU at most. An IPv4 payload that does not fit an
IPv6 tunnel is dropped without an ICMP error, as icmp_send() does not
answer multicast, and it is not fragmented before encapsulation as
s5.3.3.6.2.1 asks for DF=0; the IPv4 tunnel does not do that either.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 93 +++++++++++++++++++++++++++++------------------
1 file changed, 58 insertions(+), 35 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index b977b00..412d23c 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -1103,15 +1103,43 @@ static void amt_req_work(struct work_struct *work)
msecs_to_jiffies(100));
}
+/* RFC 7450 s5.3.3.6: Multicast Data must not be fragmented over IPv6, so
+ * a payload above the tunnel MTU is dropped, and an IPv6 source is sent a
+ * Packet Too Big of at least IPV6_MIN_MTU, as it ignores less. icmp_send()
+ * never answers an IPv4 multicast datagram. A GSO skb is judged by its
+ * segments, so @skb must not be marked as a tunnel packet yet:
+ * skb->encapsulation would leave the inner UDP header out of their size.
+ */
+static bool amt_tmtu_exceeded(struct sk_buff *skb,
+ const struct dst_entry *dst)
+{
+ int off = skb_network_offset(skb);
+ u32 mtu;
+
+ mtu = dst_mtu(dst) - sizeof(struct ipv6hdr) - sizeof(struct udphdr) -
+ off;
+ if (skb_is_gso(skb) ? skb_gso_validate_network_len(skb, mtu) :
+ skb->len - off <= mtu)
+ return false;
+
+ if (skb->protocol == htons(ETH_P_IPV6))
+ icmpv6_ndo_send(skb, ICMPV6_PKT_TOOBIG, 0,
+ max_t(u32, mtu, IPV6_MIN_MTU));
+ return true;
+}
+
/* Route an AMT-encapsulated skb to @daddr and send it over the device's
- * outer family. The caller has already pushed the AMT header.
+ * outer family. The caller has already pushed the AMT header. @data marks
+ * Multicast Data, which may be a GSO skb: it gets the IPv6 tunnel MTU
+ * check, then the UDP tunnel offload marking.
*/
static int amt_udp_xmit(struct amt_dev *amt, struct sock *sk,
struct sk_buff *skb, const union amt_addr *daddr,
- __be16 sport, __be16 dport)
+ __be16 sport, __be16 dport, bool data)
{
struct rtable *rt;
struct flowi4 fl4;
+ int err;
if (amt_v6(amt)) {
struct dst_entry *dst;
@@ -1123,6 +1151,14 @@ static int amt_udp_xmit(struct amt_dev *amt, struct sock *sk,
&daddr->ip6);
return PTR_ERR(dst);
}
+ if (data) {
+ err = amt_tmtu_exceeded(skb, dst) ? -EMSGSIZE :
+ udp_tunnel_handle_offloads(skb, true);
+ if (err) {
+ dst_release(dst);
+ return err;
+ }
+ }
/* iptunnel_xmit() scrubs the IPv4 tunnel skb, but
* udp_tunnel6_xmit_skb() does not, so drop the inner
* packet's conntrack and extensions here. Links cannot
@@ -1136,11 +1172,17 @@ static int amt_udp_xmit(struct amt_dev *amt, struct sock *sk,
return 0;
}
+ if (data) {
+ err = udp_tunnel_handle_offloads(skb, true);
+ if (err)
+ return err;
+ }
+
memset(&fl4, 0, sizeof(struct flowi4));
fl4.flowi4_oif = amt->stream_dev->ifindex;
fl4.daddr = daddr->ip4;
fl4.saddr = amt->local_ip;
- fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
+ fl4.flowi4_dscp = data ? 0 : inet_dsfield_to_dscp(AMT_TOS);
fl4.flowi4_proto = IPPROTO_UDP;
rt = ip_route_output_key(amt->net, &fl4);
if (IS_ERR(rt)) {
@@ -1229,16 +1271,14 @@ static void amt_send_multicast_data(struct amt_dev *amt,
{
struct amt_header_mcast_data *amtmd;
struct sk_buff *skb;
- struct iphdr *iph;
- struct flowi4 fl4;
- struct rtable *rt;
struct sock *sk;
+ int err;
sk = rcu_dereference_bh(amt->sk);
if (!sk)
return;
- skb = skb_copy_expand(oskb, sizeof(*amtmd) + sizeof(*iph) +
+ skb = skb_copy_expand(oskb, sizeof(*amtmd) + amt_ip_hlen(amt) +
sizeof(struct udphdr), 0, GFP_ATOMIC);
if (!skb)
return;
@@ -1249,22 +1289,6 @@ static void amt_send_multicast_data(struct amt_dev *amt,
*/
skb_reset_mac_header(skb);
skb_reset_inner_headers(skb);
- if (udp_tunnel_handle_offloads(skb, true)) {
- kfree_skb(skb);
- return;
- }
-
- memset(&fl4, 0, sizeof(struct flowi4));
- fl4.flowi4_oif = amt->stream_dev->ifindex;
- fl4.daddr = tunnel->addr.ip4;
- fl4.saddr = amt->local_ip;
- fl4.flowi4_proto = IPPROTO_UDP;
- rt = ip_route_output_key(amt->net, &fl4);
- if (IS_ERR(rt)) {
- netdev_dbg(amt->dev, "no route to %pI4\n", &tunnel->addr.ip4);
- kfree_skb(skb);
- return;
- }
amtmd = skb_push(skb, sizeof(*amtmd));
amtmd->version = 0;
@@ -1275,17 +1299,16 @@ static void amt_send_multicast_data(struct amt_dev *amt,
skb_set_inner_protocol(skb, htons(ETH_P_IP));
else
skb_set_inner_protocol(skb, htons(ETH_P_IPV6));
- udp_tunnel_xmit_skb(rt, sk, skb,
- fl4.saddr,
- fl4.daddr,
- AMT_TOS,
- ip4_dst_hoplimit(&rt->dst),
- 0,
- amt->relay_port,
- tunnel->source_port,
- false,
- false,
- 0);
+ err = amt_udp_xmit(amt, sk, skb, &tunnel->addr, amt->relay_port,
+ tunnel->source_port, true);
+ if (err) {
+ DEV_STATS_INC(amt->dev, tx_dropped);
+ kfree_skb_reason(skb, err == -EMSGSIZE ?
+ SKB_DROP_REASON_PKT_TOO_BIG :
+ err == -ENOMEM ?
+ SKB_DROP_REASON_NOMEM :
+ SKB_DROP_REASON_IP_OUTNOROUTES);
+ }
}
static bool amt_send_membership_query(struct amt_dev *amt,
@@ -1321,7 +1344,7 @@ static bool amt_send_membership_query(struct amt_dev *amt,
else
skb_set_inner_protocol(skb, htons(ETH_P_IPV6));
if (amt_udp_xmit(amt, sk, skb, &tunnel->addr, amt->relay_port,
- tunnel->source_port))
+ tunnel->source_port, false))
return true;
amt_update_relay_status(tunnel, AMT_STATUS_SENT_QUERY, true);
return false;
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 06/13] amt: forward multicast data over IPv6
2026-10-09 12:24 ` [PATCH net-next 06/13] amt: forward multicast data over IPv6 Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 0 · 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] amt_tmtu_exceeded() (drivers/net/amt.c) sends one ICMPv6 Packet
Too Big for every tunnel that a single IPv6 multicast packet is too…
- [Low] amt_tmtu_exceeded() decides whether the inner packet is IPv6, and
so whether to send an ICMPv6 Packet Too Big, from skb->protocol (line…
--
amt: forward multicast data over IPv6
This patch makes the relay send its copy of a multicast packet through
amt_udp_xmit(), so traffic can be delivered over an IPv6 outer transport.
On IPv6 tunnels, a new helper, amt_tmtu_exceeded(), drops any payload
above the tunnel MTU and sends an ICMPv6 Packet Too Big to the source.
A copy that cannot be sent is now counted in tx_dropped and freed with a
specific drop reason.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index b977b00a6fd55..412d23cc62599 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
> @@ -1103,15 +1103,43 @@ static void amt_req_work(struct work_struct *work)
[ ... ]
> +static bool amt_tmtu_exceeded(struct sk_buff *skb,
> + const struct dst_entry *dst)
> +{
> + int off = skb_network_offset(skb);
> + u32 mtu;
> +
> + mtu = dst_mtu(dst) - sizeof(struct ipv6hdr) - sizeof(struct udphdr) -
> + off;
> + if (skb_is_gso(skb) ? skb_gso_validate_network_len(skb, mtu) :
> + skb->len - off <= mtu)
> + return false;
> +
> + if (skb->protocol == htons(ETH_P_IPV6))
> + icmpv6_ndo_send(skb, ICMPV6_PKT_TOOBIG, 0,
> + max_t(u32, mtu, IPV6_MIN_MTU));
[Severity: High]
Can one multicast packet cause up to max_tunnels Packet Too Big messages
to be sent to its source here?
amt_dev_xmit() walks the tunnel list and sends one copy to each tunnel
that joined the group. Each copy can then send its own Packet Too Big:
amt_dev_xmit()
list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
amt_send_multicast_data()
amt_udp_xmit(..., true)
amt_tmtu_exceeded()
icmpv6_ndo_send(skb, ICMPV6_PKT_TOOBIG, ...)
By default, nothing limits the rate of these messages.
inet6_net_init() clears ICMPV6_PKT_TOOBIG from icmpv6_ratemask, so
icmp6_send() skips both icmpv6_global_allow() and icmpv6_xrlim_allow().
amt_request_handler() creates a tunnel for any new outer source address,
with no authentication, up to max_tunnels. The commit message also notes
that a gateway can lower its own route MTU to IPV6_MIN_MTU with a Packet
Too Big that udpv6_err() accepts.
So a remote party with max_tunnels IPv6 addresses (one /64 is enough)
could join a group and lower the route MTU of each tunnel. After that,
every datagram on that group above about 1230 bytes would cause up to
max_tunnels Packet Too Big messages of about 1280 bytes each, all sent to
the multicast source.
Does this ever stop? The check uses dst_mtu() minus the outer headers,
which is about 1230 here. The reported MTU is clamped to IPV6_MIN_MTU,
which is 1280. A compliant source cannot shrink its packets below 1280,
so its packets or first fragments keep exceeding the tunnel MTU for as
long as the attacker keeps its tunnels and route MTU entries alive.
The commit message says "so a packet draws one for each gateway with a
smaller path MTU", but it does not say that untrusted gateways control
how many there are. Relays can only be created as IPv6 once a later
netlink patch in this series lands. The final state of the series still
sends one Packet Too Big per tunnel.
Would it work to send at most one Packet Too Big per original skb,
carrying the smallest failing tunnel MTU (for example tracked in
amt_dev_xmit())?
[Severity: Low]
Should this check use the inner family the caller already parsed, rather
than skb->protocol?
amt_dev_xmit() sets v6 and data from iph->version and never checks
skb->protocol:
iph = ip_hdr(skb);
if (iph->version == 4) {
...
if (!ip_mc_check_igmp(skb)) {
...
} else {
data = true;
}
If skb->protocol does not match the contents, ip_mc_check_igmp() and
ipv6_mc_check_mld() return -EINVAL. The else branch then still marks the
packet as data and forwards it, and skb_copy_expand() keeps the wrong
protocol.
Take an over-MTU skb labelled ETH_P_IPV6 that holds an IPv4 packet.
icmpv6_ndo_send() would read the IPv4/UDP header bytes as a struct
ipv6hdr, and the Packet Too Big would go to an address built from those
bytes. In the opposite case, a real IPv6 source would get no Packet Too
Big at all.
Reaching this needs something like AF_PACKET with an arbitrary
sll_protocol, or a tc/BPF redirect. Could the v6 flag from
amt_send_multicast_data() be passed down to amt_tmtu_exceeded() instead?
> + return true;
> +}
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 07/13] amt: size the encapsulation headroom by the outer IP version
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (5 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 06/13] amt: forward multicast data over IPv6 Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-09 12:24 ` [PATCH net-next 08/13] amt: send the AMT gateway control plane over IPv6 Omar Ramadan
` (5 subsequent siblings)
12 siblings, 0 replies; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
The amt device reserves AMT_RELAY_HLEN or AMT_GW_HLEN of headroom and
subtracts the same amount from the stream device's MTU. Both assume a
20-byte IPv4 outer header. An IPv6 outer header is 40 bytes, so an IPv6
relay's MTU admits Multicast Data up to 10 bytes too long for the stream
device once encapsulated, which the relay's tunnel MTU check then drops.
Compute the headroom in amt_hlen() from amt_ip_hlen() and the AMT
headers of the mode, and use it wherever the device sizes its headroom
and MTU: once for both modes at newlink, and on NETDEV_CHANGEMTU of the
stream device. AMT_RELAY_HLEN and AMT_GW_HLEN have no other user and
are removed.
No functional change: amt_v6() is still false for every device.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 31 +++++++++++++++----------------
include/net/amt.h | 6 ------
2 files changed, 15 insertions(+), 22 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 412d23c..969ecfe 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -93,6 +93,16 @@ static unsigned int amt_ip_hlen(const struct amt_dev *amt)
return amt_v6(amt) ? sizeof(struct ipv6hdr) : sizeof(struct iphdr);
}
+/* Headroom the device reserves for its outer encapsulation. */
+static unsigned int amt_hlen(const struct amt_dev *amt)
+{
+ unsigned int hlen = amt_ip_hlen(amt) + sizeof(struct udphdr);
+
+ if (amt->mode == AMT_MODE_RELAY)
+ return hlen + sizeof(struct amt_relay_headers);
+ return hlen + sizeof(struct amt_gw_headers);
+}
+
/* Copy the outer source address of a received message by value, so that
* the caller may pull the skb afterwards.
*/
@@ -3513,11 +3523,6 @@ static int amt_newlink(struct net_device *dev,
if (amt->mode == AMT_MODE_RELAY) {
amt->qrv = READ_ONCE(amt->net->ipv4.sysctl_igmp_qrv);
amt->qri = 10;
- dev->needed_headroom = amt->stream_dev->needed_headroom +
- AMT_RELAY_HLEN;
- dev->mtu = amt->stream_dev->mtu - AMT_RELAY_HLEN;
- dev->max_mtu = dev->mtu;
- dev->min_mtu = ETH_MIN_MTU + AMT_RELAY_HLEN;
} else {
if (!data[IFLA_AMT_DISCOVERY_IP]) {
NL_SET_ERR_MSG_ATTR(extack, tb[IFLA_AMT_DISCOVERY_IP],
@@ -3538,13 +3543,11 @@ static int amt_newlink(struct net_device *dev,
"discovery must be unicast");
goto err;
}
-
- dev->needed_headroom = amt->stream_dev->needed_headroom +
- AMT_GW_HLEN;
- dev->mtu = amt->stream_dev->mtu - AMT_GW_HLEN;
- dev->max_mtu = dev->mtu;
- dev->min_mtu = ETH_MIN_MTU + AMT_GW_HLEN;
}
+ dev->needed_headroom = amt->stream_dev->needed_headroom + amt_hlen(amt);
+ dev->mtu = amt->stream_dev->mtu - amt_hlen(amt);
+ dev->max_mtu = dev->mtu;
+ dev->min_mtu = ETH_MIN_MTU + amt_hlen(amt);
amt->qi = AMT_INIT_QUERY_INTERVAL;
err = register_netdevice(dev);
@@ -3676,11 +3679,7 @@ static int amt_device_event(struct notifier_block *unused,
unregister_netdevice_many(&list);
break;
case NETDEV_CHANGEMTU:
- if (amt->mode == AMT_MODE_RELAY)
- new_mtu = dev->mtu - AMT_RELAY_HLEN;
- else
- new_mtu = dev->mtu - AMT_GW_HLEN;
-
+ new_mtu = dev->mtu - amt_hlen(amt);
dev_set_mtu(amt->dev, new_mtu);
break;
}
diff --git a/include/net/amt.h b/include/net/amt.h
index 77d17fc..d8798a9 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -398,12 +398,6 @@ struct amt_dev {
#define IANA_AMT_UDP_PORT 2268
#define AMT_MAX_TUNNELS 128
#define AMT_MAX_REQS 128
-#define AMT_GW_HLEN (sizeof(struct iphdr) + \
- sizeof(struct udphdr) + \
- sizeof(struct amt_gw_headers))
-#define AMT_RELAY_HLEN (sizeof(struct iphdr) + \
- sizeof(struct udphdr) + \
- sizeof(struct amt_relay_headers))
static inline bool netif_is_amt(const struct net_device *dev)
{
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* [PATCH net-next 08/13] amt: send the AMT gateway control plane over IPv6
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (6 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 07/13] amt: size the encapsulation headroom by the outer IP version Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 09/13] amt: receive " Omar Ramadan
` (4 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
A gateway sends three control messages: the Relay Discovery to its
discovery address, then the Request and the Membership Updates to the
relay it learned. All three are built for an IPv4 outer header only, so
a gateway on an IPv6-only access network cannot reach any relay.
Send the Discovery and the Request of a gateway with an IPv6 local
address from amt_send_discovery_v6() and amt_send_request_v6(). Both
messages are a few bytes with no payload, so they are built on the stack
and sent with amt_send_ctrl_v6(), the helper that already sends the
relay's IPv6 Advertisement, rather than with a copy of the IPv4 skb
construction. The Membership Update carries the gateway's IGMP or MLD
report, so amt_send_membership_update() keeps building on that skb: it
sizes the headroom for the outer family and sends through
amt_udp_xmit(), like the relay's Membership Query and Multicast Data,
which avoids an IPv6 copy of the function.
struct amt_dev gains the gateway's IPv6 discovery address and the IPv6
relay address it learns and sends to. The next patch writes the relay
address in process context while the transmit and receive paths read it,
and a struct in6_addr is not read in one access, so it comes with a
seqlock, remote_ipv6_lock: the senders take a snapshot with
amt_get_remote_ipv6(), which retries until the copy is consistent.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 91 +++++++++++++++++++++++++++++++++--------------
include/net/amt.h | 5 +++
2 files changed, 69 insertions(+), 27 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 969ecfe..148d1fb 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -715,6 +715,50 @@ out:
return err;
}
+/* The learned IPv6 relay address is written in process context and read
+ * on transmit and receive. A struct in6_addr is not read in one access, so
+ * readers take a snapshot under the seqlock.
+ */
+static struct in6_addr amt_get_remote_ipv6(const struct amt_dev *amt)
+{
+ struct in6_addr addr;
+ unsigned int seq;
+
+ do {
+ seq = read_seqbegin(&amt->remote_ipv6_lock);
+ addr = amt->remote_ipv6;
+ } while (read_seqretry(&amt->remote_ipv6_lock, seq));
+ return addr;
+}
+
+/* IPv6-outer variant of amt_send_discovery(). */
+static void amt_send_discovery_v6(struct amt_dev *amt)
+{
+ struct amt_header_discovery amtd = {
+ .type = AMT_MSG_DISCOVERY,
+ .nonce = amt->nonce,
+ };
+
+ if (!amt_send_ctrl_v6(amt, &amt->local_ipv6, &amt->discovery_ipv6,
+ amt->gw_port, amt->relay_port,
+ &amtd, sizeof(amtd)))
+ amt_update_gw_status(amt, AMT_STATUS_SENT_DISCOVERY, true);
+}
+
+/* IPv6-outer variant of amt_send_request(); @v6 is the inner family. */
+static void amt_send_request_v6(struct amt_dev *amt, bool v6)
+{
+ const struct in6_addr remote = amt_get_remote_ipv6(amt);
+ struct amt_header_request amtrh = {
+ .type = AMT_MSG_REQUEST,
+ .p = v6,
+ .nonce = amt->nonce,
+ };
+
+ amt_send_ctrl_v6(amt, &amt->local_ipv6, &remote, amt->gw_port,
+ amt->relay_port, &amtrh, sizeof(amtrh));
+}
+
static void amt_send_discovery(struct amt_dev *amt)
{
struct amt_header_discovery *amtd;
@@ -728,6 +772,11 @@ static void amt_send_discovery(struct amt_dev *amt)
u32 len;
int err;
+ if (amt_v6(amt)) {
+ amt_send_discovery_v6(amt);
+ return;
+ }
+
rcu_read_lock();
sk = rcu_dereference(amt->sk);
if (!sk)
@@ -818,6 +867,11 @@ static void amt_send_request(struct amt_dev *amt, bool v6)
u32 len;
int err;
+ if (amt_v6(amt)) {
+ amt_send_request_v6(amt, v6);
+ return;
+ }
+
rcu_read_lock();
remote_ip = READ_ONCE(amt->remote_ip);
sk = rcu_dereference(amt->sk);
@@ -1220,9 +1274,7 @@ static bool amt_send_membership_update(struct amt_dev *amt,
{
__be32 remote_ip = READ_ONCE(amt->remote_ip);
struct amt_header_membership_update *amtmu;
- struct iphdr *iph;
- struct flowi4 fl4;
- struct rtable *rt;
+ union amt_addr remote = {};
struct sock *sk;
int err;
@@ -1231,23 +1283,11 @@ static bool amt_send_membership_update(struct amt_dev *amt,
return true;
err = skb_cow_head(skb, LL_RESERVED_SPACE(amt->dev) + sizeof(*amtmu) +
- sizeof(*iph) + sizeof(struct udphdr));
+ amt_ip_hlen(amt) + sizeof(struct udphdr));
if (err)
return true;
skb_reset_inner_headers(skb);
- memset(&fl4, 0, sizeof(struct flowi4));
- fl4.flowi4_oif = amt->stream_dev->ifindex;
- fl4.daddr = remote_ip;
- fl4.saddr = amt->local_ip;
- fl4.flowi4_dscp = inet_dsfield_to_dscp(AMT_TOS);
- fl4.flowi4_proto = IPPROTO_UDP;
- rt = ip_route_output_key(amt->net, &fl4);
- if (IS_ERR(rt)) {
- netdev_dbg(amt->dev, "no route to %pI4\n", &remote_ip);
- return true;
- }
-
amtmu = skb_push(skb, sizeof(*amtmu));
amtmu->version = 0;
amtmu->type = AMT_MSG_MEMBERSHIP_UPDATE;
@@ -1259,17 +1299,13 @@ static bool amt_send_membership_update(struct amt_dev *amt,
skb_set_inner_protocol(skb, htons(ETH_P_IP));
else
skb_set_inner_protocol(skb, htons(ETH_P_IPV6));
- udp_tunnel_xmit_skb(rt, sk, skb,
- fl4.saddr,
- fl4.daddr,
- AMT_TOS,
- ip4_dst_hoplimit(&rt->dst),
- 0,
- amt->gw_port,
- amt->relay_port,
- false,
- false,
- 0);
+ if (amt_v6(amt))
+ remote.ip6 = amt_get_remote_ipv6(amt);
+ else
+ remote.ip4 = remote_ip;
+ if (amt_udp_xmit(amt, sk, skb, &remote, amt->gw_port,
+ amt->relay_port, false))
+ return true;
amt_update_gw_status(amt, AMT_STATUS_SENT_UPDATE, true);
return false;
}
@@ -3481,6 +3517,7 @@ static int amt_newlink(struct net_device *dev,
amt->max_tunnels = AMT_MAX_TUNNELS;
spin_lock_init(&amt->lock);
+ seqlock_init(&amt->remote_ipv6_lock);
amt->max_groups = AMT_MAX_GROUP;
amt->max_sources = AMT_MAX_SOURCE;
amt->hash_buckets = AMT_HSIZE;
diff --git a/include/net/amt.h b/include/net/amt.h
index d8798a9..02c1c33 100644
--- a/include/net/amt.h
+++ b/include/net/amt.h
@@ -360,8 +360,13 @@ struct amt_dev {
struct in6_addr local_ipv6;
/* Outer remote ip */
__be32 remote_ip;
+ /* Outer remote IPv6 address, published under remote_ipv6_lock */
+ struct in6_addr remote_ipv6;
+ seqlock_t remote_ipv6_lock;
/* Outer discovery ip */
__be32 discovery_ip;
+ /* Outer discovery IPv6 address, :: unless an IPv6 gateway */
+ struct in6_addr discovery_ipv6;
/* Only used in gateway mode */
__be32 nonce;
/* Gateway sent request and received query */
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 08/13] amt: send the AMT gateway control plane over IPv6
2026-10-09 12:24 ` [PATCH net-next 08/13] amt: send the AMT gateway control plane over IPv6 Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
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] Commit-message concern for "amt: send the AMT gateway control
plane over IPv6": none of the new IPv6 gateway sender code can run at…
- [Low] At this commit the new IPv6 gateway senders use discovery_ipv6
and remote_ipv6, but the receive side is still IPv4-only.
--
amt: send the AMT gateway control plane over IPv6
This commit adds amt_send_discovery_v6() and amt_send_request_v6(). They
send the gateway Relay Discovery and Request over an IPv6 outer header
through amt_send_ctrl_v6(). It also changes amt_send_membership_update() to
size its headroom for the outer family and send through amt_udp_xmit(), and
adds discovery_ipv6, remote_ipv6 and the remote_ipv6_lock seqlock to struct
amt_dev.
> Send the Discovery and the Request of a gateway with an IPv6 local
> address from amt_send_discovery_v6() and amt_send_request_v6(). Both
> messages are a few bytes with no payload, so they are built on the stack
> and sent with amt_send_ctrl_v6(), the helper that already sends the
> relay's IPv6 Advertisement, rather than with a copy of the IPv4 skb
> construction. The Membership Update carries the gateway's IGMP or MLD
> report, so amt_send_membership_update() keeps building on that skb: it
> sizes the headroom for the outer family and sends through
> amt_udp_xmit(), like the relay's Membership Query and Multicast Data,
> which avoids an IPv6 copy of the function.
[Severity: Low]
This isn't a bug, but could the commit message say that none of the new
IPv6 sender code can run yet?
At this commit nothing writes local_ipv6, so amt_v6() is always false:
drivers/net/amt.c:amt_v6() {
return IS_ENABLED(CONFIG_IPV6) && !ipv6_addr_any(&amt->local_ipv6);
}
Nothing writes discovery_ipv6 or remote_ipv6 here either. Later patches
fill them in: "amt: receive the AMT gateway control plane over IPv6" and
"amt: add netlink attributes for an IPv6 outer transport".
Earlier patches in the series include a line like "No functional change:
amt_v6() is still false for every device". The first patch also says
"Nothing sets local_ipv6 until the netlink attribute added at the end of
this series". Would a similar line fit here?
The message also leaves out that the IPv4 path of
amt_send_membership_update() now goes through amt_udp_xmit(). As far as I
can tell the behaviour is the same:
- With data=false, amt_udp_xmit() builds the same flowi4 as the removed
code and calls udp_tunnel_xmit_skb() with the same arguments.
- amt_ip_hlen() still reserves sizeof(struct iphdr).
- amt_dev_xmit() frees the skb on a true return, whether the route lookup
fails before or after the skb_push().
Could this be described as a refactor with no IPv4 behaviour change?
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index 969ecfe81d7ad..148d1fb369154 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
> @@ -715,6 +715,50 @@ static int amt_send_ctrl_v6(struct amt_dev *amt, const struct in6_addr *saddr,
> return err;
> }
>
> +/* The learned IPv6 relay address is written in process context and read
> + * on transmit and receive. A struct in6_addr is not read in one access, so
> + * readers take a snapshot under the seqlock.
> + */
[Severity: Low]
Is the "read on transmit and receive" part accurate at this commit?
The only readers added here are amt_send_request_v6() and the IPv6 branch
of amt_send_membership_update(). In gateway mode, amt_rcv() still checks
the outer source against the IPv4 addresses only:
drivers/net/amt.c:amt_rcv() {
...
if (saddr != amt->discovery_ip) {
...
if (saddr != remote_ip) {
...
}
amt_advertisement_handler() also learns only the IPv4 relay address:
WRITE_ONCE(amt->remote_ip, amta->ip4);
So nothing writes remote_ipv6 yet, and nothing on the receive side reads
it.
The next patch, "amt: receive the AMT gateway control plane over IPv6",
adds amt_from_relay() and amt_set_remote_ipv6(). After that patch the
comment is accurate.
amt_v6() stays false until the netlink patch, which comes after the
receive patch. So this can't be hit in any bisectable state. The comment
just describes code that arrives one commit later. Would it be clearer to
add the receive part of the comment in the next patch?
> +static struct in6_addr amt_get_remote_ipv6(const struct amt_dev *amt)
> +{
> + struct in6_addr addr;
> + unsigned int seq;
[ ... ]
> +/* IPv6-outer variant of amt_send_request(); @v6 is the inner family. */
> +static void amt_send_request_v6(struct amt_dev *amt, bool v6)
> +{
> + const struct in6_addr remote = amt_get_remote_ipv6(amt);
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 09/13] amt: receive the AMT gateway control plane over IPv6
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (7 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 08/13] amt: send the AMT gateway control plane over IPv6 Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 10/13] amt: add netlink attributes for an IPv6 outer transport Omar Ramadan
` (3 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
An IPv6 gateway now sends its Discovery, Request and Updates over IPv6,
but it cannot take the replies. amt_rcv() checks the outer source of an
Advertisement against the discovery address, and that of Multicast Data
and a Membership Query against the learned relay address, by reading
ip_hdr(skb)->saddr, which on an IPv6 outer header is bytes 4-7 of the
source address, so only 32 bits of it are compared.
amt_advertisement_handler() parses only the IPv4 form, so the gateway
never learns an IPv6 relay address.
amt_rcv() now takes the outer source with amt_outer_saddr() before
amt_parse_type() pulls, and checks it in the device's family through
amt_from_relay(). On an IPv6 gateway amt_advertisement_handler() pulls
the 24-byte IPv6 form, whose header and nonce are laid out as in the
IPv4 one, and refuses an unspecified, loopback, multicast or
IPv4-mapped relay address, the IPv6 counterparts of the checks on the
IPv4 address; the V6ONLY socket could not reach an IPv4-mapped relay.
amt_set_remote_ipv6() publishes the learned address under
remote_ipv6_lock. amt_clear_remote() forgets the relay in both families
and replaces the IPv4-only reset in amt_event_send_request(),
amt_dev_open() and amt_dev_stop(), so a gateway taken down and up again
neither accepts traffic from its previous relay's IPv6 address nor
keeps reporting it.
IPv6 input delivers a packet from ::. On a gateway that has lost its
relay it would match the cleared relay address and, with a zero nonce,
could inject a Membership Query, and a relay would create a tunnel for
it. amt_rcv() drops a :: source, and amt_from_relay() matches nothing
against a relay address that is not known, in either family: IPv4
input delivers a 0.0.0.0 source too, when the packet is sent to
255.255.255.255.
RFC 7450 s5.2.3.3 says a gateway that processes Multicast Data itself,
as amt_rcv() does, must not discard it for a zero UDP checksum, which
s5.1.6 lets a relay send, the RFC 6936 exception to RFC 8200 s8.1. An
IPv6 gateway's socket therefore accepts a zero checksum and still
verifies a non-zero one. amt_rcv() drops a zero checksum on every other
message a gateway receives, as RFC 6936 s5 asks, and a relay's socket
keeps requiring the checksum.
The Membership Query and Multicast Data handlers work on the payload
behind the outer header and need no change.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 116 +++++++++++++++++++++++++++++++++++++---------
1 file changed, 94 insertions(+), 22 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 148d1fb..86f168c 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -116,6 +116,16 @@ static void amt_outer_saddr(const struct amt_dev *amt,
addr->ip4 = ip_hdr(skb)->saddr;
}
+/* An IPv6 address no relay or gateway can use: the counterparts of the
+ * IPv4 zeronet, loopback and multicast checks, and IPv4-mapped, which the
+ * V6ONLY socket cannot reach.
+ */
+static bool amt_ip6_unusable(const struct in6_addr *addr)
+{
+ return ipv6_addr_any(addr) || ipv6_addr_loopback(addr) ||
+ ipv6_addr_is_multicast(addr) || ipv6_addr_v4mapped(addr);
+}
+
static void __amt_source_gc_work(void)
{
struct amt_source_node *snode;
@@ -731,6 +741,21 @@ static struct in6_addr amt_get_remote_ipv6(const struct amt_dev *amt)
return addr;
}
+static void amt_set_remote_ipv6(struct amt_dev *amt,
+ const struct in6_addr *addr)
+{
+ write_seqlock_bh(&amt->remote_ipv6_lock);
+ amt->remote_ipv6 = *addr;
+ write_sequnlock_bh(&amt->remote_ipv6_lock);
+}
+
+/* Forget the relay a gateway learned, in both outer families. */
+static void amt_clear_remote(struct amt_dev *amt)
+{
+ WRITE_ONCE(amt->remote_ip, 0);
+ amt_set_remote_ipv6(amt, &in6addr_any);
+}
+
/* IPv6-outer variant of amt_send_discovery(). */
static void amt_send_discovery_v6(struct amt_dev *amt)
{
@@ -1134,7 +1159,7 @@ static void amt_event_send_request(struct amt_dev *amt)
amt->qi = AMT_INIT_REQ_TIMEOUT;
WRITE_ONCE(amt->ready4, false);
WRITE_ONCE(amt->ready6, false);
- WRITE_ONCE(amt->remote_ip, 0);
+ amt_clear_remote(amt);
amt_update_gw_status(amt, AMT_STATUS_INIT, false);
amt->req_cnt = 0;
amt->nonce = 0;
@@ -2476,27 +2501,39 @@ static bool amt_advertisement_handler(struct amt_dev *amt, struct sk_buff *skb)
struct amt_header_advertisement *amta;
int hdr_size;
- hdr_size = sizeof(*amta) + sizeof(struct udphdr);
+ /* Both forms start with the same header and nonce. */
+ hdr_size = sizeof(struct udphdr) +
+ (amt_v6(amt) ? sizeof(struct amt_header_advertisement_v6) :
+ sizeof(*amta));
if (!pskb_may_pull(skb, hdr_size))
return true;
amta = (struct amt_header_advertisement *)(udp_hdr(skb) + 1);
- if (!amta->ip4)
- return true;
-
if (amta->reserved || amta->version)
return true;
- if (ipv4_is_loopback(amta->ip4) || ipv4_is_multicast(amta->ip4) ||
- ipv4_is_zeronet(amta->ip4))
- return true;
-
if (amt->status != AMT_STATUS_SENT_DISCOVERY ||
amt->nonce != amta->nonce)
return true;
- WRITE_ONCE(amt->remote_ip, amta->ip4);
- netdev_dbg(amt->dev, "advertised remote ip = %pI4\n", &amta->ip4);
+ if (amt_v6(amt)) {
+ const struct in6_addr *ip6;
+
+ ip6 = &((struct amt_header_advertisement_v6 *)amta)->ip6;
+ if (amt_ip6_unusable(ip6))
+ return true;
+
+ amt_set_remote_ipv6(amt, ip6);
+ netdev_dbg(amt->dev, "advertised remote ipv6 = %pI6c\n", ip6);
+ } else {
+ if (!amta->ip4 || ipv4_is_loopback(amta->ip4) ||
+ ipv4_is_multicast(amta->ip4) || ipv4_is_zeronet(amta->ip4))
+ return true;
+
+ WRITE_ONCE(amt->remote_ip, amta->ip4);
+ netdev_dbg(amt->dev, "advertised remote ip = %pI4\n",
+ &amta->ip4);
+ }
mod_delayed_work(amt_wq, &amt->req_wq, 0);
amt_update_gw_status(amt, AMT_STATUS_RECEIVED_ADVERTISEMENT, true);
@@ -3075,11 +3112,32 @@ drop:
}
}
+/* Whether a message a gateway received came from its relay: the discovery
+ * address for an Advertisement, the learned relay address otherwise. A
+ * relay address that is not known matches nothing, not even a 0.0.0.0
+ * source, which IPv4 input delivers in a packet sent to 255.255.255.255.
+ */
+static bool amt_from_relay(const struct amt_dev *amt,
+ const union amt_addr *saddr, bool discovery)
+{
+ __be32 relay4;
+
+ if (amt_v6(amt)) {
+ struct in6_addr relay;
+
+ relay = discovery ? amt->discovery_ipv6 :
+ amt_get_remote_ipv6(amt);
+ return !ipv6_addr_any(&relay) &&
+ ipv6_addr_equal(&saddr->ip6, &relay);
+ }
+ relay4 = discovery ? amt->discovery_ip : READ_ONCE(amt->remote_ip);
+ return relay4 && saddr->ip4 == relay4;
+}
+
static int amt_rcv(struct sock *sk, struct sk_buff *skb)
{
+ union amt_addr saddr;
struct amt_dev *amt;
- __be32 remote_ip;
- __be32 saddr;
int type;
bool err;
@@ -3090,10 +3148,14 @@ static int amt_rcv(struct sock *sk, struct sk_buff *skb)
kfree_skb(skb);
goto out;
}
- remote_ip = READ_ONCE(amt->remote_ip);
skb->dev = amt->dev;
- saddr = ip_hdr(skb)->saddr;
+ amt_outer_saddr(amt, skb, &saddr);
+ /* No relay or gateway sends from ::, but IPv6 input delivers it. */
+ if (amt_v6(amt) && ipv6_addr_any(&saddr.ip6)) {
+ err = true;
+ goto drop;
+ }
type = amt_parse_type(skb);
if (type == -1) {
err = true;
@@ -3101,9 +3163,17 @@ static int amt_rcv(struct sock *sk, struct sk_buff *skb)
}
if (amt->mode == AMT_MODE_GATEWAY) {
+ /* RFC 6936 s5: accept the zero checksum only on the message
+ * that needs it.
+ */
+ if (amt_v6(amt) && !udp_hdr(skb)->check &&
+ type != AMT_MSG_MULTICAST_DATA) {
+ err = true;
+ goto drop;
+ }
switch (type) {
case AMT_MSG_ADVERTISEMENT:
- if (saddr != amt->discovery_ip) {
+ if (!amt_from_relay(amt, &saddr, true)) {
netdev_dbg(amt->dev, "Invalid Relay IP\n");
err = true;
goto drop;
@@ -3115,7 +3185,7 @@ static int amt_rcv(struct sock *sk, struct sk_buff *skb)
}
goto out;
case AMT_MSG_MULTICAST_DATA:
- if (saddr != remote_ip) {
+ if (!amt_from_relay(amt, &saddr, false)) {
netdev_dbg(amt->dev, "Invalid Relay IP\n");
err = true;
goto drop;
@@ -3126,7 +3196,7 @@ static int amt_rcv(struct sock *sk, struct sk_buff *skb)
else
goto out;
case AMT_MSG_MEMBERSHIP_QUERY:
- if (saddr != remote_ip) {
+ if (!amt_from_relay(amt, &saddr, false)) {
netdev_dbg(amt->dev, "Invalid Relay IP\n");
err = true;
goto drop;
@@ -3264,12 +3334,14 @@ static struct sock *amt_create_sock(const struct amt_dev *amt)
* that IPv6 route lookups are strict about the output
* interface, as IPv4 ones are, and a link-local peer is
* unique. V6ONLY leaves the IPv4 wildcard port to an IPv4
- * amt device.
+ * amt device. A gateway accepts a zero UDP checksum, which
+ * RFC 7450 s5.2.3.3 requires for Multicast Data; amt_rcv()
+ * drops it on the other messages.
*/
udp_conf.family = AF_INET6;
udp_conf.bind_ifindex = amt->stream_dev->ifindex;
udp_conf.use_udp6_tx_checksums = true;
- udp_conf.use_udp6_rx_checksums = true;
+ udp_conf.use_udp6_rx_checksums = amt->mode == AMT_MODE_RELAY;
udp_conf.ipv6_v6only = true;
} else {
udp_conf.family = AF_INET;
@@ -3328,7 +3400,7 @@ static int amt_dev_open(struct net_device *dev)
}
amt->req_cnt = 0;
- WRITE_ONCE(amt->remote_ip, 0);
+ amt_clear_remote(amt);
amt->nonce = 0;
get_random_bytes(&amt->key, sizeof(siphash_key_t));
@@ -3373,7 +3445,7 @@ static int amt_dev_stop(struct net_device *dev)
amt->ready4 = false;
amt->ready6 = false;
amt->req_cnt = 0;
- WRITE_ONCE(amt->remote_ip, 0);
+ amt_clear_remote(amt);
list_for_each_entry_safe(tunnel, tmp, &amt->tunnel_list, list) {
list_del_rcu(&tunnel->list);
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 09/13] amt: receive the AMT gateway control plane over IPv6
2026-10-09 12:24 ` [PATCH net-next 09/13] amt: receive " Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
Thank you for your contribution! Sashiko AI review found 3 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 2
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] This patch also fixes an existing IPv4 gateway bug, but
carries no Fixes: tag.
- [Low] At this commit nothing writes amt->local_ipv6 or
amt->discovery_ipv6, so amt_v6() is always false and the new IPv6…
- [Low] The comment on struct amt_header_advertisement_v6
(include/net/amt.h:136-140, added in commit "amt: send the Relay…
--
amt: receive the AMT gateway control plane over IPv6
This commit lets an IPv6 AMT gateway receive Advertisements, Membership
Queries and Multicast Data that arrive with an IPv6 outer header. It adds
amt_from_relay() to check the outer source in the device's family, parses
the IPv6 form of the Advertisement, and clears the learned relay in both
families. On an IPv6 gateway, it also accepts a zero UDP checksum only on
Multicast Data.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index 148d1fb369154..86f168cb222c5 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
[ ... ]
> @@ -731,6 +741,21 @@ static struct in6_addr amt_get_remote_ipv6(const struct amt_dev *amt)
> return addr;
> }
>
> +static void amt_set_remote_ipv6(struct amt_dev *amt,
> + const struct in6_addr *addr)
> +{
> + write_seqlock_bh(&amt->remote_ipv6_lock);
> + amt->remote_ipv6 = *addr;
> + write_sequnlock_bh(&amt->remote_ipv6_lock);
> +}
[Severity: Low]
At this commit, does anything write amt->local_ipv6 or
amt->discovery_ipv6? If not, amt_v6() always returns false here:
return IS_ENABLED(CONFIG_IPV6) && !ipv6_addr_any(&amt->local_ipv6);
and the new IPv6 receive paths cannot run yet.
The remote_ipv6 address learned through amt_set_remote_ipv6() is also not
reported through netlink. amt_fill_info() and amt_get_size() only handle
the IPv4 attributes.
A later patch in the series, "amt: add netlink attributes for an IPv6
outer transport", appears to close both gaps. It adds IFLA_AMT_LOCAL_IP6,
IFLA_AMT_DISCOVERY_IP6 and IFLA_AMT_REMOTE_IP6, sets local_ipv6 and
discovery_ipv6 in amt_newlink(), and reports remote_ipv6 via
amt_fill_addr6(). Is this intermediate state intended?
[ ... ]
> @@ -2476,27 +2501,39 @@ static bool amt_advertisement_handler(struct amt_dev *amt, struct sk_buff *skb)
> struct amt_header_advertisement *amta;
> int hdr_size;
>
> - hdr_size = sizeof(*amta) + sizeof(struct udphdr);
> + /* Both forms start with the same header and nonce. */
> + hdr_size = sizeof(struct udphdr) +
> + (amt_v6(amt) ? sizeof(struct amt_header_advertisement_v6) :
> + sizeof(*amta));
> if (!pskb_may_pull(skb, hdr_size))
> return true;
[ ... ]
> + } else {
> + if (!amta->ip4 || ipv4_is_loopback(amta->ip4) ||
> + ipv4_is_multicast(amta->ip4) || ipv4_is_zeronet(amta->ip4))
> + return true;
> +
> + WRITE_ONCE(amt->remote_ip, amta->ip4);
[Severity: Low]
The comment on struct amt_header_advertisement_v6 in include/net/amt.h
says:
A gateway tells the two forms apart by the UDP datagram length
(s5.1.2.5), not by a field in the message, so it is a type of its own.
Does amt_advertisement_handler() follow that? It does not seem to look at
the UDP length. It picks the form from the device family and only enforces
a minimum length with pskb_may_pull().
Suppose an IPv4 gateway gets a 24-byte IPv6-form Advertisement from the
discovery address with the right nonce. The first 4 bytes of the IPv6
relay address would be read as amta->ip4. For 2001:db8::1 that gives
32.1.13.184, which passes the zeronet, loopback and multicast checks. The
gateway would then send Requests to that address.
An IPv6 gateway, for its part, drops a 12-byte IPv4 form without any
message. It treats any datagram of 24 bytes or more as the IPv6 form.
The relay side comment in amt_discovery_handler() ("The Advertisement
takes the form of the outer IP version") matches what this code relies
on. Should the struct comment be corrected? Or should the handler compare
ntohs(udp_hdr(skb)->len) against the expected form and reject a mismatch?
[ ... ]
> @@ -3075,11 +3112,32 @@ static void amt_gw_rcv(struct amt_dev *amt, struct sk_buff *skb)
> }
> }
>
> +/* Whether a message a gateway received came from its relay: the discovery
> + * address for an Advertisement, the learned relay address otherwise. A
> + * relay address that is not known matches nothing, not even a 0.0.0.0
> + * source, which IPv4 input delivers in a packet sent to 255.255.255.255.
> + */
> +static bool amt_from_relay(const struct amt_dev *amt,
> + const union amt_addr *saddr, bool discovery)
> +{
> + __be32 relay4;
> +
> + if (amt_v6(amt)) {
> + struct in6_addr relay;
> +
> + relay = discovery ? amt->discovery_ipv6 :
> + amt_get_remote_ipv6(amt);
> + return !ipv6_addr_any(&relay) &&
> + ipv6_addr_equal(&saddr->ip6, &relay);
> + }
> + relay4 = discovery ? amt->discovery_ip : READ_ONCE(amt->remote_ip);
> + return relay4 && saddr->ip4 == relay4;
> +}
[Severity: Medium]
The relay4 check here also seems to fix an existing IPv4 gateway bug.
Should that part be its own patch for net, with a Fixes: tag?
Before this patch, amt_rcv() accepted a Membership Query or Multicast Data
whenever saddr != remote_ip was false. That includes saddr == 0 while
remote_ip == 0. remote_ip is 0 after amt_event_send_request() gives up,
and also after amt_dev_open():
if (amt->req_cnt > AMT_MAX_REQ_COUNT) {
...
WRITE_ONCE(amt->remote_ip, 0);
...
amt->nonce = 0;
The INADDR_ANY gateway socket receives a 0.0.0.0 -> 255.255.255.255
packet. ip_route_input_slow() sends it to brd_input before the zeronet
martian check:
if (ipv4_is_lbcast(daddr) || (saddr == 0 && daddr == 0))
goto brd_input;
amt_membership_query_handler() then only checks the nonce, which has just
been reset to 0:
if (amtmq->nonce != amt->nonce)
return true;
So until the next discovery event, which can take up to
AMT_DISCOVERY_TIMEOUT, one on-link broadcast could inject a Membership
Query. That sets ready4, mac and qi, passes the inner IGMP query up, and
moves status from INIT to RECEIVED_QUERY. After that,
amt_event_send_discovery() skips rediscovery.
The commit message mentions this only in passing:
"in either family: IPv4 input delivers a 0.0.0.0 source too, when the
packet is sent to 255.255.255.255."
The change sits in a net-next IPv6 feature series and has no Fixes:
trailer, so stable kernels would not pick it up. An earlier patch in this
series, "amt: send the Relay Advertisement over IPv6", describes changing
IPv4 behaviour as "a fix of its own for net".
Could the IPv4 0.0.0.0 part be split out and sent to net, with a Fixes:
tag for the original AMT gateway receive path?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 10/13] amt: add netlink attributes for an IPv6 outer transport
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (8 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 09/13] amt: receive " Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 11/13] MAINTAINERS: amt: cover the amt headers and selftests Omar Ramadan
` (2 subsequent siblings)
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
Every path of the relay and the gateway can now run over an IPv6 outer
transport, but nothing can create such a device. Add the attributes
that do:
- IFLA_AMT_LOCAL_IP6, the local IPv6 address. It selects IPv6 as the
outer transport and is mutually exclusive with IFLA_AMT_LOCAL_IP.
- IFLA_AMT_DISCOVERY_IP6, the IPv6 address a gateway sends its Relay
Discovery to. A gateway's discovery address has to be in the family
of its local address, as both ends of the tunnel use one outer
transport.
- IFLA_AMT_REMOTE_IP6, the relay address the gateway learned from the
IPv6 Relay Advertisement. Like IFLA_AMT_REMOTE_IP it is only
reported; unlike that one, the policy rejects it on input.
amt_fill_info() reports the addresses in the device's outer family.
IFLA_AMT_LOCAL_IP6 is refused with -EAFNOSUPPORT on a kernel built
without IPv6, rather than creating an IPv4 device with no local
address. An IPv6 local or discovery address is refused when it is
unspecified, loopback or multicast, as the IPv4 ones are, and also when
it is IPv4-mapped: the V6ONLY socket could never send from or to such
an address, so the device would be created but silently fail every
send.
The existing IFLA_AMT_LOCAL_IP and IFLA_AMT_DISCOVERY_IP cannot simply
carry 16 bytes. Their policy only sets a minimum length of 4, so a
kernel without this series would accept a 16-byte value and use its
first four bytes as an IPv4 address. A kernel that does not know the
new attributes ignores them and fails the request for lack of a local
address, so userspace can tell whether IPv6 is supported. vxlan
(IFLA_VXLAN_LOCAL6, IFLA_VXLAN_GROUP6) and geneve (IFLA_GENEVE_REMOTE6)
add their IPv6 addresses the same way. The attributes are appended to
the enum, so the existing values do not change, and strict_start_type
makes the policy validate them, and any attribute added after them,
strictly.
Because of that minimum length, amt_validate() now refuses a 16-byte
IFLA_AMT_LOCAL_IP, and a 16-byte IFLA_AMT_DISCOVERY_IP on a gateway.
Every iproute2 released before the companion iproute2 patch puts an
IPv6 literal in the IPv4 attribute, and the kernel then creates an IPv4
device from the first four bytes of the address, 32.1.13.184 for
2001:db8::. Such a request now fails with an extack message that names
the problem instead of creating the wrong device. A relay never reads
IFLA_AMT_DISCOVERY_IP, so an IPv4 relay given a 16-byte one still
works as before.
A relay has always ignored IFLA_AMT_DISCOVERY_IP, and existing users may
pass it, so an IPv4 relay still accepts it. An IPv6 relay has no
existing users, so it rejects a discovery address of either family, and
an IPv4 relay rejects IFLA_AMT_DISCOVERY_IP6.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 176 +++++++++++++++++++++++++++++++--------
include/uapi/linux/amt.h | 13 +++
2 files changed, 155 insertions(+), 34 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index 86f168c..fb199d9 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -3516,6 +3516,7 @@ static void amt_link_setup(struct net_device *dev)
}
static const struct nla_policy amt_policy[IFLA_AMT_MAX + 1] = {
+ [IFLA_AMT_UNSPEC] = { .strict_start_type = IFLA_AMT_LOCAL_IP6 },
[IFLA_AMT_MODE] = { .type = NLA_U32 },
[IFLA_AMT_RELAY_PORT] = { .type = NLA_U16 },
[IFLA_AMT_GATEWAY_PORT] = { .type = NLA_U16 },
@@ -3524,8 +3525,25 @@ static const struct nla_policy amt_policy[IFLA_AMT_MAX + 1] = {
[IFLA_AMT_REMOTE_IP] = { .len = sizeof_field(struct iphdr, daddr) },
[IFLA_AMT_DISCOVERY_IP] = { .len = sizeof_field(struct iphdr, daddr) },
[IFLA_AMT_MAX_TUNNELS] = { .type = NLA_U32 },
+ [IFLA_AMT_LOCAL_IP6] = NLA_POLICY_EXACT_LEN(sizeof(struct in6_addr)),
+ [IFLA_AMT_DISCOVERY_IP6] =
+ NLA_POLICY_EXACT_LEN(sizeof(struct in6_addr)),
+ [IFLA_AMT_REMOTE_IP6] = { .type = NLA_REJECT },
};
+/* The policy of the IPv4 address attributes only sets a minimum length,
+ * and an iproute2 without IPv6 AMT support puts an IPv6 literal in them,
+ * so the device would take the first four bytes of it as its address.
+ */
+static bool amt_ip6_in_ip4_attr(const struct nlattr *attr,
+ struct netlink_ext_ack *extack)
+{
+ if (!attr || nla_len(attr) != sizeof(struct in6_addr))
+ return false;
+ NL_SET_ERR_MSG_ATTR(extack, attr, "IPv6 address in an IPv4 attribute");
+ return true;
+}
+
static int amt_validate(struct nlattr *tb[], struct nlattr *data[],
struct netlink_ext_ack *extack)
{
@@ -3550,16 +3568,63 @@ static int amt_validate(struct nlattr *tb[], struct nlattr *data[],
return -EINVAL;
}
- if (!data[IFLA_AMT_LOCAL_IP]) {
+ if (amt_ip6_in_ip4_attr(data[IFLA_AMT_LOCAL_IP], extack))
+ return -EINVAL;
+
+ if (!data[IFLA_AMT_LOCAL_IP] && !data[IFLA_AMT_LOCAL_IP6]) {
NL_SET_ERR_MSG_ATTR(extack, data[IFLA_AMT_DISCOVERY_IP],
"Local attribute is required");
return -EINVAL;
}
- if (!data[IFLA_AMT_DISCOVERY_IP] &&
- nla_get_u32(data[IFLA_AMT_MODE]) == AMT_MODE_GATEWAY) {
- NL_SET_ERR_MSG_ATTR(extack, data[IFLA_AMT_LOCAL_IP],
- "Discovery attribute is required");
+ if (data[IFLA_AMT_LOCAL_IP] && data[IFLA_AMT_LOCAL_IP6]) {
+ NL_SET_ERR_MSG_ATTR(extack, data[IFLA_AMT_LOCAL_IP6],
+ "Local IPv4 and IPv6 are mutually exclusive");
+ return -EINVAL;
+ }
+
+ if (data[IFLA_AMT_LOCAL_IP6] && !IS_ENABLED(CONFIG_IPV6)) {
+ NL_SET_ERR_MSG_ATTR(extack, data[IFLA_AMT_LOCAL_IP6],
+ "IPv6 support is disabled");
+ return -EAFNOSUPPORT;
+ }
+
+ if (nla_get_u32(data[IFLA_AMT_MODE]) != AMT_MODE_GATEWAY) {
+ struct nlattr *disc = data[IFLA_AMT_DISCOVERY_IP6];
+
+ /* An IPv4 relay has always ignored IFLA_AMT_DISCOVERY_IP,
+ * and existing users may pass it. An IPv6 relay has no such
+ * users, so it rejects a discovery address of either family.
+ */
+ if (!disc && data[IFLA_AMT_LOCAL_IP6])
+ disc = data[IFLA_AMT_DISCOVERY_IP];
+ if (disc) {
+ NL_SET_ERR_MSG_ATTR(extack, disc,
+ "Discovery is only valid in gateway mode");
+ return -EINVAL;
+ }
+ return 0;
+ }
+
+ /* A relay never reads IFLA_AMT_DISCOVERY_IP, but a gateway would
+ * take the first four bytes of an IPv6 literal as its relay.
+ */
+ if (amt_ip6_in_ip4_attr(data[IFLA_AMT_DISCOVERY_IP], extack))
+ return -EINVAL;
+
+ /* A gateway's discovery address is in the family of its local
+ * address, since both ends of the tunnel use one outer transport.
+ */
+ if (data[IFLA_AMT_LOCAL_IP6] ? !data[IFLA_AMT_DISCOVERY_IP6] :
+ !data[IFLA_AMT_DISCOVERY_IP]) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "Discovery attribute of the local family is required");
+ return -EINVAL;
+ }
+
+ if (data[IFLA_AMT_DISCOVERY_IP] && data[IFLA_AMT_DISCOVERY_IP6]) {
+ NL_SET_ERR_MSG_ATTR(extack, data[IFLA_AMT_DISCOVERY_IP6],
+ "Discovery IPv4 and IPv6 are mutually exclusive");
return -EINVAL;
}
@@ -3609,13 +3674,22 @@ static int amt_newlink(struct net_device *dev,
goto err;
}
- amt->local_ip = nla_get_in_addr(data[IFLA_AMT_LOCAL_IP]);
- if (ipv4_is_loopback(amt->local_ip) ||
- ipv4_is_zeronet(amt->local_ip) ||
- ipv4_is_multicast(amt->local_ip)) {
- NL_SET_ERR_MSG_ATTR(extack, tb[IFLA_AMT_LOCAL_IP],
- "Invalid Local address");
- goto err;
+ if (data[IFLA_AMT_LOCAL_IP6]) {
+ amt->local_ipv6 = nla_get_in6_addr(data[IFLA_AMT_LOCAL_IP6]);
+ if (amt_ip6_unusable(&amt->local_ipv6)) {
+ NL_SET_ERR_MSG_ATTR(extack, data[IFLA_AMT_LOCAL_IP6],
+ "Invalid Local IPv6 address");
+ goto err;
+ }
+ } else {
+ amt->local_ip = nla_get_in_addr(data[IFLA_AMT_LOCAL_IP]);
+ if (ipv4_is_loopback(amt->local_ip) ||
+ ipv4_is_zeronet(amt->local_ip) ||
+ ipv4_is_multicast(amt->local_ip)) {
+ NL_SET_ERR_MSG_ATTR(extack, tb[IFLA_AMT_LOCAL_IP],
+ "Invalid Local address");
+ goto err;
+ }
}
amt->relay_port = nla_get_be16_default(data[IFLA_AMT_RELAY_PORT],
@@ -3633,24 +3707,32 @@ static int amt_newlink(struct net_device *dev,
amt->qrv = READ_ONCE(amt->net->ipv4.sysctl_igmp_qrv);
amt->qri = 10;
} else {
- if (!data[IFLA_AMT_DISCOVERY_IP]) {
- NL_SET_ERR_MSG_ATTR(extack, tb[IFLA_AMT_DISCOVERY_IP],
- "discovery must be set in gateway mode");
- goto err;
- }
if (!amt->gw_port) {
NL_SET_ERR_MSG_ATTR(extack, tb[IFLA_AMT_DISCOVERY_IP],
"gateway port must not be 0");
goto err;
}
- WRITE_ONCE(amt->remote_ip, 0);
- amt->discovery_ip = nla_get_in_addr(data[IFLA_AMT_DISCOVERY_IP]);
- if (ipv4_is_loopback(amt->discovery_ip) ||
- ipv4_is_zeronet(amt->discovery_ip) ||
- ipv4_is_multicast(amt->discovery_ip)) {
- NL_SET_ERR_MSG_ATTR(extack, tb[IFLA_AMT_DISCOVERY_IP],
- "discovery must be unicast");
- goto err;
+ if (data[IFLA_AMT_DISCOVERY_IP6]) {
+ struct nlattr *attr = data[IFLA_AMT_DISCOVERY_IP6];
+
+ amt->discovery_ipv6 = nla_get_in6_addr(attr);
+ if (amt_ip6_unusable(&amt->discovery_ipv6)) {
+ NL_SET_ERR_MSG_ATTR(extack, attr,
+ "discovery must be unicast");
+ goto err;
+ }
+ } else {
+ WRITE_ONCE(amt->remote_ip, 0);
+ amt->discovery_ip =
+ nla_get_in_addr(data[IFLA_AMT_DISCOVERY_IP]);
+ if (ipv4_is_loopback(amt->discovery_ip) ||
+ ipv4_is_zeronet(amt->discovery_ip) ||
+ ipv4_is_multicast(amt->discovery_ip)) {
+ NL_SET_ERR_MSG_ATTR(extack,
+ tb[IFLA_AMT_DISCOVERY_IP],
+ "discovery must be unicast");
+ goto err;
+ }
}
}
dev->needed_headroom = amt->stream_dev->needed_headroom + amt_hlen(amt);
@@ -3702,7 +3784,27 @@ static size_t amt_get_size(const struct net_device *dev)
nla_total_size(sizeof(__u32)) + /* IFLA_MAX_TUNNELS */
nla_total_size(sizeof(__be32)) + /* IFLA_AMT_DISCOVERY_IP */
nla_total_size(sizeof(__be32)) + /* IFLA_AMT_REMOTE_IP */
- nla_total_size(sizeof(__be32)); /* IFLA_AMT_LOCAL_IP */
+ nla_total_size(sizeof(__be32)) + /* IFLA_AMT_LOCAL_IP */
+ /* IFLA_AMT_{LOCAL,DISCOVERY,REMOTE}_IP6. Only one address of
+ * each IPv4/IPv6 pair is emitted, but this sizes for both.
+ */
+ 3 * nla_total_size(sizeof(struct in6_addr));
+}
+
+/* The IPv6 addresses of an IPv6 device, one of each IPv4/IPv6 pair. */
+static int amt_fill_addr6(struct sk_buff *skb, const struct amt_dev *amt)
+{
+ const struct in6_addr remote = amt_get_remote_ipv6(amt);
+
+ if (nla_put_in6_addr(skb, IFLA_AMT_LOCAL_IP6, &amt->local_ipv6))
+ return -EMSGSIZE;
+ if (amt->mode == AMT_MODE_GATEWAY &&
+ nla_put_in6_addr(skb, IFLA_AMT_DISCOVERY_IP6, &amt->discovery_ipv6))
+ return -EMSGSIZE;
+ if (!ipv6_addr_any(&remote) &&
+ nla_put_in6_addr(skb, IFLA_AMT_REMOTE_IP6, &remote))
+ return -EMSGSIZE;
+ return 0;
}
static int amt_fill_info(struct sk_buff *skb, const struct net_device *dev)
@@ -3719,15 +3821,21 @@ static int amt_fill_info(struct sk_buff *skb, const struct net_device *dev)
goto nla_put_failure;
if (nla_put_u32(skb, IFLA_AMT_LINK, amt->stream_dev->ifindex))
goto nla_put_failure;
- if (nla_put_in_addr(skb, IFLA_AMT_LOCAL_IP, amt->local_ip))
- goto nla_put_failure;
- if (nla_put_in_addr(skb, IFLA_AMT_DISCOVERY_IP, amt->discovery_ip))
- goto nla_put_failure;
-
- remote_ip = READ_ONCE(amt->remote_ip);
- if (remote_ip)
- if (nla_put_in_addr(skb, IFLA_AMT_REMOTE_IP, remote_ip))
+ if (amt_v6(amt)) {
+ if (amt_fill_addr6(skb, amt))
+ goto nla_put_failure;
+ } else {
+ if (nla_put_in_addr(skb, IFLA_AMT_LOCAL_IP, amt->local_ip))
goto nla_put_failure;
+ if (nla_put_in_addr(skb, IFLA_AMT_DISCOVERY_IP,
+ amt->discovery_ip))
+ goto nla_put_failure;
+
+ remote_ip = READ_ONCE(amt->remote_ip);
+ if (remote_ip)
+ if (nla_put_in_addr(skb, IFLA_AMT_REMOTE_IP, remote_ip))
+ goto nla_put_failure;
+ }
if (nla_put_u32(skb, IFLA_AMT_MAX_TUNNELS, amt->max_tunnels))
goto nla_put_failure;
diff --git a/include/uapi/linux/amt.h b/include/uapi/linux/amt.h
index 2dccff4..5290ece 100644
--- a/include/uapi/linux/amt.h
+++ b/include/uapi/linux/amt.h
@@ -54,6 +54,19 @@ enum {
IFLA_AMT_DISCOVERY_IP,
/* This attribute specify number of maximum tunnel. */
IFLA_AMT_MAX_TUNNELS,
+ /* This attribute specifies the local IPv6 address. It selects IPv6
+ * as the outer transport and excludes IFLA_AMT_LOCAL_IP.
+ */
+ IFLA_AMT_LOCAL_IP6,
+ /* This attribute specifies the IPv6 address of the relay a gateway
+ * sends its Discovery to. It is the IPv6 form of
+ * IFLA_AMT_DISCOVERY_IP and needs IFLA_AMT_LOCAL_IP6.
+ */
+ IFLA_AMT_DISCOVERY_IP6,
+ /* This attribute reports the IPv6 relay address a gateway learned
+ * from the Relay Advertisement. It is read-only.
+ */
+ IFLA_AMT_REMOTE_IP6,
__IFLA_AMT_MAX,
};
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 10/13] amt: add netlink attributes for an IPv6 outer transport
2026-10-09 12:24 ` [PATCH net-next 10/13] amt: add netlink attributes for an IPv6 outer transport Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
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] This patch makes amt_validate() reject a 16-byte
IFLA_AMT_LOCAL_IP for every device, and a 16-byte…
--
amt: add netlink attributes for an IPv6 outer transport
This adds IFLA_AMT_LOCAL_IP6, IFLA_AMT_DISCOVERY_IP6 and
IFLA_AMT_REMOTE_IP6, so a device with an IPv6 outer transport can be
created and reported. It also makes amt_validate() refuse a 16-byte
IFLA_AMT_LOCAL_IP, and a 16-byte IFLA_AMT_DISCOVERY_IP on a gateway.
Until now the IPv4 policy accepted such values and truncated them.
> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index 86f168cb222c5..fb199d98d29d4 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
[ ... ]
> @@ -3524,8 +3525,25 @@ static const struct nla_policy amt_policy[IFLA_AMT_MAX + 1] = {
> [IFLA_AMT_REMOTE_IP] = { .len = sizeof_field(struct iphdr, daddr) },
> [IFLA_AMT_DISCOVERY_IP] = { .len = sizeof_field(struct iphdr, daddr) },
> [IFLA_AMT_MAX_TUNNELS] = { .type = NLA_U32 },
[ ... ]
> };
>
> +/* The policy of the IPv4 address attributes only sets a minimum length,
> + * and an iproute2 without IPv6 AMT support puts an IPv6 literal in them,
> + * so the device would take the first four bytes of it as its address.
> + */
> +static bool amt_ip6_in_ip4_attr(const struct nlattr *attr,
> + struct netlink_ext_ack *extack)
> +{
> + if (!attr || nla_len(attr) != sizeof(struct in6_addr))
> + return false;
> + NL_SET_ERR_MSG_ATTR(extack, attr, "IPv6 address in an IPv4 attribute");
> + return true;
> +}
> +
> static int amt_validate(struct nlattr *tb[], struct nlattr *data[],
> struct netlink_ext_ack *extack)
> {
> @@ -3550,16 +3568,63 @@ static int amt_validate(struct nlattr *tb[], struct nlattr *data[],
> return -EINVAL;
> }
>
> - if (!data[IFLA_AMT_LOCAL_IP]) {
> + if (amt_ip6_in_ip4_attr(data[IFLA_AMT_LOCAL_IP], extack))
> + return -EINVAL;
[Severity: Low]
The new code itself looks fine. Could this check, and the matching
IFLA_AMT_DISCOVERY_IP one further down in amt_validate(), be split out
into a separate patch for net with a Fixes: tag?
The truncation this check guards against does not depend on IPv6 outer
transport support. Since b9022b53adad ("amt: add control plane of amt
interface"), amt_policy has had this entry:
[IFLA_AMT_LOCAL_IP] = { .len = sizeof_field(struct iphdr, daddr) },
The type is left at NLA_UNSPEC, so this length is only a minimum and a
16-byte payload gets through. amt_newlink() then does:
amt->local_ip = nla_get_in_addr(data[IFLA_AMT_LOCAL_IP]);
This reads only the first four bytes, giving 32.1.13.184 for 2001:db8::.
That address passes the loopback, zeronet and multicast checks, so the
device is created with the wrong address. The same thing happens with
IFLA_AMT_DISCOVERY_IP on a gateway.
This fix is part of a net-next feature patch and has no Fixes: tag. As a
result, stable kernels will keep creating the wrong device without
reporting an error.
The series already handles a similar case as separate net work. The
commit "amt: key relay tunnels on a union amt_addr endpoint" calls the
change to the IPv4 Advertisement source a fix of its own for net.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 11/13] MAINTAINERS: amt: cover the amt headers and selftests
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (9 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 10/13] amt: add netlink attributes for an IPv6 outer transport Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-09 12:24 ` [PATCH net-next 12/13] selftests: net: add amt_v6.sh for an IPv6 outer transport Omar Ramadan
2026-10-09 12:24 ` [PATCH net-next 13/13] selftests: net: add amt_gw_v6.sh for the IPv6 netlink attributes Omar Ramadan
12 siblings, 0 replies; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
The AMT entry lists only drivers/net/amt.c, so get_maintainer.pl does
not find the AMT maintainer for changes to include/net/amt.h,
include/uapi/linux/amt.h or tools/testing/selftests/net/amt.sh, and this
series touches the first two and adds selftests next to the third. Add
the two headers, and a glob that covers amt.sh and the new amt_*.sh
scripts.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
MAINTAINERS | 3 +++
1 file changed, 3 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 509b35b..51799f5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1461,6 +1461,9 @@ S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git
T: git git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git
F: drivers/net/amt.c
+F: include/net/amt.h
+F: include/uapi/linux/amt.h
+F: tools/testing/selftests/net/amt*.sh
ANALOG DEVICES INC AD3530R DRIVER
M: Kim Seer Paller <kimseer.paller@analog.com>
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* [PATCH net-next 12/13] selftests: net: add amt_v6.sh for an IPv6 outer transport
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (10 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 11/13] MAINTAINERS: amt: cover the amt headers and selftests Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
2026-10-10 12:41 ` netdev-bot+sashiko
2026-10-09 12:24 ` [PATCH net-next 13/13] selftests: net: add amt_gw_v6.sh for the IPv6 netlink attributes Omar Ramadan
12 siblings, 1 reply; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
amt.sh runs the gateway and the relay over IPv4; its 2001:db8:: addresses
are inner MLD carried in that IPv4 tunnel. Nothing covers the IPv6 outer
transport this series adds.
amt_v6.sh is amt.sh with the gateway<->relay link moved to IPv6 and two
more gateways on that link, as its header shows; it needs jq on top of
amt.sh's tools. The relay forwards to a tunnel only after the gateway's
Membership Update, and the gateway sends that only after the relay's
Membership Query, so the forwarding checks cover the whole handshake.
Besides the Advertisement its header describes, the test checks that:
- IPv4 and IPv6 multicast both reach the listener through the tunnel;
- IPv4 multicast also reaches a listener on the second gateway, which
shares the first one's /64 and gateway port, so the relay has to
match each gateway's whole address;
- a third gateway, beyond the relay's max_tunnels, gets an ICMPv6
Destination Unreachable for its Request, on a global and on a
link-local address;
- with the relay's route MTU to the gateway lowered, an IPv6 payload
above the tunnel MTU earns its source a Packet Too Big, and the AMT
datagram is neither fragmented nor counted in Ip6FragFails;
- a gateway accepts Multicast Data with a zero UDP checksum and refuses
one on an Advertisement, and a relay refuses it on a Discovery; the
test sends these messages itself, with socat setting UDP_NO_CHECK6_TX;
- a Relay Discovery sent to ff02::1 draws no reply, while one sent to
the discovery address, the positive control, draws an Advertisement;
- taking amtg down clears the learned relay address, and bringing it
up discovers the relay again.
The checks that read packets off the wire need tcpdump and are skipped
without it.
A probe creates a throwaway IPv6 relay first and skips only when the
kernel or iproute2 cannot create one, including an iproute2 that puts
the IPv6 literal into IFLA_AMT_LOCAL_IP, which the probe reads back.
Any other failure after the probe, including in setup, is a FAIL.
smcroute's IPv4 routes need CONFIG_IP_MROUTE, which the net config did
not enable; amt.sh has depended on it as well. The iproute2 side is
posted to iproute2-next separately.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/amt_v6.sh | 535 ++++++++++++++++++++++++++
tools/testing/selftests/net/config | 1 +
3 files changed, 537 insertions(+)
create mode 100755 tools/testing/selftests/net/amt_v6.sh
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 54beea2..efdc77b 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -9,6 +9,7 @@ CFLAGS += -I../
TEST_PROGS := \
altnames.sh \
amt.sh \
+ amt_v6.sh \
arp_ndisc_evict_nocarrier.sh \
arp_ndisc_untracked_subnets.sh \
bareudp.sh \
diff --git a/tools/testing/selftests/net/amt_v6.sh b/tools/testing/selftests/net/amt_v6.sh
new file mode 100755
index 0000000..d273294
--- /dev/null
+++ b/tools/testing/selftests/net/amt_v6.sh
@@ -0,0 +1,535 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# amt.sh with an IPv6 outer transport. The gateways and the relay speak AMT
+# over IPv6, and IPv4 and IPv6 multicast are carried inside. The namespaces,
+# roles and inner addresses are those of amt.sh; the gateway<->relay link
+# differs, and two more gateways share it:
+#
+# LISTENER l_gw -- gw_l [br0: gw_l, amtg] GATEWAY gw_relay 2001:db8:a::1
+# | IPv6 outer
+# [amtg2] GATEWAY2 gw2_relay 2001:db8:a::4
+# [amtg3] GATEWAY3 gw3_relay 2001:db8:a::6
+# |
+# SOURCE src_relay -- relay_src [amtr] RELAY br_gw 2001:db8:a::2
+# [br_gw: relay_gw, relay_gw2, relay_gw3] 2001:db8:a::3
+#
+# The relay's local address is 2001:db8:a::2. The gateways discover it
+# through 2001:db8:a::3, so the Relay Advertisement has to come from the
+# address the Discovery was sent to (RFC 7450 s5.1.2) and carry ::2. The
+# relay has room for two tunnels, amtg's and amtg2's.
+
+# The namespace names are created by setup_ns, RELAY6 is an address.
+# shellcheck disable=SC2153
+source lib.sh
+
+readonly GW6="2001:db8:a::1"
+readonly GW2_6="2001:db8:a::4"
+readonly GW3_6="2001:db8:a::6"
+readonly RELAY6="2001:db8:a::2"
+readonly DISC6="2001:db8:a::3"
+SMCDIR=
+
+cleanup()
+{
+ cleanup_all_ns
+ [ -n "$SMCDIR" ] && rm -rf "$SMCDIR"
+}
+
+setup_fail()
+{
+ echo "FAIL: setup failed at line $1" >&2
+ exit "$ksft_fail"
+}
+
+amt_field()
+{
+ ip -n "$1" -d -j link show "$2" |
+ jq -r ".[0].linkinfo.info_data.$3 // empty"
+}
+
+# remote_is <addr> [<ns> <dev>]: the relay address a gateway learned.
+remote_is()
+{
+ [ "$(amt_field "${2:-$GATEWAY}" "${3:-amtg}" remote)" = "$1" ]
+}
+
+snmp6()
+{
+ # shellcheck disable=SC2016
+ ip netns exec "$1" awk -v k="$2" '$1 == k { print $2 }' /proc/net/snmp6
+}
+
+setup_links()
+{
+ local ns
+
+ # No DAD: the relay sources its MLD General Query from amtr's
+ # link-local address, which must not be tentative when it is sent.
+ for ns in "$LISTENER" "$GATEWAY" "$GATEWAY2" "$GATEWAY3" "$RELAY" \
+ "$SOURCE"; do
+ ip netns exec "$ns" sysctl -wq \
+ net.ipv6.conf.all.accept_dad=0 \
+ net.ipv6.conf.default.accept_dad=0
+ done
+
+ ip -n "$LISTENER" link add l_gw type veth peer name gw_l \
+ netns "$GATEWAY"
+ ip -n "$GATEWAY" link add gw_relay type veth peer name relay_gw \
+ netns "$RELAY"
+ ip -n "$GATEWAY2" link add gw2_relay type veth peer name relay_gw2 \
+ netns "$RELAY"
+ ip -n "$GATEWAY3" link add gw3_relay type veth peer name relay_gw3 \
+ netns "$RELAY"
+ ip -n "$RELAY" link add relay_src type veth peer name src_relay \
+ netns "$SOURCE"
+
+ ip -n "$GATEWAY" addr add "$GW6/64" dev gw_relay
+ ip -n "$GATEWAY" link set gw_relay up
+ ip -n "$GATEWAY2" addr add "$GW2_6/64" dev gw2_relay
+ ip -n "$GATEWAY2" link set gw2_relay up
+ ip -n "$GATEWAY3" addr add "$GW3_6/64" dev gw3_relay
+ ip -n "$GATEWAY3" link set gw3_relay up
+ ip -n "$RELAY" link add br_gw type bridge
+ ip -n "$RELAY" link set relay_gw master br_gw up
+ ip -n "$RELAY" link set relay_gw2 master br_gw up
+ ip -n "$RELAY" link set relay_gw3 master br_gw up
+ ip -n "$RELAY" addr add "$RELAY6/64" dev br_gw
+ ip -n "$RELAY" addr add "$DISC6/64" dev br_gw
+ ip -n "$RELAY" link set br_gw up
+}
+
+# An iproute2 without IPv6 AMT support either rejects the address or puts
+# the whole literal into IFLA_AMT_LOCAL_IP, which this kernel refuses and
+# an older one reads as an IPv4 address (its first four bytes). A kernel
+# without IPv6 AMT support ignores IFLA_AMT_LOCAL_IP6 and asks for a local
+# address. Skip in those cases only.
+probe_v6_relay()
+{
+ local err got
+
+ if ! err=$(ip -n "$RELAY" link add amtprobe type amt mode relay \
+ local "$RELAY6" dev br_gw 2>&1); then
+ case "$err" in
+ *"Local attribute is required"*|*"expected rather than"*|\
+ *"IPv6 address in an IPv4 attribute"*|\
+ *"IPv6 support is disabled"*)
+ echo "SKIP: no IPv6 AMT support: $err"
+ exit "$ksft_skip"
+ ;;
+ esac
+ echo "FAIL: cannot create an IPv6 relay: $err"
+ exit "$ksft_fail"
+ fi
+ got=$(amt_field "$RELAY" amtprobe local)
+ ip -n "$RELAY" link del amtprobe
+ if [[ "$got" != *:* ]]; then
+ echo "SKIP: iproute2 lacks IPv6 AMT support (read back '$got')"
+ exit "$ksft_skip"
+ fi
+}
+
+setup_topology()
+{
+ ip -n "$LISTENER" addr add 192.168.0.2/24 dev l_gw
+ ip -n "$LISTENER" addr add 2001:db8::2/64 dev l_gw
+ ip -n "$LISTENER" link set l_gw up
+ ip -n "$LISTENER" route add default via 192.168.0.1 dev l_gw
+ ip -n "$LISTENER" route add default via 2001:db8::1 dev l_gw
+ ip -n "$LISTENER" addr add 239.0.0.1/32 dev l_gw autojoin
+ ip -n "$LISTENER" addr add ff0e::5:6/128 dev l_gw autojoin
+
+ ip -n "$GATEWAY" addr add 192.168.0.1/24 dev gw_l
+ ip -n "$GATEWAY" addr add 2001:db8::1/64 dev gw_l
+ ip -n "$GATEWAY" link add br0 type bridge
+ ip -n "$GATEWAY" link set br0 up
+ ip -n "$GATEWAY" link set gw_l master br0 up
+ ip -n "$GATEWAY" link add amtg master br0 type amt mode gateway \
+ local "$GW6" discovery "$DISC6" dev gw_relay \
+ gateway_port 2268 relay_port 2268
+
+ ip -n "$GATEWAY2" link add amtg2 type amt mode gateway \
+ local "$GW2_6" discovery "$DISC6" dev gw2_relay \
+ gateway_port 2268 relay_port 2268
+
+ ip -n "$RELAY" link add amtr type amt mode relay local "$RELAY6" \
+ dev br_gw relay_port 2268 max_tunnels 2
+ ip -n "$RELAY" addr add 172.17.0.1/24 dev relay_src
+ ip -n "$RELAY" addr add 2001:db8:3::1/64 dev relay_src
+ ip -n "$RELAY" link set relay_src up
+ ip netns exec "$RELAY" sysctl -wq net.ipv4.ip_forward=1
+ ip netns exec "$RELAY" iptables -t mangle -I PREROUTING \
+ -d 239.0.0.1 -j TTL --ttl-set 2
+ # Only the group: rewriting the hop limit of Neighbour Discovery on
+ # the IPv6 outer link would break it.
+ ip netns exec "$RELAY" ip6tables -t mangle -I PREROUTING \
+ -d ff0e::5:6 -j HL --hl-set 2
+
+ ip -n "$SOURCE" addr add 172.17.0.2/24 dev src_relay
+ ip -n "$SOURCE" addr add 2001:db8:3::2/64 dev src_relay
+ ip -n "$SOURCE" link set src_relay up
+ ip -n "$SOURCE" route add default via 172.17.0.1 dev src_relay
+ ip -n "$SOURCE" route add default via 2001:db8:3::1 dev src_relay
+
+ ip -n "$RELAY" link set amtr up
+ ip -n "$GATEWAY" link set amtg up
+ ip -n "$GATEWAY2" link set amtg2 up
+
+ SMCDIR=$(mktemp -d)
+ ip netns exec "$RELAY" smcrouted -P "$SMCDIR/pid" -u "$SMCDIR/sock"
+ slowwait 5 test -S "$SMCDIR/sock"
+ ip netns exec "$RELAY" smcroutectl -u "$SMCDIR/sock" \
+ a relay_src 172.17.0.2 239.0.0.1 amtr
+ ip netns exec "$RELAY" smcroutectl -u "$SMCDIR/sock" \
+ a relay_src 2001:db8:3::2 ff0e::5:6 amtr
+}
+
+test_discovery()
+{
+ RET=0
+ slowwait 10 remote_is "$RELAY6"
+ check_err $? "remote is '$(amt_field "$GATEWAY" amtg remote)'"
+ log_test "IPv6 discovery through a secondary relay address"
+}
+
+# test_forward <ns> <socat address> <port> <group> <source> <description>
+test_forward()
+{
+ local ns=$1 addr=$2 port=$3 grp=$4 src=$5 desc=$6
+ local out pid i
+
+ RET=0
+ out=$(mktemp)
+ ip netns exec "$ns" timeout 20 \
+ socat -u "$addr,readbytes=128" - > "$out" &
+ pid=$!
+ wait_local_port_listen "$ns" "$port" udp
+ for i in $(seq 15); do
+ ip netns exec "$SOURCE" bash -c \
+ "printf '%s %128s' $src | nc -w 1 -u $grp $port"
+ grep -q "$src" "$out" && break
+ done
+ wait "$pid"
+ grep -q "$src" "$out"
+ check_err $? "nothing from $src reached the listener"
+ rm -f "$out"
+ log_test "$desc"
+}
+
+# The tunnel MTU is the path MTU to the gateway less the outer headers.
+# Lower the relay's route MTU to the gateway below the payload: the relay
+# must not fragment the AMT datagram, and it must send the payload's
+# source a Packet Too Big (RFC 7450 s5.3.3.6).
+test_tmtu()
+{
+ local ptb frag fragfail i
+
+ RET=0
+ ip -n "$RELAY" -6 route add "$GW6/128" dev br_gw mtu 1400
+ ptb=$(snmp6 "$SOURCE" Icmp6InPktTooBigs)
+ frag=$(snmp6 "$RELAY" Ip6FragCreates)
+ fragfail=$(snmp6 "$RELAY" Ip6FragFails)
+ for i in 1 2 3; do
+ ip netns exec "$SOURCE" bash -c \
+ "printf '%1352s' x | nc -w 1 -u ff0e::5:6 6000"
+ done
+ [ "$(snmp6 "$SOURCE" Icmp6InPktTooBigs)" -gt "$ptb" ] ||
+ check_err 1 "the source got no Packet Too Big"
+ [ "$(snmp6 "$RELAY" Ip6FragCreates)" -eq "$frag" ] ||
+ check_err 1 "the relay fragmented an AMT datagram"
+ # amt_udp_xmit() clears ignore_df, so an oversized datagram that got
+ # past the tunnel MTU check would be refused by IPv6 output and
+ # counted as a FragFail rather than a FragCreate.
+ [ "$(snmp6 "$RELAY" Ip6FragFails)" -eq "$fragfail" ] ||
+ check_err 1 "an oversized AMT datagram reached IPv6 output"
+ ip -n "$RELAY" -6 route del "$GW6/128" dev br_gw mtu 1400
+ log_test "IPv6 payload above the tunnel MTU"
+}
+
+# A second gateway in the same /64, with the same gateway port as the first.
+# The relay has to send each gateway its own Membership Query: a query sent
+# to the wrong one fails its nonce check, and the intended gateway never
+# reports, so the relay never forwards to it.
+test_second_gateway()
+{
+ RET=0
+ slowwait 10 remote_is "$RELAY6" "$GATEWAY2" amtg2
+ check_err $? "amtg2 remote is '$(amt_field "$GATEWAY2" amtg2 remote)'"
+ log_test "second IPv6 gateway on the link discovers the relay"
+
+ test_forward "$GATEWAY2" \
+ UDP4-RECV:4000,ip-add-membership=239.0.0.1:amtg2 \
+ 4000 239.0.0.1 172.17.0.2 \
+ "IPv4 multicast to a second gateway in the same /64"
+}
+
+# disc_answer <dst> <filter> [<socat options>]: send a Relay Discovery from
+# the gateway's namespace to [<dst>]:2268 and succeed if gw_relay sees a
+# packet matching the tcpdump <filter> within a few seconds.
+disc_answer()
+{
+ local dst=$1 filter=$2 opts=${3:-} pid i rc log
+ local to="UDP6-SENDTO:[$dst]:2268,sourceport=40000"
+
+ log=$(mktemp)
+ ip netns exec "$GATEWAY" timeout 6 \
+ tcpdump -nni gw_relay -c 1 "$filter" > /dev/null 2> "$log" &
+ pid=$!
+ # Send nothing before the capture is live, or a missed packet would
+ # pass the negative check.
+ busywait 5000 grep -q "listening on" "$log"
+ # Type 1 (Relay Discovery), version 0, then a nonce.
+ for i in 1 2 3; do
+ printf '\x01\x00\x00\x00\x12\x34\x56\x78' |
+ ip netns exec "$GATEWAY" socat -u - \
+ "$to,so-bindtodevice=gw_relay$opts"
+ sleep 0.5
+ done
+ wait "$pid"
+ rc=$?
+ rm -f "$log"
+ return "$rc"
+}
+
+# The relay's socket is bound to ::, so it also receives a Discovery sent
+# to ff02::1. Answering it from that destination would put a multicast
+# source address on the wire (RFC 4291 s2.7), so the relay must not answer
+# it at all. The unicast Discovery is the positive control: it shows that
+# the probe and the capture work, and it uses the same capture filter.
+test_mcast_discovery()
+{
+ RET=0
+ if ! command -v tcpdump > /dev/null; then
+ log_test_skip "Discovery to ff02::1 is not answered" \
+ "tcpdump not installed"
+ return
+ fi
+ disc_answer "$DISC6" "udp and src port 2268 and dst port 40000"
+ check_err $? "no Advertisement for a unicast Discovery"
+ disc_answer ff02::1 "udp and src port 2268 and dst port 40000"
+ check_fail $? "the relay answered a Discovery sent to ff02::1"
+ log_test "Discovery to ff02::1 is not answered"
+}
+
+# amtg and amtg2 hold the relay's two tunnels, so amtg3's Request draws an
+# ICMPv6 Destination Unreachable (address unreachable). From a link-local
+# address it also shows that the relay sends the error through its
+# underlying link: icmp6_send() routes it by skb->dev, and the amt device
+# would drop it.
+test_tunnel_limit()
+{
+ local f="icmp6 and ip6[40] == 1 and ip6[41] == 3"
+ local kind addr pid log
+
+ for kind in global link-local; do
+ RET=0
+ if ! command -v tcpdump > /dev/null; then
+ log_test_skip "tunnel limit: error to a $kind gateway" \
+ "tcpdump not installed"
+ continue
+ fi
+ addr=$GW3_6
+ # With a scope filter, ip -j prints {} for each address it
+ # leaves out, so select the link-local one instead.
+ [ "$kind" = link-local ] &&
+ addr=$(ip -n "$GATEWAY3" -6 -j addr show dev gw3_relay |
+ jq -r '.[0].addr_info[] |
+ select(.scope == "link") | .local')
+ log=$(mktemp)
+ ip netns exec "$GATEWAY3" timeout 10 tcpdump --immediate-mode \
+ -nni gw3_relay -c 1 "$f and dst host $addr" \
+ > /dev/null 2> "$log" &
+ pid=$!
+ busywait 5000 grep -q "listening on" "$log"
+ ip -n "$GATEWAY3" link add amtg3 type amt mode gateway \
+ local "$addr" discovery "$DISC6" dev gw3_relay \
+ gateway_port 2268 relay_port 2268
+ ip -n "$GATEWAY3" link set amtg3 up
+ wait "$pid"
+ check_err $? "no Destination Unreachable reached amtg3"
+ ip -n "$GATEWAY3" link del amtg3
+ rm -f "$log"
+ log_test "tunnel limit: error to a $kind gateway"
+ done
+}
+
+# mcast_data <payload>: printf escapes of AMT Multicast Data carrying an
+# IPv4 UDP datagram from 172.17.0.2:5000 to 239.0.0.1:4000.
+mcast_data()
+{
+ local ulen=$((8 + ${#1})) out='\x06\x00' sum b
+
+ # IPv4 header checksum: 0x45, the length, TTL 64 and UDP, the addresses
+ sum=$((0x4500 + 20 + ulen + 0x4011 + 0xac11 + 0x0002 + 0xef00 + 0x0001))
+ while ((sum >> 16)); do
+ sum=$(((sum & 0xffff) + (sum >> 16)))
+ done
+ sum=$((~sum & 0xffff))
+ for b in 0x45 0 $(((20 + ulen) >> 8)) $(((20 + ulen) & 0xff)) 0 0 0 0 \
+ 64 17 $((sum >> 8)) $((sum & 0xff)) 172 17 0 2 239 0 0 1 \
+ 0x13 0x88 0x0f 0xa0 $((ulen >> 8)) $((ulen & 0xff)) 0 0; do
+ out+=$(printf '\\x%02x' "$b")
+ done
+ printf '%s%s' "$out" "$1"
+}
+
+# inject_data <payload> [<socat options>]: send AMT Multicast Data from the
+# relay's address to amtg until the listener behind it gets <payload>.
+inject_data()
+{
+ local out pid i rc
+
+ out=$(mktemp)
+ ip netns exec "$LISTENER" timeout 20 \
+ socat -u "UDP4-LISTEN:4000,readbytes=${#1}" - > "$out" &
+ pid=$!
+ wait_local_port_listen "$LISTENER" 4000 udp
+ for i in $(seq 15); do
+ printf '%b' "$(mcast_data "$1")" |
+ ip netns exec "$RELAY" socat -u - \
+ "UDP6-SENDTO:[$GW6]:2268,bind=[$RELAY6]:40001${2:-}"
+ grep -q "$1" "$out" && break
+ sleep 1
+ done
+ wait "$pid"
+ grep -q "$1" "$out"
+ rc=$?
+ rm -f "$out"
+ return "$rc"
+}
+
+# A gateway accepts Multicast Data with a zero UDP checksum, as RFC 7450
+# s5.2.3.3 requires (RFC 6936). The relay always sends a checksum, so the
+# test sends the message itself, from the relay's address, which is all the
+# gateway checks, with socat setting UDP_NO_CHECK6_TX (SOL_UDP 17, option
+# 101). The same message with a checksum is the control.
+test_zero_data()
+{
+ RET=0
+ inject_data amt-csum
+ check_err $? "control: Multicast Data with a checksum did not arrive"
+ inject_data amt-zero ",setsockopt-int=17:101:1"
+ check_err $? "Multicast Data with a zero checksum did not arrive"
+ log_test "a gateway accepts a zero UDP checksum on Multicast Data"
+}
+
+# A relay requires the UDP checksum on every message: a Relay Discovery
+# with a zero checksum draws no Advertisement, and counts as an error.
+test_zero_relay()
+{
+ local err
+
+ RET=0
+ if ! command -v tcpdump > /dev/null; then
+ log_test_skip "a relay refuses a zero UDP checksum" \
+ "tcpdump not installed"
+ return
+ fi
+ err=$(snmp6 "$RELAY" Udp6InCsumErrors)
+ disc_answer "$DISC6" "udp and src port 2268 and dst port 40000" \
+ ",setsockopt-int=17:101:1"
+ check_fail $? "the relay answered a zero-checksum Discovery"
+ [ "$(snmp6 "$RELAY" Udp6InCsumErrors)" -gt "$err" ] ||
+ check_err 1 "the relay counted no checksum error"
+ log_test "a relay refuses a zero UDP checksum"
+}
+
+# adv_listen: receive one Relay Discovery on port 2269 of the discovery
+# address, where no relay listens, into $ADV_FILE.
+adv_listen()
+{
+ ip netns exec "$RELAY" timeout 15 socat -u \
+ "UDP6-RECV:2269,bind=[$DISC6],readbytes=8" - > "$ADV_FILE" &
+ ADV_PID=$!
+ wait_local_port_listen "$RELAY" 2269 udp
+}
+
+# adv_answer <byte> [<socat options>]: answer that Discovery with a Relay
+# Advertisement of 2001:db8:a::<byte>.
+adv_answer()
+{
+ local z9='\x00\x00\x00\x00\x00\x00\x00\x00\x00' nonce='' b
+
+ wait "$ADV_PID" || return 1
+ # Type 2 and the nonce, the Discovery's bytes 4-7, then the address.
+ for b in $(od -An -tx1 -j4 -N4 "$ADV_FILE"); do
+ nonce+="\\x$b"
+ done
+ printf '%b' "\x02\x00\x00\x00$nonce\x20\x01\x0d\xb8\x00\x0a$z9\x$1" |
+ ip netns exec "$RELAY" socat -u - \
+ "UDP6-SENDTO:[$GW3_6]:2269,bind=[$DISC6]:2269${2:-}"
+}
+
+# A gateway accepts a zero UDP checksum on nothing but Multicast Data. The
+# relay never sends one, so the test answers amtg3's Discoveries itself.
+# amtg3 runs on port 2269, where no relay listens. It must not learn relay
+# ::21 from a zero checksum, but learns ::22 from a checksum, the control.
+test_zero_adv()
+{
+ RET=0
+ ADV_FILE=$(mktemp)
+ adv_listen
+ ip -n "$GATEWAY3" link add amtg3 type amt mode gateway local "$GW3_6" \
+ discovery "$DISC6" dev gw3_relay gateway_port 2269 \
+ relay_port 2269
+ ip -n "$GATEWAY3" link set amtg3 up
+ adv_answer 21 ",setsockopt-int=17:101:1"
+ check_err $? "no Relay Discovery reached port 2269"
+ adv_listen
+ sleep 1
+ [ -z "$(amt_field "$GATEWAY3" amtg3 remote)" ] ||
+ check_err 1 "amtg3 learned a relay from a zero checksum"
+ adv_answer 22
+ check_err $? "no second Relay Discovery reached port 2269"
+ slowwait 5 remote_is 2001:db8:a::22 "$GATEWAY3" amtg3
+ check_err $? "control: amtg3 did not learn the checksummed relay"
+ ip -n "$GATEWAY3" link del amtg3
+ rm -f "$ADV_FILE"
+ log_test "a gateway refuses a zero UDP checksum on an Advertisement"
+}
+
+test_down_up()
+{
+ RET=0
+ ip -n "$GATEWAY" link set amtg down
+ remote_is ""
+ check_err $? "relay still reported while down"
+ ip -n "$GATEWAY" link set amtg up
+ slowwait 10 remote_is "$RELAY6"
+ check_err $? "relay not rediscovered after up"
+ log_test "gateway forgets its IPv6 relay on link down"
+}
+
+for cmd in jq socat nc smcrouted smcroutectl iptables ip6tables; do
+ require_command "$cmd"
+done
+
+trap cleanup EXIT
+setup_ns LISTENER GATEWAY GATEWAY2 GATEWAY3 RELAY SOURCE ||
+ exit "$ksft_skip"
+
+set -E
+trap 'setup_fail $LINENO' ERR
+setup_links
+trap - ERR
+probe_v6_relay
+trap 'setup_fail $LINENO' ERR
+setup_topology
+trap - ERR
+set +E
+
+test_discovery
+test_forward "$LISTENER" UDP4-LISTEN:4000 4000 239.0.0.1 172.17.0.2 \
+ "IPv4 multicast over an IPv6 tunnel"
+test_forward "$LISTENER" UDP6-LISTEN:6000 6000 ff0e::5:6 2001:db8:3::2 \
+ "IPv6 multicast over an IPv6 tunnel"
+test_second_gateway
+test_tunnel_limit
+test_tmtu
+test_zero_data
+test_zero_relay
+test_zero_adv
+test_mcast_discovery
+test_down_up
+
+exit "$EXIT_STATUS"
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index d355cf9..07296f0 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -28,6 +28,7 @@ CONFIG_IP6_NF_MATCH_RPFILTER=m
CONFIG_IP6_NF_NAT=m
CONFIG_IP6_NF_RAW=m
CONFIG_IP6_NF_TARGET_REJECT=m
+CONFIG_IP_MROUTE=y
CONFIG_IP_NF_FILTER=m
CONFIG_IP_NF_IPTABLES=m
CONFIG_IP_NF_IPTABLES_LEGACY=m
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH net-next 12/13] selftests: net: add amt_v6.sh for an IPv6 outer transport
2026-10-09 12:24 ` [PATCH net-next 12/13] selftests: net: add amt_v6.sh for an IPv6 outer transport Omar Ramadan
@ 2026-10-10 12:41 ` netdev-bot+sashiko
0 siblings, 0 replies; 23+ messages in thread
From: netdev-bot+sashiko @ 2026-10-10 12:41 UTC (permalink / raw)
To: omar
Cc: ap420073, andrew+netdev, davem, edumazet, kuba, pabeni, shuah,
horms, netdev, linux-kselftest, linux-kernel
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · 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).
- [Medium] disc_answer() in tools/testing/selftests/net/amt_v6.sh (lines
269-292) ignores two exit statuses.
- [Low] In tools/testing/selftests/net/amt_v6.sh, the `*"IPv6 support is
disabled"*` branch in probe_v6_relay() (line 117) can never run.
--
selftests: net: add amt_v6.sh for an IPv6 outer transport
This adds amt_v6.sh, a variant of amt.sh where the gateways and the relay
run AMT over IPv6, with two extra gateways on the outer link. It covers
discovery, IPv4 and IPv6 multicast forwarding, the tunnel limit, tunnel MTU
handling, zero UDP checksum handling, Discovery sent to ff02::1, and link
down/up. It also enables CONFIG_IP_MROUTE in the net selftest config.
> diff --git a/tools/testing/selftests/net/amt_v6.sh b/tools/testing/selftests/net/amt_v6.sh
> new file mode 100755
> index 0000000000000..d2732945a3c7b
> --- /dev/null
> +++ b/tools/testing/selftests/net/amt_v6.sh
> @@ -0,0 +1,535 @@
[ ... ]
> +setup_links()
> +{
> + local ns
> +
> + # No DAD: the relay sources its MLD General Query from amtr's
> + # link-local address, which must not be tentative when it is sent.
> + for ns in "$LISTENER" "$GATEWAY" "$GATEWAY2" "$GATEWAY3" "$RELAY" \
> + "$SOURCE"; do
> + ip netns exec "$ns" sysctl -wq \
> + net.ipv6.conf.all.accept_dad=0 \
> + net.ipv6.conf.default.accept_dad=0
> + done
[ ... ]
> +probe_v6_relay()
> +{
> + local err got
> +
> + if ! err=$(ip -n "$RELAY" link add amtprobe type amt mode relay \
> + local "$RELAY6" dev br_gw 2>&1); then
> + case "$err" in
> + *"Local attribute is required"*|*"expected rather than"*|\
> + *"IPv6 address in an IPv4 attribute"*|\
> + *"IPv6 support is disabled"*)
> + echo "SKIP: no IPv6 AMT support: $err"
> + exit "$ksft_skip"
> + ;;
[Severity: Low]
Can the "IPv6 support is disabled" branch ever match?
In drivers/net/amt.c, amt_validate() returns that extack only when
CONFIG_IPV6 is off:
if (data[IFLA_AMT_LOCAL_IP6] && !IS_ENABLED(CONFIG_IPV6)) {
NL_SET_ERR_MSG_ATTR(extack, data[IFLA_AMT_LOCAL_IP6],
"IPv6 support is disabled");
On a kernel like that, the script never gets to the probe. The main body
runs setup_links() under the ERR trap before probe_v6_relay() runs:
set -E
trap 'setup_fail $LINENO' ERR
setup_links
trap - ERR
probe_v6_relay
setup_links() starts with the net.ipv6.conf.*.accept_dad sysctl writes
quoted above, then adds IPv6 addresses. Without IPv6, those commands fail,
and setup_fail() exits with ksft_fail.
The commit message says:
A probe creates a throwaway IPv6 relay first and skips only when the
kernel or iproute2 cannot create one
Would a kernel built without CONFIG_IPV6 report FAIL here rather than the
intended SKIP? The net selftest config sets CONFIG_IPV6=y, so this only
affects configurations that don't use it.
[ ... ]
> +disc_answer()
> +{
> + local dst=$1 filter=$2 opts=${3:-} pid i rc log
> + local to="UDP6-SENDTO:[$dst]:2268,sourceport=40000"
> +
> + log=$(mktemp)
> + ip netns exec "$GATEWAY" timeout 6 \
> + tcpdump -nni gw_relay -c 1 "$filter" > /dev/null 2> "$log" &
> + pid=$!
> + # Send nothing before the capture is live, or a missed packet would
> + # pass the negative check.
> + busywait 5000 grep -q "listening on" "$log"
> + # Type 1 (Relay Discovery), version 0, then a nonce.
> + for i in 1 2 3; do
> + printf '\x01\x00\x00\x00\x12\x34\x56\x78' |
> + ip netns exec "$GATEWAY" socat -u - \
> + "$to,so-bindtodevice=gw_relay$opts"
> + sleep 0.5
> + done
> + wait "$pid"
> + rc=$?
> + rm -f "$log"
> + return "$rc"
> +}
[Severity: Medium]
Can test_mcast_discovery() pass its negative check when no Discovery was
ever captured or sent?
The exit status of busywait is thrown away, and loopy_wait() in lib.sh
returns 1 on timeout. The status of each socat send is thrown away too.
The function returns only tcpdump's status. That status is non-zero when
no matching packet was seen (124 from timeout 6), and also when tcpdump
itself failed (1).
test_mcast_discovery() then does:
disc_answer ff02::1 "udp and src port 2268 and dst port 40000"
check_fail $? "the relay answered a Discovery sent to ff02::1"
So if the capture isn't live within 5 seconds, tcpdump fails to start, or
the send to ff02::1 fails, the check records a pass. This is the case the
comment above the busywait says must not happen.
The unicast positive control runs its own tcpdump and its own send. It
doesn't show that the second capture went live or that the ff02::1 send
worked.
test_zero_relay() calls disc_answer() in the same way, though its
Udp6InCsumErrors check limits the effect there.
Should disc_answer() return a separate failure when the busywait or the
socat sends fail, so the negative checks can tell that apart from no reply?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261009122426.551178-1-omar%40blockcast.net
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH net-next 13/13] selftests: net: add amt_gw_v6.sh for the IPv6 netlink attributes
2026-10-09 12:24 [PATCH net-next 00/13] amt: add an IPv6 outer transport Omar Ramadan
` (11 preceding siblings ...)
2026-10-09 12:24 ` [PATCH net-next 12/13] selftests: net: add amt_v6.sh for an IPv6 outer transport Omar Ramadan
@ 2026-10-09 12:24 ` Omar Ramadan
12 siblings, 0 replies; 23+ messages in thread
From: Omar Ramadan @ 2026-10-09 12:24 UTC (permalink / raw)
To: Taehee Yoo, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Shuah Khan
Cc: Simon Horman, netdev, linux-kselftest, linux-kernel
amt_v6.sh builds a gateway and a relay over an IPv6 outer transport and
checks the data plane, but only with valid attributes. The link-creation
rules this series adds for IFLA_AMT_LOCAL_IP6 and IFLA_AMT_DISCOVERY_IP6
are not exercised.
This adds a small companion without a data plane: no relay, socat or
smcroute. It checks that the IPv6 local and discovery addresses of a
gateway, and the local address of a relay, read back through
amt_fill_info(), that a relay reports no discovery address, that an
IPv4 gateway still creates and reports its IPv4 discovery address, and
that an IPv4 relay still accepts the IPv4 discovery address it has
always ignored. It then checks each rejection: a gateway without a
discovery address, a gateway whose local and discovery addresses differ
in family (both ways), an IPv6 relay given a discovery address of either
family, an IPv4 relay given an IPv6 one, and an unspecified, loopback,
multicast or IPv4-mapped IPv6 local or discovery address. Each rejection
is matched on its extack message as well as on the failure, so a rule
that fails for the wrong reason is not a pass.
Besides the duplicate arguments its header names, a 16-byte
IFLA_AMT_LOCAL_IP or IFLA_AMT_DISCOVERY_IP is not built: an iproute2
with IPv6 support never sends one. An older iproute2 puts the whole
16-byte literal into the IPv4 attribute, which a kernel without this
series reads as an IPv4 address from its first four bytes (32.1.13.184
for 2001:db8:a::2), so the probe checks the readback as well as the
result. This kernel refuses it ("IPv6 address in an IPv4 attribute"),
which is logged as a passing test before the probe skips. The probe
skips on an iproute2 that rejects the literal, on a kernel that ignores
the attributes ("Local attribute is required"), and on a kernel without
IPv6 ("IPv6 support is disabled", or no /proc/net/if_inet6); any other
failure to create the probe link is a FAIL.
The script uses lib.sh: setup_ns for the namespace, check_err and
log_test for reporting, and $ksft_skip when a prerequisite is missing.
Assisted-by: LLM
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/amt_gw_v6.sh | 204 +++++++++++++++++++++++
2 files changed, 205 insertions(+)
create mode 100755 tools/testing/selftests/net/amt_gw_v6.sh
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index efdc77b..4212915 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -9,6 +9,7 @@ CFLAGS += -I../
TEST_PROGS := \
altnames.sh \
amt.sh \
+ amt_gw_v6.sh \
amt_v6.sh \
arp_ndisc_evict_nocarrier.sh \
arp_ndisc_untracked_subnets.sh \
diff --git a/tools/testing/selftests/net/amt_gw_v6.sh b/tools/testing/selftests/net/amt_gw_v6.sh
new file mode 100755
index 0000000..c18ceff
--- /dev/null
+++ b/tools/testing/selftests/net/amt_gw_v6.sh
@@ -0,0 +1,204 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Netlink coverage for the AMT IPv6 attributes, IFLA_AMT_LOCAL_IP6 and
+# IFLA_AMT_DISCOVERY_IP6, and the rules that validate them. amt_v6.sh covers
+# the data plane; this needs only one netns and one dummy device.
+#
+# Not covered: `discovery <v4>` together with `discovery <v6>`, or `local <v4>`
+# together with `local <v6>`, in one command. iproute2 rejects the duplicate
+# argument itself, so those checks are reachable only from a raw netlink
+# client.
+
+source lib.sh
+
+readonly V6_LOCAL="2001:db8:a::1"
+readonly V6_DISC="2001:db8:a::2"
+readonly V4_LOCAL="192.168.0.1"
+readonly V4_DISC="192.168.0.2"
+
+# add_amt <name> <args...>: create an amt link on gw_dev, stderr in $ADD_ERR.
+add_amt()
+{
+ local name=$1; shift
+
+ ADD_ERR=$(ip -n "$GW" link add "$name" type amt dev gw_dev "$@" 2>&1)
+}
+
+# amt_field <name> <key>: one attribute as amt_fill_info reports it.
+amt_field()
+{
+ ip -n "$GW" -d -j link show "$1" 2>/dev/null |
+ jq -r ".[0].linkinfo.info_data.$2 // empty"
+}
+
+# An iproute2 without IFLA_AMT_DISCOVERY_IP6 may not reject `discovery
+# <v6>`: it can pack the literal into IFLA_AMT_DISCOVERY_IP, which this
+# kernel refuses and an older one turns into a v4 gateway from the first
+# four bytes. So probe on the readback, not only on the exit code. A
+# kernel without the IPv6 attributes ignores them and asks for a local
+# address. Skip in those cases only; any other failure is a failure of the
+# code under test.
+probe_v6_gateway()
+{
+ local got
+
+ if ! add_amt amtprobe mode gateway local "$V6_LOCAL" \
+ discovery "$V6_DISC"; then
+ case "$ADD_ERR" in
+ *"IPv6 address in an IPv4 attribute"*)
+ # An older iproute2 on this kernel: the rule that
+ # refuses the 16-byte IPv4 attribute is what ran.
+ RET=0
+ log_test "16-byte IPv4 address attribute is refused"
+ echo "SKIP: iproute2 lacks IPv6 AMT support"
+ exit "$ksft_skip"
+ ;;
+ *"Local attribute is required"*|*"expected rather than"*|\
+ *"IPv6 support is disabled"*)
+ echo "SKIP: no IPv6 AMT support: $ADD_ERR"
+ exit "$ksft_skip"
+ ;;
+ esac
+ echo "FAIL: cannot create an IPv6 gateway: $ADD_ERR"
+ exit "$ksft_fail"
+ fi
+ got=$(amt_field amtprobe discovery)
+ ip -n "$GW" link del amtprobe
+ if [[ "$got" != *:* ]]; then
+ # A kernel with the IPv6 attributes names the missing
+ # discovery address this way, and must have refused the
+ # 16-byte IPv4 attribute that an older iproute2 sent.
+ add_amt amtprobe mode gateway local "$V4_LOCAL"
+ ip -n "$GW" link del amtprobe 2>/dev/null
+ if [[ "$ADD_ERR" == *"of the local family is required"* ]]; then
+ echo "FAIL: 16-byte IPv4 attribute read back as '$got'"
+ exit "$ksft_fail"
+ fi
+ echo "SKIP: iproute2 lacks IPv6 AMT support (read back '$got')"
+ exit "$ksft_skip"
+ fi
+}
+
+test_v6_gateway_roundtrip()
+{
+ RET=0
+ add_amt amtg6 mode gateway local "$V6_LOCAL" discovery "$V6_DISC"
+ check_err $? "create failed: $ADD_ERR"
+ [ "$(amt_field amtg6 discovery)" = "$V6_DISC" ] ||
+ check_err 1 "discovery did not read back as $V6_DISC"
+ [ "$(amt_field amtg6 local)" = "$V6_LOCAL" ] ||
+ check_err 1 "local did not read back as $V6_LOCAL"
+ ip -n "$GW" link del amtg6 2>/dev/null
+ log_test "v6 gateway: discovery round-trips through fill_info"
+}
+
+test_v6_relay_roundtrip()
+{
+ RET=0
+ add_amt amtr6 mode relay local "$V6_LOCAL"
+ check_err $? "create failed: $ADD_ERR"
+ [ "$(amt_field amtr6 local)" = "$V6_LOCAL" ] ||
+ check_err 1 "local did not read back as $V6_LOCAL"
+ [ -z "$(amt_field amtr6 discovery)" ] ||
+ check_err 1 "a relay reported a discovery address"
+ ip -n "$GW" link del amtr6 2>/dev/null
+ log_test "v6 relay: local round-trips, no discovery reported"
+}
+
+test_v4_gateway_unchanged()
+{
+ RET=0
+ add_amt amtg4 mode gateway local "$V4_LOCAL" discovery "$V4_DISC"
+ check_err $? "create failed: $ADD_ERR"
+ [ "$(amt_field amtg4 discovery)" = "$V4_DISC" ] ||
+ check_err 1 "discovery did not read back as $V4_DISC"
+ ip -n "$GW" link del amtg4 2>/dev/null
+ log_test "v4 gateway still creates (no ABI change)"
+}
+
+# An IPv4 relay has always ignored IFLA_AMT_DISCOVERY_IP, and still must.
+test_v4_relay_unchanged()
+{
+ RET=0
+ add_amt amtr4 mode relay local "$V4_LOCAL" discovery "$V4_DISC"
+ check_err $? "create failed: $ADD_ERR"
+ ip -n "$GW" link del amtr4 2>/dev/null
+ log_test "v4 relay still accepts a v4 discovery (no ABI change)"
+}
+
+# expect_reject <description> <extack substring> <args...>: the create must
+# fail, and for the stated reason.
+expect_reject()
+{
+ local desc=$1 want=$2; shift 2
+
+ RET=0
+ add_amt amtbad "$@"
+ check_fail $? "link was created but should have been rejected"
+ ip -n "$GW" link del amtbad 2>/dev/null
+ grep -qi -- "$want" <<< "$ADD_ERR"
+ check_err $? "rejected for the wrong reason: $ADD_ERR"
+ log_test "$desc"
+}
+
+require_command jq
+if [ ! -e /proc/net/if_inet6 ]; then
+ echo "SKIP: the kernel has no IPv6"
+ exit "$ksft_skip"
+fi
+trap cleanup_all_ns EXIT
+setup_ns GW || exit "$ksft_skip"
+ip -n "$GW" link add gw_dev type dummy
+ip -n "$GW" link set gw_dev up
+
+probe_v6_gateway
+
+test_v6_gateway_roundtrip
+test_v6_relay_roundtrip
+test_v4_gateway_unchanged
+test_v4_relay_unchanged
+expect_reject "gateway without discovery is rejected" \
+ "Discovery attribute" \
+ mode gateway local "$V6_LOCAL"
+expect_reject "gateway v6 local + v4 discovery is rejected" \
+ "of the local family is required" \
+ mode gateway local "$V6_LOCAL" discovery "$V4_DISC"
+expect_reject "gateway v4 local + v6 discovery is rejected" \
+ "of the local family is required" \
+ mode gateway local "$V4_LOCAL" discovery "$V6_DISC"
+expect_reject "v6 discovery in relay mode is rejected" \
+ "only valid in gateway mode" \
+ mode relay local "$V6_LOCAL" discovery "$V6_DISC"
+expect_reject "v4 discovery on a v6 relay is rejected" \
+ "only valid in gateway mode" \
+ mode relay local "$V6_LOCAL" discovery "$V4_DISC"
+expect_reject "v6 discovery on a v4 relay is rejected" \
+ "only valid in gateway mode" \
+ mode relay local "$V4_LOCAL" discovery "$V6_DISC"
+expect_reject "unspecified v6 local is rejected" \
+ "Invalid Local IPv6" \
+ mode relay local ::
+expect_reject "v4-mapped v6 local is rejected" \
+ "Invalid Local IPv6" \
+ mode relay local ::ffff:192.168.0.1
+expect_reject "loopback v6 local is rejected" \
+ "Invalid Local IPv6" \
+ mode relay local ::1
+expect_reject "multicast v6 local is rejected" \
+ "Invalid Local IPv6" \
+ mode relay local ff02::1
+expect_reject "multicast v6 discovery is rejected" \
+ "must be unicast" \
+ mode gateway local "$V6_LOCAL" discovery ff02::1
+expect_reject "loopback v6 discovery is rejected" \
+ "must be unicast" \
+ mode gateway local "$V6_LOCAL" discovery ::1
+expect_reject "unspecified v6 discovery is rejected" \
+ "must be unicast" \
+ mode gateway local "$V6_LOCAL" discovery ::
+expect_reject "v4-mapped v6 discovery is rejected" \
+ "must be unicast" \
+ mode gateway local "$V6_LOCAL" discovery ::ffff:192.168.0.2
+
+exit "$EXIT_STATUS"
--
2.43.0
^ permalink raw reply [flat|nested] 23+ messages in thread