[PATCH net-next 02/13] amt: send the Relay Advertisement over IPv6

From: Omar Ramadan

Date: Fri Oct 09 2026 - 08:31:51 EST


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