From: Omar Ramadan <omar@blockcast.net>
To: Taehee Yoo <ap420073@gmail.com>,
Andrew Lunn <andrew+netdev@lunn.ch>,
"David S. Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@kernel.org>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>
Cc: netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
Simon Horman <horms@kernel.org>
Subject: [PATCH net 1/4] amt: key relay tunnel state on the (address, port) endpoint, not the address
Date: Thu, 8 Oct 2026 00:36:02 +0000 [thread overview]
Message-ID: <20261008003606.3666617-2-omar@blockcast.net> (raw)
In-Reply-To: <20261008003606.3666617-1-omar@blockcast.net>
amt_request_handler and amt_update_handler both look a tunnel up by the
outer source address alone:
list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list)
if (tunnel->ip4 == iph->saddr)
goto send;
RFC 7450 s4.2.2 defines the unit of relay tunnel state differently: "an
AMT 'tunnel' is identified by the IP address and UDP port pair used as
the destination address for sending encapsulated multicast IP datagrams
to a gateway", and "each unique combination represents a unique tunnel
endpoint".
Because the port term is missing, two distinct endpoints that share a
source address alias onto one tunnel. The second Request to arrive
reaches `send:`, overwrites tunnel->nonce and re-derives tunnel->mac, so
the first gateway's subsequent Membership Updates no longer match and
are dropped at the "Invalid MAC" arm. That arm returns rather than
continuing the walk, so there is no recovery path: the first gateway has
had its Request answered and its membership accepted, and simply never
receives data again. The failure is silent on both sides.
Two deployments reach this, and the same RFC section names both:
- NAT, which s4.2.2 calls out explicitly ("this address may differ from
that carried by the message when it exited the gateway as a result of
network address translation"). CGNAT, a single-WAN site with a
redundant gateway pair, or two subscriber devices behind one
residential NAT all present as one source address.
- A single gateway host, with no NAT anywhere, which s4.2.2 says "may
use separate ports for the IPv4/IGMP and IPv6/MLD protocols".
Add the port term to both lookups. amt_update_handler snapshots the
source port before iptunnel_pull_header() strips the encap, alongside
the existing pre-pull reads.
With the endpoint keyed correctly, a gateway that re-Requests from a new
ephemeral port no longer aliases onto its own previous tunnel: it gets a
new one addressed to the port it is listening on, and the old one ages
out on gc_wq. That is the same stale-Membership-Query symptom addressed
by refreshing tunnel->source_port at `send:`, fixed at the cause instead
-- so this change supersedes that approach rather than stacking on it.
Also count the "Invalid MAC" drop in rx_dropped. It is currently a
netdev_dbg only, and an Update dropped for failing validation is the one
delivery failure a gateway cannot observe from its own side.
Note this removes an accidental bound: while tunnels were keyed on the
address alone, one source address could never hold more than one tunnel,
whatever it did. RFC 7450 s5.3.3 asks for that bound explicitly, and it
is restored in a companion net-next patch ("amt: bound relay tunnels
admitted per source address") rather than here, since it adds UAPI and
this is a fix.
Fixes: cbc21dc1cfe9 ("amt: add data plane of amt interface")
Signed-off-by: Omar Ramadan <omar@blockcast.net>
---
drivers/net/amt.c | 24 ++++++++++++++++++++++--
1 file changed, 22 insertions(+), 2 deletions(-)
diff --git a/drivers/net/amt.c b/drivers/net/amt.c
index f2f3139e3..a652c8c79 100644
--- a/drivers/net/amt.c
+++ b/drivers/net/amt.c
@@ -2455,6 +2455,7 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
struct ethhdr *eth;
struct iphdr *iph;
int len, hdr_size;
+ __be16 sport;
iph = ip_hdr(skb);
@@ -2466,13 +2467,17 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
if (amtmu->reserved || amtmu->version)
return true;
+ /* Snapshot the tunnel endpoint port before the encap is stripped. */
+ sport = udp_hdr(skb)->source;
+
if (iptunnel_pull_header(skb, hdr_size, skb->protocol, false))
return true;
skb_reset_network_header(skb);
list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
- if (tunnel->ip4 == iph->saddr) {
+ if (tunnel->ip4 == iph->saddr &&
+ tunnel->source_port == sport) {
if ((amtmu->nonce == tunnel->nonce &&
amtmu->response_mac == tunnel->mac)) {
mod_delayed_work(amt_wq, &tunnel->gc_wq,
@@ -2480,7 +2485,13 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
* 3);
goto report;
} else {
+ /* The endpoint match is unique, so no other
+ * tunnel can validate this Update. Count the
+ * drop: an unauthenticated Update is not
+ * observable from the gateway's own side.
+ */
netdev_dbg(amt->dev, "Invalid MAC\n");
+ amt->dev->stats.rx_dropped++;
return true;
}
}
@@ -2681,7 +2692,8 @@ 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 (tunnel->ip4 == iph->saddr &&
+ tunnel->source_port == udph->source)
goto send;
spin_lock_bh(&amt->lock);
@@ -2719,6 +2731,14 @@ static bool amt_request_handler(struct amt_dev *amt, struct sk_buff *skb)
spin_unlock_bh(&amt->lock);
send:
+ /* source_port is part of the tunnel's identity and is set once, in
+ * the allocation path above; the lookup only reaches here on an
+ * exact (address, port) match, so it is already udph->source. A
+ * gateway that re-Requests from a new ephemeral port no longer
+ * aliases onto this tunnel -- it gets its own, and this one ages
+ * out on gc_wq. Do not "refresh" the port here: that is what made
+ * a colliding Request steal an established tunnel outright.
+ */
tunnel->nonce = amtrh->nonce;
mac = siphash_3u32((__force u32)tunnel->ip4,
(__force u32)tunnel->source_port,
--
2.43.0
next prev parent reply other threads:[~2026-10-08 0:36 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-08 0:36 [PATCH net 0/4] amt: fix relay tunnel keying and unauthenticated-Request DoS Omar Ramadan
2026-10-08 0:36 ` Omar Ramadan [this message]
2026-10-08 0:36 ` [PATCH net 2/4] amt: send the relay General Query directly instead of via dev_queue_xmit Omar Ramadan
2026-10-08 0:36 ` [PATCH net 3/4] amt: make pre-query report drops visible Omar Ramadan
2026-10-08 0:36 ` [PATCH net 4/4] amt: do not create tunnel state for unauthenticated Requests Omar Ramadan
2026-10-08 0:39 ` [PATCH net 0/4] amt: fix relay tunnel keying and unauthenticated-Request DoS netdev-bot+sinfo
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261008003606.3666617-2-omar@blockcast.net \
--to=omar@blockcast.net \
--cc=andrew+netdev@lunn.ch \
--cc=ap420073@gmail.com \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=horms@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®