ICMP Redirect 配置

背景

我的网络环境有一个主路由负责分发流量,但是很多流量其实不需要走主路由,因为主路由通常也只是简单地forward流量,但因为路由规则复杂且动态,无法在局域网的每一台设备上都配置路由。 回包可以不用走主路由,所以这是一个不对称网络链路,这个拓扑其实已经很优化了,大部分时候也不会有性能问题。 网络拓扑如下: router1 但是本着追求极致的精神,发现了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:地址,解决了

新拓扑

新的网络拓扑如下,主路由直接向设备发送优化后的路由规则,效率更高。 router2