From 78213fc62683d625bd9561897c550a5295dda112 Mon Sep 17 00:00:00 2001 From: Loic Lambiel Date: Fri, 4 Sep 2026 10:56:26 +0200 Subject: [PATCH] tcp/ipv6: re-resolve route after binding the ephemeral source port tcp_v6_connect() resolves the route before the ephemeral source port is bound: fl6->fl6_sport is still 0 and, since commit 65e9024643c7 ("ip: load balance tcp connections to single dst addr and port"), FLOWI_FLAG_ANY_SPORT makes rt6_multipath_hash() substitute a random source port so that connections to a single destination still spread over the available next hops. The resulting dst is cached on the socket by ip6_dst_store() and, unlike tcp_v4_connect() which re-resolves via ip_route_newports(), it is never refreshed with the port that inet6_hash_connect() goes on to bind. Every later route rebuild for the socket goes through inet6_csk_route_socket(), which starts from a zeroed fl6 and fills in the real inet->inet_sport. So under a port-based multipath hash policy (fib_multipath_hash_policy 1 or 3) the connect-time next hop and the steady-state next hop are hashed from different source ports, and in general are not the same next hop. The difference stays hidden while the cached dst is valid. IPv6 socket dst entries are revalidated against the FIB node cookie, so when something invalidates the cached dst -- unrelated route churn in the same table, or, since commit 658eb696544c ("tcp: rehash onto different local ECMP path on retransmit timeout"), a retransmission timeout -- the socket re-resolves and the established flow silently moves to a different next hop mid-connection. This is harmless for ordinary unicast multipath, where every next hop reaches the same destination. It breaks configurations where per-next-hop state matters, e.g. an anycast destination whose next hops each NAT to a different backend and keep per-path connection state: the flow arrives at a next hop that holds no state for it and is reset. Commit 658eb696544c ("tcp: rehash onto different local ECMP path on retransmit timeout") addressed the equivalent problem for fib_multipath_hash_policy 0, by keying fl6->mp_hash on sk_txhash and selecting the initial path with it so that the connect-time and steady-state lookups agree. Policies 1 and 3 hash the source port and are still exposed. Fix those the same way IPv4 does: once inet6_hash_connect() has bound the ephemeral port, install it in fl6, clear FLOWI_FLAG_ANY_SPORT and resolve again, so the next hop cached on the socket is the one every subsequent lookup will select. The source address is already final at this point -- the first lookup selected it and wrote it back into fl6->saddr. The second lookup only runs when the port was not already bound before connect(), and it is a no-op for policy 0, where fl6->mp_hash carries the first lookup's decision. Fixes: 51ebd3181572 ("ipv6: add support of equal cost multipath (ECMP)") Signed-off-by: Loic Lambiel --- net/ipv6/tcp_ipv6.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/net/ipv6/tcp_ipv6.c b/net/ipv6/tcp_ipv6.c index b55d036c7f4dac..559cd0aa851a60 100644 --- a/net/ipv6/tcp_ipv6.c +++ b/net/ipv6/tcp_ipv6.c @@ -325,6 +325,30 @@ static int tcp_v6_connect(struct sock *sk, struct sockaddr_unsized *uaddr, if (err) goto late_failure; + /* The route above was resolved before the ephemeral source port was + * bound: fl6->fl6_sport was 0 and FLOWI_FLAG_ANY_SPORT had + * rt6_multipath_hash() substitute a random source port, so under a + * port-based hash policy the next hop was picked for a port this + * connection does not use. Every later route rebuild goes through + * inet6_csk_route_socket(), which hashes the real source port, so the + * flow would move to a different next hop the first time the cached + * dst is invalidated. Re-resolve now that the port is final, as + * tcp_v4_connect() does via ip_route_newports(), so the next hop + * stored here is the one every later lookup selects. The source + * address is already final: the first lookup wrote it into fl6->saddr. + */ + if (fl6->fl6_sport != inet->inet_sport) { + fl6->fl6_sport = inet->inet_sport; + fl6->flowi6_flags &= ~FLOWI_FLAG_ANY_SPORT; + security_sk_classify_flow(sk, flowi6_to_flowi_common(fl6)); + dst = ip6_dst_lookup_flow(net, sk, fl6, final_p); + if (IS_ERR(dst)) { + err = PTR_ERR(dst); + goto late_failure; + } + ip6_dst_store(sk, dst, false, false); + } + if (likely(!tp->repair)) { union tcp_seq_and_ts_off st;