闲谈IPv6-Anycast以及在Linux/Win7系统上的Anycast配置[通俗易懂]

2022-08-25 11:05:37 浏览数 (2)

大家好,又见面了,我是你们的朋友全栈君。

正则安安每晚每隔三小时必然哭闹,我索性也就不睡了,反正也睡不好,起来泡茶,喝酒,作文。

浙江温州皮鞋?湿,下雨☔️进水不会胖!

杭州,外面依然是寒雨夜,屋里也没开空调,我穿个夏天的短袖,旁边放一杯热茶,喝完了还有昨晚实在喝不下去的汉拿山烧酒,再喝完还有上周菜市场买的米酒…作此文一篇。


在我们check IPv6的基本特征列表时,总是可以看到IPv6对Anycast的支持。说实话,对于很多人而言,这是个比较陌生的概念,对于希望看看Anycast到底是什么样子的人而言,甚至在网上很难搜到关于 如何配置Anycast 的资源。这是比较令人遗憾的。

抛开概念,那么本文尝试从不同的角度来针对Anycast探究一番。


IPv4年代的Anycast

说起Anycast,并不是在IPv6标准中突然出现的概念,一个概念怎么可能突然出现?不可能的。

早在很久很久以前,业界就针对IPv4提出了Anycast的说法,只不过相对而言,IPv6在操作上将其标准化了而已,如果说IPv4年代的Anycast标准只是 建议 ,那么IPv6的Anycast就是规定了些许 MUSTMAY

建议阅读: RFC1546-Host Anycasting Service:https://tools.ietf.org/html/rfc1546 RFC3513:IPv6 Addressing Architecture :https://tools.ietf.org/html/rfc3513#section-2.6

那么,到底如何理解Anycast?


本质上, Anycast就是将同一个IP地址配置在不同的主机网卡上,然后利用各种选路机制欺骗源主机的一种通信方式。

什么?同一个IP地址配置在不同的主机上,这不是地址冲突了吗?我们记得在初学网络基础的时候,教程上就讲过 IP地址不能冲突! 现在为什么IP冲突变成了一种通信方式了呢?真是只许州官放火,不许百姓点灯啊!

其实不然,我倒是觉得Anycast是内功深到一定程度,自然而然的一个想法。我们简单地从路由说起。

我们知道,IP地址存在的目的就是为了指挥路由器选路,最终将数据包路由到目的地,那么IP地址冲突的结果是什么?

IP地址冲突不是问题,路由冲突才是!! IP地址冲突只有导致路由器的路由冲突(be confusing)的时候才有问题。

比如路由器R上配置的下面的两条路由:

代码语言:javascript复制
1.1.1.1 nexthop 2.2.2.2 dev e2
1.1.1.1 nexthop 3.3.3.3 dev e3

请问一个去往目的地1.1.1.1的数据包到达路由器R之后到底是从e2走呢,还是从e3走呢?这就是问题。但是同样的路由,加一个约束就不会有问题:

代码语言:javascript复制
1.1.1.1 nexthop 2.2.2.2 dev e2 metric 100
1.1.1.1 nexthop 3.3.3.3 dev e3 metric 10

路由器会毫不犹豫地将去往1.1.1.1的数据包从e3发出!

对于上述的两条路由,你说是IP地址冲突吗?不!并不是。

互联网本身就是一个互相网状连通的连通图,到达同一个目的地的路径不止一条,对于路由器R而言,它只管选路,逐跳转发数据包,它并不关注1.1.1.1这个目标地址到底在哪里。

好了,现在我们知道只要路由不冲突,就没有问题,数据包总是可以特定的路径,逐跳被转发,最终到达目的地。现在,我们看一下这条路是如何形成的,或者说路由器R是怎么知道到达1.1.1.1有两条路可走的。

由于这只是一篇散文,并不是技术文档,所以我并不想引入BGP,AS这些概念,姑且把全球的互联网看作是一张如下图所示的连通图:

然后在这张图标识的网络开始工作时,各个节点开始彼此交换自己携带的子网信息,我们可以将其理解为 路由通告。显然路由器节点P向上游路由器R0通告了子网1.1.1.0/24,意思就是说, “嗨,R0邻居兄弟,如果有到达1.1.1.0/24的包,请交给我” ,同样的信息,路由器P也会告诉它的所有其它邻居,然后路由器R0回复路由器P,放心吧,我知道了,我已经配置上了1.1.1.0/24 nexthop 4.4.4.4 dev e0,并且我也已经将这个消息转给了我的所有邻居,放心吧,它们如果有到达你那里1.1.1.0/24的包,会先交给我的…

就这样,子网信息,路由信息在整张网上彼此交换,传播,最终在每一台路由器上形成了稳定的路由表:

请注意,A会在两个接口同时收到关于1.1.1.0/24的通告,这并不会造成路由冲突:

  • 常规路由协议以及SPF算法均有避环的措施;
  • 有意为之的环被视为备份路由,由Metric做优先级取舍或者由ECMP管理。

如果说在这张网中有A和C需要访问1.1.1.1,那么很显然,路径如下:

非常好,道路是通达的,但是并不完美!Why?

这会造成路由器R1和路由器P之间的链路异常拥堵,为什么R0不能分担一部分流量呢?

嗯,我能想到的,TCP/IP标准化协会的那帮人难道能想不到?这就是ECMP的由来。再后来,干脆来个SDN全局统一分发好了…但是这和Anycast没有半毛钱的关系,所以就此打住,我们来看看另外一种引出Anycast的解法。

试想, 如果B节点也通告1.1.1.0/24的路由会怎样?

按照前面的路子,我们知道,B和P均通告相同的路由,这会造成图上所有路由器均会收到来自B和P关于1.1.1.0/24的通告,按照上面的两个基本原则:

  • 常规路由协议以及SPF算法均有避环的措施;
  • 有意为之的环被视为备份路由,由Metric做优先级取舍或者由ECMP管理。

你猜怎么着?最终成了下面的样子了:

是不是很像在世界互联网上部署了一个天然的负载均衡设施啊!是的!这就是所有的 IP地址冲突导致的结果! 这就是 Anycast

其精髓就是: 在路由器看来,它们并不知道不同指向的下一跳最终将数据包导向不同的目的地,它们只是认为这只是通往同一个目的地的不同路径罢了! 简单点说, Anycast之所以得以部署和实现,就是利用了IP协议逐跳寻址的特性!

事实上,Anycast的结果是,相同的IP地址位于不同的主机,因此,它的弊端也是显而易见的。

由于 逐跳的路由收敛端到端的五元组连接 之间并没有同步,因此Anycast并不适合基于端到端连接的TCP应用。

TCP并没有广域范围的连接迁移机制,因此如果路由重新收敛,将会导致连接断开!比如上述的例子,如果A到B之间的线路拥塞或者说断开,那么路由会重新收敛到A-R-R1-P这条路径,A和B的1.1.1.0/24子网的主机之间的TCP连接将会断开,这并不是人们所期望的。

因此,上述描述中的Anycast只适合于一来一回两个包的oneshot式的交互通信,比如DNS!

是的,DNS就是这么部署的,比如我们经常使用的Google DNS 8.8.8.8,它实际上就是Anycast部署在世界不同地方的多台主机,地址全部都是8.8.8.8!

请使劲阅读:https://developers.google.com/speed/public-dns/faq

然后去深撸这个站点,溯源:https://bgp.he.net

至此,我觉得关于Anycast的概念,已经大致陈述清楚了。

下面是IPv6的世界。我们跟踪问题本身以及其解法,跟着RFC来梳理IPv6 Anycast的来龙去脉,其实一切都很清晰。


IPv6的Anycast

重看Anycast在IPv4上的问题,我们知道, 把同一个IP地址配置在不同的主机上,这确实是不妥的,比如占据互联网流量头把交椅的TCP应用就不适合, 既然无法让主子心安理得的承认,那索性在Anycast标准化中就不要这么做就是了。

但是,只要Anycast不是部署在端节点,而是部署在路径节点,比如路由器上,那就是妥妥的。逐跳寻址原则最终导致Anycast部署在路由器上之后,会自然而然地实现ECMP,即多条路径分担同样的端到端通信。

在继续下去之前,这里先说一个观点。那就是 端到端通信多路分担这种机制对TCP是不好的!

又TMD的是TCP,是的,这里,我觉得这不是逐跳寻址的问题,这根本就是TCP的缺陷!缺陷!缺陷! TCP要求在协议层面而不是应用层面按序到达,这就要求所有的字节最好是一路上顺序地排队前进,而多路径会影响顺序同步性,导致乱序。TCP的字节按序到达这个约束会恶化流量高峰期的链路拥塞。 由此而得到的另一个Trick就是Flowlet!它大大增加了端到端拥塞控制的复杂度。 然而我们看看QUIC,它就并没有要求严格的字节按序到达,而是基于窗口的按序到达,这就使得QUIC可以大大利用多路径分担带来的收益。 其实对于TCP而言,不光是链路最好不要多路径,甚至在路由器,交换机这种中间节点,多CPU,多处理卡也不能负载均衡分担同一条TCP流的不同数据包! 有了TCP,几乎所有的负载均衡都得按照流的粒度进行。 TCP真是太恶心了!

好了,我们先不管TCP了,任它腐烂吧!


IPv6对Anycast进行了标准化,首先在RFC3513中,它对Anycast提出了两点约束:

o An anycast address must not be used as the source address of an IPv6 packet. o An anycast address must not be assigned to an IPv6 host, that is, it may be assigned to an IPv6 router only.

有了这两点约束,我们可以知道: 在IPv6中,Anycast不是用来通信的,而是用来寻址的。

紧接着,RFC3513要求 所有的路由器的所有接口 都必须配置一个 必选的Anycast地址:

2.6.1 Required Anycast Address The Subnet-Router anycast address is predefined. Its format is as follows: | n bits | 128-n bits | ±———————————————–±————— | subnet prefix | 00000000000000 | ±———————————————–±————— The “subnet prefix” in an anycast address is the prefix which identifies a specific link. This anycast address is syntactically the same as a unicast address for an interface on the link with the interface identifier set to zero. Packets sent to the Subnet-Router anycast address will be delivered to one router on the subnet. All routers are required to support the Subnet-Router anycast addresses for the subnets to which they have interfaces.

比方说,路由R有两个接口,分别配置了两个IP地址:

代码语言:javascript复制
e0:  240e:909:2001::4e3/64
e1:  240e:101:4004::111/64

那么根据RFC的要求,这个路由器上将会生成下面的Anycast地址:

代码语言:javascript复制
e0 Anycast:  240e:909:2001::/64
e1 Anycast:  240e:101:4004::/64

为了使得这些Anycast能被访问到,需要添加两条本地主机路由:

代码语言:javascript复制
Local 240e:909:2001::/128 dev loopback
Local 240e:101:4004::/128 dev loopback

这里需要解释一点,刚才不是说IPv6的Anycast不是用来通信的吗?那 为什么还要被访问呢?

因为需要邻居解析(IPv6 Ndp)。如果有谁把这个地址设置为下一跳了,那么需要解析这个地址,这就是 被访问!

纳尼?Anycast作为下一跳?

是的,这就是IPv6 Anycast的核心用法,它不是用来标识主机服务让你的应用程序通信,它是用来寻址的:

  • Anycast地址被设置为下一跳
  • Anycast地址被设置进IPv6路由扩展头以支持源路由

来吧,我们还是举例子的好。

我的局域网为了备份,希望部署两台或者多台路由器做热备,姑且就先两台吧:

假设两台路由器都能接外网,如果跑IPv4协议,那么自然而然的想法就是安装keepalived跑VRRP,这种成熟的东西,想必都可以瞬间完成配置。

但是,我们知道,这两台路由器只有一台是工作状态,另外一台处于backup standby,是不是感觉浪费了资源?如果你想让它们一起工作,那就要:

  1. 为它们配置不同的IP地址,gw1,gw2;
  2. 内部局域网一半一半分别配置两个不同的gateway,即gw1,gw2。

万万不能配置成相同的IPv4地址的,因为这种地址冲突会导致交换机的转发表以及ARP表的混乱。当然,对于我个人而言,我是有办法将其配置成相同的IP地址又不会confuse各种表的,但是,配置太复杂了(涉及iptables,ebtables,arptables,arp,iproute2,STP等等)。正如IPv4的Anycast一样,没有什么是IPv4配置不出来的,只是这些大部分都是奇技淫巧般的Trick!玩物丧志!

我们看一下用IPv6会怎样。

不用干别的,如果路由器的实现遵循RFC标准,那么你只需要配置两台路由器不同的同网段地址即可,如下:

代码语言:javascript复制
路由器1 e0: 240e:110:1001::fbb2:0a1e:0e59:15eb/64  [低64bit为EUI-64映射而来]
路由器2 e0: 240e:110:1001::eb7c:b2da:7088:3c38/64  [低64bit为EUI-64映射而来]

然后所有主机的默认网关设置成其Anycast地址即可:

代码语言:javascript复制
::/0 nexthop 240e:110:1001::

就这么简单!Why?

因为路由器1和路由器2在配置好内网e0的地址后,根据RFC3513,它们会自动生成Anycast地址以及Anycast地址的主机路由以被内网主机邻居解析。

RFC要求各层寻址Anycast地址时以 本层次的路由距离 来度量,那么对于二层链路,其距离默认就是物理距离,光速不变,距离等效于时间,因此会等效为 谁先回复我的邻居请求,谁就是我要寻址的Anycast节点!

是不是天然的负载均衡了呢?炫酷吧!

现在有个不可回避的问题需要解决,那就是 访问互联网服务的正向包和返回包路径不对称问题。

比如,h1主机访问服务器S走的R1作为默认网关,然而S返回h1时,却从R2返回,此时R2解析h1的地址,h1收到R2的解析请求后,会不会更新自己关于Anycast邻居的信息呢?如果是的话,那势必会造成邻居表信息的剧烈抖动啊!

答案是 并不会! Why?

因为IPv6在解析邻居时,ICMPv6协议头里会写清楚下面的信息:

  • 自己是不是路由器
  • 邻居信息需不需要覆盖

这一点和IPv4的ARP不同,ARP是双向更新的,在回复自己的MAC地址时,同时也更新了自己的ARP表,但在IPv6中,两者分开了:

  • 请求你的MAC信息
  • 请更新你自己的邻居信息

R2发送给h1的邻居请求,只是请求h1的MAC地址而已,并没有说要h1更新其邻居信息,所以万事大吉:


配置和实现

现在该看看实现了。

很少有资料讲 如何在Linux上配置IPv6的Anycast 的,这一次可能我又占了坑。

其实很简单,只要开启IPv6的转发即可:

代码语言:javascript复制
sysctl -w net.ipv6.conf.all.forwarding=1

这个时候,你就会在/proc/net/anycast6看到内容:

代码语言:javascript复制
[root@localhost src]# cat /proc/net/anycast6
3    enp0s8          fe800000000000000000000000000000     1
4    enp0s9          fe800000000000000000000000000000     1
4    enp0s9          240e0918800300000000000000000000     1

我在enp0s9上配置了如下的IPv6地址:

代码语言:javascript复制
[root@localhost src]# ip -6 addr ls dev enp0s9
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qlen 1000
    inet6 240e:918:8003::3f5/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::eb7c:b2da:7088:3c38/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

所以说,什么都不用干,Linux内核自动帮我生成了其对应的Anycast地址,对应RFC3513的2.6.1 Required Anycast Address格式:240e:918:8003::

按照上面一个小节最后的例子,我们知道,这个 240e:918:8003:: 是可以被邻居发现而解析的,而我们知道,IPv6的邻居发现使用的是组播地址,其组播构成规则详见: RFC3513-2.7 Multicast Addresses: https://tools.ietf.org/html/rfc3513#section-2.7

对应组播地址: FF02::1:FF00:0000/104 Solicited-Node Address [RFC3513] -RFC4291对其进行了增强更新

Solicited-Node Address: FF02:0:0:0:0:1:FFXX:XXXX Solicited-node multicast address are computed as a function of a node’s unicast and anycast addresses. A solicited-node multicast address is formed by taking the low-order 24 bits of an address (unicast or anycast) and appending those bits to the prefix FF02:0:0:0:0:1:FF00::/104 resulting in a multicast address in the range FF02:0:0:0:0:1:FF00:0000 to FF02:0:0:0:0:1:FFFF:FFFF

我们针对Linux的如上配置确认一下:

代码语言:javascript复制
[root@localhost src]# cat /proc/net/igmp6
1    lo              ff020000000000000000000000000001     1 0000000C 0
1    lo              ff010000000000000000000000000001     1 00000008 0
...
# 下面这个便是!
4    enp0s9          ff0200000000000000000001ff000000     2 00000004 0
...

将Anycast地址作为默认网关发送数据,最终邻居解析的时候,只要发送到组播地址 ff02::1:FF00:: 就可以解析出该网段上的Anycast地址的MAC地址信息,然后 取第一个到达的作为邻居 即可!

上面关于组播的设置,请看 addrconf_join_anycast 函数:

代码语言:javascript复制
static void addrconf_join_anycast(struct inet6_ifaddr *ifp)
{ 
   
    struct in6_addr addr;

    if (ifp->prefix_len >= 127) /* RFC 6164 */
        return;
    ipv6_addr_prefix(&addr, &ifp->addr, ifp->prefix_len);
    if (ipv6_addr_any(&addr))
        return;
    ipv6_dev_ac_inc(ifp->idev->dev, &addr);
}

其中,ipv6_dev_ac_inc 值得观摩!


配置也配好了,那么我们找两台机器练一练手吧。

这次我部署的另外一个机器是Windows 7系统,顺便玩一下netsh。这台Win7系统机器和我们的Linux Rh7.2直连,拓扑我就不画了,非常简单。我只是把Win7上的地址配置发布出来。

很简单,Win7上配置一个 240e:918:8003::/64* 同网段的IPv6地址:

这个时候,将Win7的默认网关设置成 240e:918:8003::* 这个Linux上使能的Anycast地址,看看如何通信。

按照惯例,ping一下这个 240e:918:8003::* 地址:

不通!

在Linux上抓包,发现是有回复ICMPv6 Echo Reply的,只是说回复的源IP地址不是Win7期望的Anycast地址,而是Linux上enp0s9网卡的地址,这正是印证了 An anycast address must not be used as the source address of an IPv6 packet. 这句话!

我很好奇Linux内核是怎么做到 不让Anycast地址作为源地址的 ,ping不行,TCP的Telnet也不行…其实看一下代码就完全明白了。

先看下Telnet为什么完全就没有SYN-ACK回复:

再看看为什么ping回复的时候用的不是Anycast地址,而是选择了网卡上配置的地址:

这个关于源地址选择的细节,详见RFC3484以及我的前一篇闲谈: Default Address Selection for Internet Protocol version 6 (IPv6) :https://www.ietf.org/rfc/rfc3484.txt 闲谈IPv6-源IP地址的选择(RFC3484读后感) :https://blog.csdn.net/dog250/article/details/87815123


脉络理清了之后,我们来反一下,让Win7主机配置Anycast。

我特意没有按照RFC的规定,去配置了一个非标准的Required Anycast Address,且看:

配置方法如下:

我并没有让低n-bit为全0,竟然成功了,这说明Win7并没有严格按照RFC的规范行事,它完全是手动的。

那么好,我在Linux上去ping这个Win7的Anycast地址:

得到了Win7的回复,然而源地址不是Win7的Anycast地址,却是Win7的物理网卡 本地连接 3 上配置的IPv6地址。这依然印证了 An anycast address must not be used as the source address of an IPv6 packet. 这句话。

只是说,Win7对Anycast地址 并没有严格遵循subnet后面的低bits均为0的约束 ,即Win7没有实现 严格的Required Anycast Address!

不管怎么样,痛则不通,通则不痛,后面也没啥可玩的了。


总结

IPv6的Anycast:

  • 以往IPv4的规则在IPv6 RFC约束下依然适用
  • 独添了subnet-Anycast,见上文

因为我们可以将Anycast总结为:

  1. 广义的Global Anycast-作用于IPv4/IPv6
  2. 狭义的IPv6 subnet Anycast-作用于IPv6

此外,RFC2526又规定了 保留的Anycast地址 用于不同的目的: RFC2526-Reserved IPv6 Subnet Anycast Addresses :https://tools.ietf.org/html/rfc2526 本文不涉及RFC2526的内容,但是提醒注意,仅此而已。

阅读笔记

为了写这篇文章,我深夜快速浏览了RFC标准,因为我要在这些个标准之上才能胡扯形而上吧。

这两个截图体现了IPv6 Anycast的作用,它到底用在哪?我想我在上文呢,已经阐述清楚了。


后记

为Anycast添加一条host路由,让anycast地址可以被设置成网关。

昨晚睡的有点早,这周末又要去公司附近拉一车行李,有点忙。白天基本没空,所以就以安安吃奶闹铃为由,作文一篇。

浙江温州皮鞋湿,下雨进水不会胖。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/142198.html原文链接:https://javaforall.cn

0 人点赞