ICMP Redirect 配置
背景
我的网络环境有一个主路由负责分发流量,但是很多流量其实不需要走主路由,因为主路由通常也只是简单地forward流量,但因为路由规则复杂且动态,无法在局域网的每一台设备上都配置路由。 回包可以不用走主路由,所以这是一个不对称网络链路,这个拓扑其实已经很优化了,大部分时候也不会有性能问题。
网络拓扑如下:
但是本着追求极致的精神,发现了ICMP Redirect协议,可以用来进一步优化路由.
操作
ipv4开启Redirect
打开flag:
sysctl -w net.ipv4.conf.all.send_redirects=1
发现发送了ICMP redirect,但是发送的源IP不太对。即使通过iptables 结合策略路由也无法选择正确的源IP. 路由表中的src只是建议而不是决定,如果在发送包之前,就对socket bind了local address(无论直接还是间接,例如connect也会隐含bind),那么路由表中的src就是没有用了。 最终调整了网络绑定的IP顺序,将期望src IP放在了第一个解决。
ipv6就要曲折得多了
现在Ipv4 Redirect 能发送但是v6不行.
这应该是路由器内核协议栈的问题,使用bpftrace查看linux内核包处理流程。
Linux IPv6发送ICMP redirect的路径是
net/ipv6/ip6_output.c:ip6_forward()
-> net/ipv6/ndisc.c:ndisc_send_redirect()
于是追踪ndisc_send_redirect()
# bpftrace -l | grep ndisc_send_redirect
fentry:vmlinux:ndisc_send_redirect
kprobe:ndisc_send_redirect
开始追踪
#!/usr/bin/env bpftrace
fentry:vmlinux:ndisc_send_redirect {
$ip6h = (struct ipv6hdr *)(args->skb->head + args->skb->network_header);
$src = $ip6h->saddr.in6_u.u6_addr8;
$dst = $ip6h->daddr.in6_u.u6_addr8;
printf("Redirect - src: %s, dst: %s, hop: %s\n", ntop($src), ntop($dst), ntop(args->target->in6_u.u6_addr8));
}
发现正常调用了ndisc_send_redirect, 而且src,dst,next hop都是正确的
Redirect - src: fdxx::xxx, dst: 240e:xxx, hop: fdxx::xxx
但是却没有真正发出包来
看到源码里面有打日志
ND_PRINTK(2, warn, "Redirect: no link-local address on %s\n",
dev->name);
进一步发现,开启日志需要重新编译内核。本来这儿还想都使用预处理了,还是用普通的C语言语句,不会导致生成多余的指令吗。不过这对于现代编译器的优化能力应该没有问题,if语句的条件在编译时就确定,优化器应该有能力优化掉这段指令。
#define ND_DEBUG 1
#define ND_PRINTK(val, level, fmt, ...) \
do { \
if (val <= ND_DEBUG) \
net_##level##_ratelimited(fmt, ##__VA_ARGS__); \
} while (0)
不太想编译内核来调试。打算probe被调用的函数,看看是哪儿中断了,又担心被调函数在其他地方被调用会干扰分析。所以可以把栈打印出来排除干扰。
kprobe:ipv6_get_lladdr{
$caller = (kstack(3));
printf("ipv6_get_lladdr called with %s\n", $caller);
}
kretprobe:ipv6_get_lladdr{
$caller = (kstack(3));
printf("ipv6_get_lladdr return %lld with %s\n", retval, $caller);
}
kprobe:icmpv6_flow_init{
$caller = kstack(3);
printf("icmpv6_flow_init called with %s\n", $caller);
}
运行,结果
Redirect - src: fdxx::xxx, dst: 240e:xxx, hop: fdxx::xxx
ipv6_get_lladdr called with
ipv6_get_lladdr+5
ndisc_send_redirect+230
ip6_forward+2384
ipv6_get_lladdr return 0 with
ndisc_send_redirect+230
ip6_forward+2384
__netif_receive_skb_one_core+96
...
icmpv6_flow_init called with
icmpv6_flow_init+5
ndisc_send_skb+603
ndisc_send_ns+110
其中 icmpv6_flow_init called 是干扰项,因为它并不是被ndisc_send_redirect调用的。而且ipv6_get_lladdr正常返回了。
结合linux源码,确定问题出在这块儿代码
if (!ipv6_addr_equal(&ipv6_hdr(skb)->daddr, target) &&
ipv6_addr_type(target) != (IPV6_ADDR_UNICAST|IPV6_ADDR_LINKLOCAL)) {
ND_PRINTK(2, warn,
"Redirect: target address is not link-local unicast\n");
return;
}
确定需要IPV6_ADDR_LINKLOCAL类型的地址, 这儿也就是需要FE80打头的地址
if ((st & htonl(0xFFC00000)) == htonl(0xFE800000))
return (IPV6_ADDR_LINKLOCAL | IPV6_ADDR_UNICAST |
IPV6_ADDR_SCOPE_TYPE(IPV6_ADDR_SCOPE_LINKLOCAL));
大功告成
配置路由,将via 地址从fdxx:改为fe80:地址,解决了
新拓扑
新的网络拓扑如下,主路由直接向设备发送优化后的路由规则,效率更高。