The tcp_metrics bucket is derived from a fold of the peer address.
For IPv4 the fold is ipv4_addr_hash(), which returns the address
unchanged. For IPv6 it is ipv6_addr_hash(), which XORs the four
32-bit words of the address. net_hash_mix() is only XORed in after
that fold, so two addresses of either family with the same fold share
a bucket in every netns.
A host owning an IPv6 /64 can thus compute, without knowing
net_hash_mix(), addresses that collide with a chosen IPv4 or IPv6 peer:
w3 = w2 ^ p0 ^ p1 ^ target /* p0, p1: prefix, w2: free */
A bucket holds six entries and the seventh insert replaces the oldest.
An off-path host can fill a peer's bucket with its own entries and
later tell whether one was evicted, because tcp_init_metrics() seeds
the initial RTO from the cached RTT. This reveals whether that peer,
or another address in the same bucket, connected in the meantime.
XOR and multiply is not a PRF, so net_hash_mix() can be recovered the
same way, after which evictions leak about 14 bits of any client's
address, depending on the number of buckets.
Hash the address with SipHash and a random key instead, as commit
2ee66e948717 ("inetpeer: randomize RB-tree node comparison using SipHash")
did for inet_peer. net_hash_mix() and hash_32() are kept, to separate
namespaces and to reduce the result to the table size.
Fixes: 51c5d0c4b169 ("tcp: Maintain dynamic metrics in local cache.")
Cc: <redacted>
Signed-off-by: Juan Agustin Niccolini <redacted>
---
This can be exploited remotely to recover a client's IPv4 address.
A single host narrows the address to about 262 thousand candidates,
and because each netns keys the hash differently, intersecting buckets
extracted from multiple hosts converges quickly into one candidate;
3-5 are enough. The net_hash_mix() value only needs to be extracted
once per boot per host.
Many common cases facilitate this. For instance, a browser loading
one page connects to several hosts within the same second. That
supplies the buckets needed to converge, and the shared timing shows
they belong to the same client.
I reproduced the bug on a QEMU guest running mainline (2a500a04d6f9) and
against Ubuntu 24.04 over the internet. With the patch applied, the
reproducer no longer finds addresses that share a bucket. The
reproducer is available on request.
For stable: 5.10 and 5.15 lack siphash_aligned_key_t;
"static siphash_key_t tcpm_hash_key __read_mostly __aligned(16);" can
be used there.
This bug was discovered during an LLM-based code review.
net/ipv4/tcp_metrics.c | 32 +++++++++++++++++++++++++-------
1 file changed, 25 insertions(+), 7 deletions(-)
--- a/net/ipv4/tcp_metrics.c
+++ b/net/ipv4/tcp_metrics.c
@@ -8,6 +8,7 @@
#include <linux/init.h>
#include <linux/tcp.h>
#include <linux/hash.h>
+#include <linux/siphash.h>
#include <linux/tcp_metrics.h>
#include <linux/vmalloc.h>
@@ -94,6 +95,23 @@
static DEFINE_SPINLOCK(tcp_metrics_lock);
static DEFINE_SEQLOCK(fastopen_seqlock);
+
+static siphash_aligned_key_t tcpm_hash_key __read_mostly;
+
+static u32 tcpm_addr_hash(const struct inetpeer_addr *a)
+{
+ net_get_random_once(&tcpm_hash_key, sizeof(tcpm_hash_key));
+
+ if (a->family == AF_INET)
+ return siphash_2u32((__force u32)a->a4.addr, a->a4.vif,
+ &tcpm_hash_key);
+
+ return siphash_4u32((__force u32)a->a6.s6_addr32[0],
+ (__force u32)a->a6.s6_addr32[1],
+ (__force u32)a->a6.s6_addr32[2],
+ (__force u32)a->a6.s6_addr32[3],
+ &tcpm_hash_key);
+}
static void tcpm_suck_dst(struct tcp_metrics_block *tm,
const struct dst_entry *dst,@@ -270,13 +288,13 @@
case AF_INET:
inetpeer_set_addr_v4(&saddr, inet_rsk(req)->ir_loc_addr);
inetpeer_set_addr_v4(&daddr, inet_rsk(req)->ir_rmt_addr);
- hash = ipv4_addr_hash(inet_rsk(req)->ir_rmt_addr);
+ hash = tcpm_addr_hash(&daddr);
break;
#if IS_ENABLED(CONFIG_IPV6)
case AF_INET6:
inetpeer_set_addr_v6(&saddr, &inet_rsk(req)->ir_v6_loc_addr);
inetpeer_set_addr_v6(&daddr, &inet_rsk(req)->ir_v6_rmt_addr);
- hash = ipv6_addr_hash(&inet_rsk(req)->ir_v6_rmt_addr);
+ hash = tcpm_addr_hash(&daddr);
break;
#endif
default:
@@ -310,18 +328,18 @@
if (sk->sk_family == AF_INET) {
inetpeer_set_addr_v4(&saddr, inet_sk(sk)->inet_saddr);
inetpeer_set_addr_v4(&daddr, inet_sk(sk)->inet_daddr);
- hash = ipv4_addr_hash(inet_sk(sk)->inet_daddr);
+ hash = tcpm_addr_hash(&daddr);
}
#if IS_ENABLED(CONFIG_IPV6)
else if (sk->sk_family == AF_INET6) {
if (ipv6_addr_v4mapped(&sk->sk_v6_daddr)) {
inetpeer_set_addr_v4(&saddr, inet_sk(sk)->inet_saddr);
inetpeer_set_addr_v4(&daddr, inet_sk(sk)->inet_daddr);
- hash = ipv4_addr_hash(inet_sk(sk)->inet_daddr);
+ hash = tcpm_addr_hash(&daddr);
} else {
inetpeer_set_addr_v6(&saddr, &sk->sk_v6_rcv_saddr);
inetpeer_set_addr_v6(&daddr, &sk->sk_v6_daddr);
- hash = ipv6_addr_hash(&sk->sk_v6_daddr);
+ hash = tcpm_addr_hash(&daddr);
}
}
#endif@@ -818,7 +836,7 @@
if (a) {
inetpeer_set_addr_v4(addr, nla_get_in_addr(a));
if (hash)
- *hash = ipv4_addr_hash(inetpeer_get_addr_v4(addr));
+ *hash = tcpm_addr_hash(addr);
return 0;
}
a = info->attrs[v6];@@ -828,7 +846,7 @@
in6 = nla_get_in6_addr(a);
inetpeer_set_addr_v6(addr, &in6);
if (hash)
- *hash = ipv6_addr_hash(inetpeer_get_addr_v6(addr));
+ *hash = tcpm_addr_hash(addr);
return 0;
}
return optional ? 1 : -EAFNOSUPPORT;