tcp_v4_connect 三次路由查询详解

概述

tcp_v4_connect() 为例,一次 TCP 连接建立会触发 3 次路由查询,每次的目的和 flowi4 参数不同。这三次查询与 ECMP 多路径选路机制密切相关,参见 ECMP选路: fib_select_multipath Hash 机制深度解析

三次查询对比

查询 触发函数 saddr sport hash 特征 主要目的
第 1 次 __ip_route_output_key() 0 或已绑定 0(wildcard) 随机 sport → 随机 hash 获取源/目的地址
第 2 次 ip_route_output_flow() 已确定 0(wildcard) 随机 sport → 随机 hash 正式路由 + xfrm 策略
第 3 次 ip_route_newports() 已确定 真实端口 真实五元组 → 确定性 hash 端口变更后重查 xfrm

第 1 次查询:ip_route_connect() → __ip_route_output_key()

1
2
3
4
5
/* include/net/route.h — ip_route_connect() */
/* 条件:!dst || !src,即目的或源地址未确定 */
rt = __ip_route_output_key(net, fl4); /* 查询获取地址 */
ip_rt_put(rt); /* 释放,只取地址不取路由 */
flowi4_update_output(fl4, oif, fl4->daddr, fl4->saddr); /* 回写地址 */

此时 fl4 的关键状态:

  • saddr = 0(未绑定源地址时)或已绑定的源地址
  • sport = orig_sport(通常为 0,即 wildcard)
  • flow_flags = FLOWI_FLAG_ANY_SPORT(因为 sport=0,由 ip_route_connect_init() 设置)

目的:获取源地址(saddr)和目的地址(daddr),为后续端口分配提供地址信息。此次查询中 fib_multipath_hash()FLOWI_FLAG_ANY_SPORT 会用随机源端口做 hash(L4 策略下),使新连接随机分散到不同 nexthop。查询完成后路由对象被释放,只保留地址。

第 2 次查询:ip_route_connect() → ip_route_output_flow()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* include/net/route.h — ip_route_connect() 末尾 */
return ip_route_output_flow(net, fl4, sk);

/* net/ipv4/route.c — ip_route_output_flow() */
struct rtable *ip_route_output_flow(struct net *net, struct flowi4 *flp4,
const struct sock *sk)
{
struct rtable *rt = __ip_route_output_key(net, flp4); /* 再次 FIB 查找 + ECMP */
if (flp4->flowi4_proto) {
flp4->flowi4_oif = rt->dst.dev->ifindex;
rt = xfrm_lookup_route(net, &rt->dst, ...); /* IPsec/xfrm 策略查找 */
}
return rt;
}

此时 fl4 的关键状态(与第 1 次的区别):

  • saddr = 第 1 次查询得到的源地址(非零
  • sport = orig_sport(仍为 0)
  • flow_flags = 仍含 FLOWI_FLAG_ANY_SPORT
  • flowi4_oif = 可能已被 flowi4_update_output() 回写

目的:用回填的地址做正式路由查找,并经过 xfrm(IPsec)策略 检查。如果配置了 IPsec,可能返回一条加密隧道路由。此次 hash 仍然带随机端口(sport 还是 0),可能选中与第 1 次不同的 nexthop。

第 3 次查询:ip_route_newports()

1
2
3
4
5
6
7
8
9
10
11
12
/* tcp_v4_connect() 中,inet_hash_connect() 分配端口之后 */
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet->inet_sport, inet->inet_dport, sk);

/* include/net/route.h — ip_route_newports() */
if (sport != orig_sport || dport != orig_dport) { /* 端口变了才重查 */
fl4->fl4_dport = dport;
fl4->fl4_sport = sport; /* 填入真实端口 */
ip_rt_put(rt);
flowi4_update_output(fl4, sk->sk_bound_dev_if, fl4->daddr, fl4->saddr);
return ip_route_output_flow(sock_net(sk), fl4, sk);
}

此时 fl4 的关键状态(与前两次的区别):

  • saddr = 已确定的源地址
  • sport = inet_hash_connect() 分配的真实源端口(非零)
  • dport = 目的端口
  • flow_flags = 不再含 FLOWI_FLAG_ANY_SPORT(因为 sport 非零)

目的:端口分配后用完整五元组重查路由,确保 IPsec 等基于端口的策略能正确匹配。此次 hash 用的是真实端口号,产生的 hash 值与前两次大概率不同。

问题:源地址不一致

三次查询的 hash 可能各不相同,导致选中不同的 nexthop。第 1 次选了 NH0(saddr=10.0.0.1),第 3 次可能选了 NH1(saddr=10.0.1.1),但 fl4->saddr 已经锁定为 10.0.0.1。数据包从 NH1 出去但带着 NH0 的源 IP,在双 ISP 场景下会被运营商丢包。

这就是 v6.16 引入 源地址优选 机制要解决的问题,详见 ECMP选路: fib_select_multipath Hash 机制深度解析 第 4.4 节。