Traceroute 的实现原理

traceroute 是一个很常用的工具,用来检查当前设备到目的 IP 地址的路径以及每个中间设备产生的延迟。如下图所示:

traceroute to baidu.com (110.242.68.66), 64 hops max, 52 byte packets
 1  10.43.244.2 (10.43.244.2)  4.132 ms  2.294 ms  2.683 ms
 2  10.41.0.217 (10.41.0.217)  3.178 ms  1.846 ms  1.686 ms
 3  10.40.0.54 (10.40.0.54)  2.554 ms  2.033 ms  2.174 ms
 4  10.42.0.69 (10.42.0.69)  4.058 ms  2.905 ms  2.957 ms
 5  10.42.0.54 (10.42.0.54)  3.304 ms  3.058 ms *
 6  10.42.0.20 (10.42.0.20)  3.205 ms  3.199 ms  3.086 ms
 7  14.17.22.130 (14.17.22.130)  4.497 ms  3.424 ms  3.195 ms
 8  10.162.89.97 (10.162.89.97)  10.903 ms  4.797 ms  4.136 ms
 9  10.200.52.57 (10.200.52.57)  5.684 ms
    10.200.52.65 (10.200.52.65)  5.288 ms
    10.200.52.73 (10.200.52.73)  7.327 ms
10  * * *
11  * * *

在 mac/linux/windows 下都有类似的工具。因为在 https://github.com/joyme123/gnt 中实现了 traceroute 的能力,这里记录一下。

traceroute 的实现中,利用了一些基本的网络协议的特性:

  • IP (网络层)数据包在网络中传输时,可以通过 TTL 字段来控制这个数据包的生命周期。每经过一个三层设备的转发,这个 TTL 都会减少1,当 TTL 变为 0 时,三层设备就会丢弃这个数据包而不是继续转发它。这样的设计可以避免网络链路形成环时,数据包会被无限的转发。
  • 中间的三层设备在丢弃数据包时,会使用 ICMP 协议,向数据包的源发送方(通过 src ip)发送 ICMP 包,来告知数据包因为 TTL 为 0 而被丢弃。当然有的三层设备不会发送这个 ICMP 消息,所以 traceroute 时部分中间环节会显示 *。并且这种 ICMP 包的 payload 都会包含源数据包的二层和三层 header。
  • traceroute 支持多种协议: TCP、UDP 和 ICMP。当 TCP, UDP 的包到达目标设备后,如果 TCP, UDP 的目的端口不能访问,那么目标设备也会通过 ICMP 消息向源设备告知该端口不可达。如果是使用 ICMP echo request,那么目标设备会返回 ICMP echo reply 向源设备告知收到了 echo request。

有了以上的网络协议特性,traceroute 的实现可行性就有了。以使用 TCP 协议为例,具体的步骤如下:

  1. traceroute 构造 TCP 数据,源端口根据当前进程 ID 生成,目的端口选择一个不太可能使用的端口,比如 33434。
    1. 源端口根据当前进程 ID 生成,这样收到中间网络设备的 ICMP 包时,就可以通过 payload 中携带的源数据包信息判断出这个 ICMP 是响应哪个进程的。
    2. 目的端口选择一个不常用端口,是防止对目的端的 TCP 服务产生影响。并且这个目的端口每次请求都会加1。
  2. traceroute 构建 IP 头,TTL 一开始设置为 1。这样第一个中间设备收到后,就会丢弃这个包,并返回 ICMP 包了。后续 TTL 逐渐加 1,就可以探测到每一个中间设备了。
  3. 当 traceroute 收到 ICMP 包时,先根据源端口判断这个包属不属于当前进程,再根据目的端口判断这个包是第几个发出的。比如目的端口是 33436,那么已知起始目的端口是 33034 的情况下,就知道这个包是第三个发出的。这样根据第三个包发出的时间,就知道延迟情况了。
    1. 如果这个 ICMP 包是 ttl exceeded,说明中间网络设备返回的。
    2. 如果这个 ICMP 包是 destination unreachable,说明是目的设备返回的。

Ping 与 ICMP 协议

概述

ping 命令是一个非常常用的网络工具,通过 ICMP 协议来探测本地到远端地址之间网络的连通性,以及延迟,稳定等性能指标。但是大多数人其实对 ping 命令的实现了解的并不会太多,因为我们日常的开发工作中,很少会和 ICMP 协议打交道。因为最近在开发 https://github.com/joyme123/gnt,目标是通过单个二进制文件,实现大多数的网络工具的能力。所以接触了一些 ping 命令的实现,这里做一些简单的记录和分享。

ping 命令基于 ICMP 协议的实现

ICMP 协议本身这里不多做介绍,网络上有很多很好很详细的资料。比如维基百科上的这篇介绍:Internet_Control_Message_Protocol

ICMP 和 TCP/UDP 这样的协议有这很大的区别,像 TCP/UDP 这种传输层的协议,都是进程级别的,即可以通过 Port 对应到一个或多个进程。因此在运行使用 TCP/UDP 协议的应用时不需要特殊的权限。而 ICMP 则不一样,它是操作系统级别的,因此早期的 linux 上,如果应用要发送 ICMP 包,则必须通过 socket(AF_INET, SOCK_RAW, int protocol) 这种方式来实现,而调用 SOCK_RAW 则需要 root 权限,或者通过 linux network capability。

因为这种特殊性,后来的 linux 又提供了 socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) 这种方式去发送 ICMP,这里的 SOCK_DGRAM 是 UDP 协议使用的。不过容易误解的是,并不是说用 UDP 协议去包装或实现了 ICMP 的能力,使用这种方式发送出去的仍然是 ICMP 数据包,不过不再需要 root 权限或者特殊的 network capability 设置了。通常称这种方式为 Unprivileged ICMP。linux 同样也提供了 sysctl 的配置去限制这一能力的使用

# 999~59999 指定了允许的用户组 ID 范围,如果所有用户组都不允许可以设置为 1 0
net.ipv4.ping_group_range = 999 59999

不过需要注意的是,通过 SOCK_DGRAM 只能发送这几种 ICMP 请求:ICMP_ECHO, ICMP_TSTAMP or ICMP_MASKREQ.

解决了如何发送 icmp 包的问题,就可以考虑实现几个主要的 ping 命令特性了。

丢包检测

每个 ICMP 包都有一个 sequence 字段,发送的时候可以指定这个 sequence 的值,目的端响应的时候会把这个 sequence 值设置成一样的,表示响应的是哪一个请求包。这样我们就可以知道每个发送出去的 ICMP 的响应包了,那么没有响应的 sequence 就是被丢弃的包。通过这种方式就可以检测出网络中使用存在丢包现象。

延迟检测

ICMP 支持 echo request 和 reply,即通过 ICMP 协议包装的 payload 发送出去,目的端会原样返回。所以我们可以通过在 request 的时候写入发送时的时间,然后收到回包时取出这个时间就能知道延迟了。

关于写入的时间格式,实现方式上可以随意。但是建议使用 unix time,精确到微秒。然后保留 4 字节的秒+4字节的微秒,写入到 payload 的开头。这样的实现方式和 linux ping 的实现一致,像 wireshark 这种抓包工具就可以识别出来了。

OpenFlow 的流表匹配规则

OpenFlow 里有一个重要的概念—流表(FlowTable),通过 Flow Table,我们可以制定交换机处理流量的行为。因此,理解流表匹配规则,是理解 OpenFlow 的重要一环。

OpenvSwitch(下文称为OVS) 是一个开源的软件交换机的实现,同时也是支持 OpenFlow 的,因此下文也会通过 OVS 来说明流表是如何匹配的。

流表匹配规则

流表项

在一个 OpenFlow 的网络中,每一个支持 OpenFlow 的交换机都必须包含至少 1 个流表(table 0),这个流表里会包含 0 或多个流表项。这些流表项描述了流量的匹配规则,计数器以及如果针对这些流量做出动作。

一个流表项通常由以下元素组成

字段 描述
Match Fileds 用来匹配数据包
Priority 匹配流表项的优先级。如果一个数据包被多个流表项匹配到,则会根据优先级进行选择
Counters 当数据包被匹配成功时会更新该字段,主要用来流量统计
Instructions 用来修改 Actions 或 流水线处理
Timeouts 流表项的超时时间
Cookie 一些不透明的数据值。控制器可以使用它来过滤流量统计,流量修改和流量删除。在处理数据包时不使用这些数据值

流水线处理

流表在处理时,就像流水线一下,每个 table 都是一个处理阶段。在每一个 table 里,数据包都可能被:

  • 丢弃。
  • 转发给下一个 table
  • 发送给 controller

https://www.myway5.com/wp-content/uploads/2023/01/openflow-flow.webp

如上图所示,数据包通过 port 进入交换机后,首先会匹配 table 0 中的流表项。如果流表项匹配到了,则会根据该流表项设定的 actions 进行操作。如果未匹配到,则会被丢弃。

数据包在不同的 table 中流转时,只能按照升序进行,也就是说可以从 table 0 → table10,但是不能从 table10 → table0。并且需要注意的是,在流表之间跳转需要使用 goto_table 或 resubmit 语句来将数据包 copy 一份转发到其他的流表中处理。如果没有指定跳转动作,是不会继续在其他 table 中进行匹配的。

https://www.myway5.com/wp-content/uploads/2023/01/openflow-flow-det.webp

OVS 操作演示

为了加深对 OpenFlow 的理解,可以使用 OVS 提供的一个例子进行练习。这个例子中,使用 OVS 的流表实现了交换机二层 Mac 地址的学习,以及 VLAN 的支持。

环境准备

和 ovs 官网不同的是,这里我使用 mininet 快速创建一个实验环境。

sudo mn --topo=single,4 --mac --controller=none

这里创建了一个名为 s1 的交换机和 4 台连接到交换机上的主机 h1, h2, h3, h4,通过 port1, port2, port3, port4 连接。port1 trunk 所有的 vlan,port2 access vlan 20, port3, port3 access vlan 30。

https://www.myway5.com/wp-content/uploads/2023/01/ovs.drawio-1.png

table 0: 准入控制

table 0 是数据包进入交换机的起始位置。我们在这一步去禁止某些数据包的进入。比如:以多播源地址的数据包是非法的,所以我们可以在这里丢弃掉。

sh ovs-ofctl add-flow s1 "table=0, dl_src=01:00:00:00:00:00/01:00:00:00:00:00, actions=drop"

交换机也不应该转发 IEEE 802.1D Spanning Tree Protocol(STP) 数据包,所以我们在可以通过流表项来丢弃掉。

sh ovs-ofctl add-flow s1 "table=0, dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, actions=drop"

对于其他合法的数据包,我们都提交到流水线的下一阶段(table1) 中去处理。这里我们使用最低的优先级来做兜底

sh ovs-ofctl add-flow s1 "table=0, priority=0, actions=resubmit(,1)"

测试 table 0

这里因为有一些数据包不方便构造去做实际的测试,所以使用 ofproto/trace 去模拟数据库的匹配。

例子1

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=01:80:c2:00:00:05

会出现以下输出

Flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=01:80:c2:00:00:05,dl_type=0x0000

bridge("s1")
------------
 0. dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, priority 32768
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

可以看到以下关键信息:

  1. 数据包匹配到了 dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, priority 32768 ,根据 action 需要被 drop 掉
  2. Final flow 为 unchanged ,说明数据包本身没有被修改。

例子2

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=01:80:c2:00:00:10

出现以下输出

Flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=01:80:c2:00:00:10,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=01:80:c2:00:00:10/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

可以看出数据包被 resubmit 到 table 1 了,但是因为 table 1 中没有任何流表项,所以被 drop 了。

table 1: VLAN 进入数据包处理

进入 table 1 的数据包,都是在 table 0 中被校验有效的数据包。table 1 被设计用来校验数据包的 VLAN,这个校验是基于数据包进入交换机经过的 Port 配置。同时我们也会在这些进入 access port 的数据包 header 里添加 VLAN tag,来保证后续的处理都可以基于这些 VLAN tag 进行。

首先我们添加一个默认 drop 的规则

sh ovs-ofctl add-flow s1 "table=1, priority=0, actions=drop"

对于 trunk port 1,可以接收任意数据包,无论数据包是否有 VLAN header 或者这个 VLAN tag 是多少。所以可以添加一个流表项,将进入 port1 的所有数据包 resubmit 到 table 2 中。

sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=1, actions=resubmit(,2)"

在 access port 上,只接收没有 VLAN header 的数据包,然后对数据包打上 VLAN tag,然后再提交到下一阶段(table2) 去处理。

sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=2, vlan_tci=0, actions=mod_vlan_vid:20, resubmit(,2)"
sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=3, vlan_tci=0, actions=mod_vlan_vid:30, resubmit(,2)"
sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=4, vlan_tci=0, actions=mod_vlan_vid:30, resubmit(,2)"

测试 table 1

例子1:数据包进入 trunk port

数据包进入 trunk port 1

sh ovs-appctl ofproto/trace s1 in_port=1,vlan_tci=5

得到以下输出,数据包在 table 0 中 resubmit 到 table 1,再到 table 2 后没有规则,被默认丢弃

Flow: in_port=1,vlan_tci=0x0005,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

例子2:有效的数据包进入 access port

一个没有 802.1Q header 的数据包进入 port 2

sh ovs-appctl ofproto/trace s1 in_port=2

得到以下输出,数据包在 table 0 中 resubmit 到 table 1,再到 table 2 后没有规则,被默认丢弃i

Flow: in_port=2,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=2,vlan_tci=0x0000, priority 99
    mod_vlan_vid:20
    resubmit(,2)
 2. No match.
    drop

Final flow: in_port=2,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=2,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

例子3: 无效的数据包进入 access port

一个有 802.1Q header 的数据包进入 port 2

sh ovs-appctl ofproto/trace s1 in_port=2,vlan_tci=5

得到以下输出,在 table 1 中被 drop 了。

Flow: in_port=2,vlan_tci=0x0005,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. priority 0
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=2,vlan_tci=0x0005,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

table 2: 进入 port 后学习 MAC+VLAN

table 2允许我们实现的交换机学习数据包的 source mac。只需要一个流表项

sh ovs-ofctl add-flow s1 "table=2 actions=learn(table=10, NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[], load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15]),resubmit(,3)"

learn 这个 action 会基于正在处理的数据包,动态的修改流表。对于 learn 的几个字段说明如下:

table=10

    Modify flow table 10.  This will be the MAC learning table.

NXM_OF_VLAN_TCI[0..11]

    Make the flow that we add to flow table 10 match the same VLAN
    ID that the packet we're currently processing contains.  This
    effectively scopes the MAC learning entry to a single VLAN,
    which is the ordinary behavior for a VLAN-aware switch.

NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[]

    Make the flow that we add to flow table 10 match, as Ethernet
    destination, the Ethernet source address of the packet we're
    currently processing.

load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15]

    Whereas the preceding parts specify fields for the new flow to
    match, this specifies an action for the flow to take when it
    matches.  The action is for the flow to load the ingress port
    number of the current packet into register 0 (a special field
    that is an Open vSwitch extension to OpenFlow).

测试 table 2

例子1

sh ovs-appctl ofproto/trace s1 in_port=1,vlan_tci=20,dl_src=50:00:00:00:00:01 -generate

得到以下输出。上面的命令中使用了 -generate,是为了让数据包真实的在 OVS 中生效,不指定的话,OVS 不会真实的生成 table 10 中的流表项。

Flow: in_port=1,vlan_tci=0x0014,vlan_tci1=0x0000,dl_src=50:00:00:00:00:01,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 priority=32768 actions=load:0x1->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,vlan_tci=0x0014/0x1fff,dl_src=50:00:00:00:00:01,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

在 table 10 中确认是否有新的流表项生成

sh ovs-ofctl dump-flows s1 table=10

cookie=0x0, duration=142.170s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 actions=load:0x1->NXM_NX_REG0[0..15]

例子2

sh ovs-appctl ofproto/trace s1 in_port=2,dl_src=50:00:00:00:00:01 -generate

cookie=0x0, duration=193.493s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 actions=load:0x2->NXM_NX_REG0[0..15]

可以看到,在例子 1 中 table 10 中,在 port 1 上学习到了 mac 地址 50:00:00:00:00:01,现在 port 2 中也出现了该 mac 地址,所以这个 mac 地址更新到了 port 2 上。

table 3: 查找目标 Port

这个 table 实现了如何通过 MAC 和 VLAN 查找到目标的 output port。通过以下流表项来实现查找

sh ovs-ofctl add-flow s1 "table=3 priority=50 actions=resubmit(,10), resubmit(,4)"

这个流表项首先将数据包提交到 table 10 中。table 10 中存储了学习到的 mac 地址。如果这个 mac 地址没有被学习过,则 table 10 中不会被匹配。那么就会被 resubmit 到 table 4 中继续处理。

测试 table 3

下面的命令会让 OVS 学习到 port 1上 VLAN 20 的 mac 地址 f0:00:00:00:00:01

sh ovs-appctl ofproto/trace s1 in_port=1,dl_vlan=20,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01 -generate

得到以下输出,数据包在 table 10 中没有匹配到,所以被 resubmit 到 table 4.

Flow: in_port=1,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01 priority=32768 actions=load:0x1->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=20,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01,dl_type=0x0000
Datapath actions: drop

可以通过以下两种方式验证 port 1 上学习到的 mac 地址:

方法一:

sh ovs-ofctl dump-flows s1 table=10

cookie=0x0, duration=107.451s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01 actions=load:0x1->NXM_NX_REG0[0..15]

方法二:

sh ovs-appctl ofproto/trace s1 in_port=2,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01 -generate

Flow: in_port=2,vlan_tci=0x0000,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=2,vlan_tci=0x0000, priority 99
    mod_vlan_vid:20
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x0014/0x0fff,dl_dst=90:00:00:00:00:01 priority=32768 actions=load:0x2->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01, priority 32768
            load:0x1->NXM_NX_REG0[0..15]
    resubmit(,4)
 4. No match.
    drop

Final flow: reg0=0x1,in_port=2,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=2,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000
Datapath actions: drop

可以看到在 table 10 中匹配到了流表项,学习到了 load:0x1->NXM_NX_REG0[0..15]

table 4: 数据包输出处理

在 table 4 中,我们知道 register 0 包含了需要的 output port,如果该 output port 是 0,则说明需要将数据包 flood。我们也知道数据包的 VLAN 在它的 802.1Q header 上。

sh ovs-ofctl add-flow s1 "table=4 reg0=1 actions=1"

对于要 output 的 port,还需要把 VLAN header 移除掉。

sh ovs-ofctl add-flow s1 "table=4 reg0=2 actions=strip_vlan,2"
sh ovs-ofctl add-flow s1 "table=4 reg0=3 actions=strip_vlan,3"
sh ovs-ofctl add-flow s1 "table=4 reg0=4 actions=strip_vlan,4"

flood 广播或多播包

sh ovs-ofctl add-flow s1 "table=4 reg0=0 priority=99 dl_vlan=20 actions=1,strip_vlan,2"
sh ovs-ofctl add-flow s1 "table=4 reg0=0 priority=99 dl_vlan=30 actions=1,strip_vlan,3,4"
sh ovs-ofctl add-flow s1 "table=4 reg0=0 priority=50            actions=1"

测试 table 4

例子1:广播,多播以及未知目的地址

测试在 port 1 上进入广播包,VLAN 是 30

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff,dl_vlan=30

Flow: in_port=1,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     >> suppressing side effects, so learn action ignored
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. reg0=0,dl_vlan=30, priority 99
    output:1
     >> skipping output to input port
    strip_vlan
    output:3
    output:4

Final flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=30,dl_vlan_pcp=0,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000
Datapath actions: pop_vlan,4,3

可以看到 Datapath actions: pop_vlan,4,3,最终数据包被移除 vlan,从 port3,4出去了。

而下面的广播包都会被 drop,因为 VLAN 必须属于 input port

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff
sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff,dl_vlan=55

例子2: MAC 学习

VLAN 30, port 1 学习 MAC 地址:

sh ovs-appctl ofproto/trace s1 in_port=1,dl_vlan=30,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01 -generate

Flow: in_port=1,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x001e/0x0fff,dl_dst=10:00:00:00:00:01 priority=32768 actions=load:0x1->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. reg0=0,dl_vlan=30, priority 99
    output:1
     >> skipping output to input port
    strip_vlan
    output:3
    output:4

Final flow: in_port=1,vlan_tci=0x0000,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=30,dl_vlan_pcp=0,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000
Datapath actions: pop_vlan,4,3

因为目的地址是未知的,所以数据包被 flood 到 port3,port4 上。然后我们再次测试 MAC 地址是否学习到了。

sh ovs-appctl ofproto/trace s1 in_port=4,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01 -generate

Flow: in_port=4,vlan_tci=0x0000,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=4,vlan_tci=0x0000, priority 99
    mod_vlan_vid:30
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x001e/0x0fff,dl_dst=20:00:00:00:00:01 priority=32768 actions=load:0x4->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. vlan_tci=0x001e/0x0fff,dl_dst=10:00:00:00:00:01, priority 32768
            load:0x1->NXM_NX_REG0[0..15]
    resubmit(,4)
 4. reg0=0x1, priority 32768
    output:1

Final flow: reg0=0x1,in_port=4,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=4,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000
Datapath actions: push_vlan(vid=30,pcp=0),2

linux 网络问题排查手册

这里主要记录一些在实际排查网络问题过程中,觉得非常好用的工具或方法。大多数和 k8s 的网络问题相关

1. iptables 处理规则流程图

出处:https://www.zsythink.net/archives/1199

https://www.myway5.com/wp-content/uploads/2022/12/screenshot-20221210-113850.png

2. 使用 xtables-monitor 追踪数据包在 iptables 规则中的流向

这种场景常用在宿主机上有比较复杂的 iptables 规则,导致在排查问题时比较难发现。比如 k8s 中 pod 访问 svc ip 不通时。

首先在数据包必经的地方添加 TRACE,比如 raw 表的 PREROUTING 就是个好地方。

iptables -t raw -A PREROUTING -p tcp --dport 5432 -j TRACE

添加 TRACE 之后,就可以追踪该数据包后续的流向了。通过 trace 的信息,可以看到数据库的流入流出网卡,mark 值,匹配的 iptables 规则等等。非常容易帮我们判断出问题

xtables-monitor --trace

3. 使用 conntrack 查看连接的生命周期

conntrack 是 linux 网络协议栈提供的能力,顾名思义,conntrack 提供的是连接追踪的能力。这里的连接并不是 tcp 的连接,而且传输层的 5 元组(源IP,源端口,目的IP,目的端口,协议)来唯一确定的连接。像 NAT 之类的功能,都是基于 conntrack 之上的。

# 比如这条命令就可以看 10.233.101.170 这个 pod 访问 svc ip 建立的连接请。
conntrack -p tcp -s 10.233.101.170 -L -o ktimestamp

4. no route to host 怎么解决

这个问题相信大多数人都遇到过,之所以要单独写在这里,是因为遇到过一些小众场景非常难排查。

大多数情况下,这个问题可以通过检查下面几项:

  1. 检查机器上的路由表,是否有通往目的 IP 的路由。
  2. 确认网关配置是否正确,网关路由是否正常。
  3. 抓包检查,这里我们可以选择抓 ARP 包。如:tcpdump -i any arp and host xx.xxx.xx.xx -ne。这种方式看似没什么用,但是非常好用,特别是在网络配置非常复杂(多网卡,多网络平面的 k8s 集群)时。有的时候很难准确判断出出去的数据包命中了什么样的路由。这种情况下,我们可以通过 arp 请求的网关 IP,来判断出路由是哪条,且网关是否正常连通。

5. 路由判断理解是否正确

通过第一点中提供的图,我们可以发现,路由判断只在两个地方发生:

  1. PREROUTING 之后
  2. OUTPUT 之前

也就是说,当数据包通过了这两个地方之后,路由就已经确定了。在这之后做任何数据包的修改都不会再影响路由判断。

实际场景:使用 ip rule 可以创建一些策略路由,比如来自于 10.100.0.0/24 的数据包,mark 值为 0x06 时走什么路由策略。当在 POSTROUTING 链上做 SNAT,set mark 的时候,是不会影响路由的。

6. iptables 与 LVS

https://www.myway5.com/wp-content/uploads/2022/12/nf-lvs.png

正常情况下,LVS nat 模式的流量不能做 SNAT,因为不会经过 POSTROUTING 链。需要增加下的配置来让 LVS 数据包经过 POSTROUTING

net.ipv4.vs.conntrack=1

iptables 在 PREROUTING 阶段做的 mark,在 LVS NAT 后仍然有效。

7. 容器网络不通解决思路

这里的容器网络仅指 docker/nerdctl 部署的容器网络,k8s 情况会更复杂,也要结合 CNI 去看,这里不讨论。

实际使用中,常见的使用方式是部署容器时使用端口(以 443 为例)映射,将宿主机的 port 映射到容器的 port。一般的底层实现都是使用 iptables DNAT,将发到 443 端口的流量 DNAT 到容器的 ip:port。这样再根据本机的路由,将流量通过 docker0/nerdctl1 这样的网桥进行转发即可。

根据以上的背景,一般我们可以检查下面几个地方:

  1. 宿主机的 ip_forward 是否打开:sysctl -a | grep ip_forward。需要确认值为 1,这时候宿主机才会将不属于自己的流量进行 FORWARD。
  2. 宿主机的 bridge-nf-call-iptables 是否打开:sysctl -a | grep bridge。需要确认值为 1,这时候 bridge 设备上的流量就会经过 iptables conntrack。
  3. 检查 DNAT 规则。docker/nerdctl 一般都会在 iptables NAT 表上写这些规则。iptables -t nat -L PREROUTING
  4. 检查 FILTER 表。实际使用中遇到过在宿主机上同时使用 docker 和 nerdctl 的用户,docker 默认将 FILTER 表 FORWARD 链改为 DROP Policy,也就是不匹配 FORWARD 下规则的流量都会丢弃。而 nerdctl 则是默认 ACCEPT。这样 nerdctl 启动的容器网络就会不通。

IPv6 学习笔记

1. 概述

IPv6 全称是 Internet Protocol Version 6,不过虽然是叫 Version 6,事实上是网络层协议的第二代标准协议。其出现主要是为了解决 IPv4 在实际应用场景中存在的一些缺陷。

与 IPv4 的优缺点对比

摘抄自:华为 IPv6 技术白皮书

问题 IPv4缺陷 IPv6优势
地址空间 IPv4 地址只有32位,因此总共可表示的地址在 43 亿左右。另外由于历史原因,IP 地址的分配也非常不均衡:美国占全球地址 空间的一半左右,而欧洲则相对匮乏;亚太地区则更加匮乏。IPv4 中用来解决地址短缺的方法有:CIDR 和 NAT。不过这两种方案都有其本身的缺点。 IPv6 有 128 位。理论上总共可以支持 43亿x43亿x43亿x43亿的地址。
报文格式 IPv4报头包含可选字段Options,内 容涉及Security、Timestamp、 Record route等,这些Options可以将 IPv4报头长度从20字节扩充到60字 节。携带这些Options的IPv4报文在 转发过程中往往需要中间路由转发 设备进行软件处理,对于性能是个 很大的消耗,因此实际中也很少使 用。 IPv6和IPv4相比,去除了IHL、 Identifier、Flag、Fragment Offset、Header Checksum、 Option、Paddiing域,只增加了流 标签域,因此IPv6报文头的处理 较IPv4更为简化,提高了处理效 率。另外,IPv6为了更好支持各 种选项处理,提出了扩展头的概 念,新增选项时不必修改现有结 构,理论上可以无限扩展,体现 了优异的灵活性。
自动配置和重新编制 由于IPv4地址只有32比特,并且地 址分配不均衡,导致在网络扩容或 重新部署时,经常需要重新分配IP 地址,因此需要能够进行自动配置 和重新编址,以减少维护工作量。 目前IPv4的自动配置和重新编址机 制主要依靠DHCP协议。 IPv6协议内置支持通过地址自动 配置方式使主机自动发现网络并 获取IPv6地址,大大提高了内部 网络的可管理性。
路由聚合 由于IPv4发展初期的分配规划问 题,造成许多IPv4地址分配不连 续,不能有效聚合路由。日益庞大 的路由表耗用大量内存,对设备成 本和转发效率产生影响,这一问题 促使设备制造商不断升级其产品, 以提高路由寻址和转发性能。 巨大的地址空间使得IPv6可以方 便的进行层次化网络部署。层次 化的网络结构可以方便的进行路 由聚合,提高了路由转发效率。
端对端安全 IPv4协议制定时并没有仔细针对安全性进行设计,因此固有的框架结构并不能支持端到端的安全。 IPv6中,网络层支持IPSec的认证 和加密,支持端到端的安全。
QoS 随着网络会议、网络电话、网络电 视迅速普及与使用,客户要求有更 好的QoS来保障这些音视频实时转 发。IPv4并没有专门的手段对QoS 进行支持。 IPv6新增了流标记域,提供QoS 保证。
支持移动特性 随着Internet的发展,移动IPv4出现 了一些问题,比如:三角路由,源 地址过滤等。 IPv6协议规定必须支持移动特 性。和移动IPv4相比,移动IPv6 使用邻居发现功能可直接实现外 地网络的发现并得到转交地址, 而不必使用外地代理。同时,利 用路由扩展头和目的地址扩展头 移动节点和对等节点之间可以直 接通信,解决了移动IPv4的三角 路由、源地址过滤问题,移动通 信处理效率更高且对应用层透 明。

IPv6 地址

表示方法

IPv6 总共有 128 位,通过分为8组,每组 16 位,由 4 个十六进制数表示。每组之间由冒号分隔。如:FC00:0000:130F:0000:0000:09C0:876A:130B。为了方便书写,提供了一些压缩后的写法:

  • 可以省略前缀0。所以这个地址还可以写成:FC00:0:130F:0:0:9C0:876A:130B
  • 地址中包含的连续两个或多个均为0的组,可以用双冒号”::”代替。所以进一步缩写成:FC00:0:130F::9C0:876A:130B。不过需要注意的是,一个 IPv6 地址中只能有一个 “::”,因为如果有多个的话,就无法辨别出每个 “::” 代表几组 0。

地址结构

类似 IPv4 的设计,一个 IPv6 地址也是由两部分组成:

  • 网络前缀:n 位,相当于 IPv4 的网络号。
  • 接口标识:128-n位,相当于 IPv4 地址中的主机号。

地址分类

IPv6 地址分为单播地址,任播地址,组播地址。相比于 IPv4,取消了广播地址,以更丰富的组播地址代替,同时增加了任播地址。

单播地址

单播地址用来表示一个节点的一个网络接口的地址。有以下几种单播地址:

类型 说明
未指定地址 ::/128。该地址表示讴歌接口或者节点还没有 IP 地址。
环回地址 ::1/128。与 IPv4 中的 127.0.0.1 作用相同
全球单播地址 类似于 IPv4 中的单播地址。由 全球路由前缀(Global routing prefix,至少48位)+子网ID(Subnet ID)+接口标识(Interface ID)组成。全球路由前缀由提供商指定给一个组织机构,因此也可以起到聚合路由的作用。
链路本地地址 链路本地地址是 IPv6 中的应用范围受限制的地址类型,只能在连接到同一本地链 路的节点之间使用。它使用了特定的本地链路前缀FE80::/10(最高10位值为 1111111010),同时将接口标识添加在后面作为地址的低64比特。当一个节点启动IPv6协议栈时,启动时节点的每个接口会自动配置一个链路本地 地址(其固定的前缀+EUI-64规则形成的接口标识)。在 IPv4 中,链路本地地址为 169.254.0.0/16
唯一本地地址 唯一本地地址是另一种应用范围受限的地址,它仅能在一个站点内使用。由于本地站点地址的废除(RFC3879),唯一本地地址被用来代替本地站点地址。唯一本地地址的作用类似于IPv4中的私网地址,任何没有申请到提供商分配的全 球单播地址的组织机构都可以使用唯一本地地址。唯一本地地址只能在本地网络内部被路由转发而不会在全球网络中被路由转发。唯一本地地址的固定前缀为FC00::/7,二进制表示为 1111 110

任播地址

任播地址一般用来表示一组节点上的接口,当数据包发往任播地址时,中间路由设备会将数据包发往最近的一个节点上的接口。所以可以看出,任播地址是被设计用来给多个主机或者节点提供相同服务时提供冗余功能和负载均衡功能的。不过目前实际应用中,任播地址只能分配给路由设备,并不能应用于主机等设备。并且任播地址不能作为 IPv6 报文的源地址。

任播地址并没有单独的地址空间,和单播地址使用相同的地址空间。

组播地址

IPv6的组播与IPv4相同,用来标识一组接口,一般这些接口属于不同的节点。一个节点 可能属于0到多个组播组。发往组播地址的报文被组播地址标识的所有接口接收。例如 组播地址FF02::1表示链路本地范围的所有节点,组播地址FF02::2表示链路本地范围的 所有路由器。

一个IPv6组播地址由前缀,标志(Flag)字段、范围(Scope)字段以及组播组ID (Global ID)4个部分组成:

  • 前缀:IPv6组播地址的前缀是FF00::/8。

  • 标志字段(Flag):长度4bit,目前只使用了最后一个比特(前三位必须置0), 当该位值为0时,表示当前的组播地址是由IANA所分配的一个永久分配地址;当 该值为1时,表示当前的组播地址是一个临时组播地址(非永久分配地址)。

  • 范围字段(Scope):长度4bit,用来限制组播数据流在网络中发送的范围,该字 段取值和含义的对应关系如图1-5所示。

  • 组播组ID(Group ID):长度112bit,用以标识组播组。目前,RFC2373并没有将 所有的112位都定义成组标识,而是建议仅使用该112位的最低32位作为组播组 ID,将剩余的80位都置0。这样每个组播组ID都映射到一个唯一的以太网组播 MAC地址(RFC2464)。

image-20220220175332438

被请求节点组播地址通过节点的单播或任播地址生成。当一个节点具有了单播或任播地址,就会对应生成一个被请求节点组播地址,并且加入这个组播组。一个单播地址或任播地址对应一个被请求节点组播地址。该地址主要用于邻居发现机制和地址重复检测功能。

IPv6中没有广播地址,也不使用ARP。但是仍然需要从IP地址解析到MAC地址的 功能。在IPv6中,这个功能通过邻居请求NS(Neighbor Solicitation)报文完成。 当一个节点需要解析某个IPv6地址对应的MAC地址时,会发送NS报文,该报文目的IP就是需要解析的IPv6地址对应的被请求节点组播地址;只有具有该组播地 址的节点会检查处理。

被请求节点组播地址由前缀FF02::1:FF00:0/104和单播地址的最后24位组成。

IPv6 报文格式

IPv6 除了在大小上做了变动,也针对 IPv4 报文格式在实际应用场景中的设计不合理之处做了优化。IPv6 报文主要由三部分组成:

  • IPv6 基本报头:8个字段,固定为 40 字节。
  • IPv6 扩展报头:扩展报头是链式结构的,理论上可无限扩展
  • 上层协议数据单元:一般由上层协议报头和它的有效载荷构成,有效载荷可以是一个 ICMPv6 报文、一个 TCP 报文或一个 UDP 报文。

一个 IPv6 的基本报头格式如下:

image-20220220193727771

这些字段的解释如下:

  • Version:版本号,长度为4bit。对于IPv6,该值为6。

  • Traffic Class:流类别,长度为8bit。等同于IPv4中的TOS字段,表示IPv6数据报的 类或优先级,主要应用于QoS。

  • Flow Label:流标签,长度为20bit。IPv6中的新增字段,用于区分实时流量,不同 的流标签+源地址可以唯一确定一条数据流,中间网络设备可以根据这些信息更加 高效率的区分数据流。

  • Payload Length:有效载荷长度,长度为16bit。有效载荷是指紧跟IPv6报头的数据 报的其它部分(即扩展报头和上层协议数据单元)。该字段只能表示最大长度为 65535字节的有效载荷。如果有效载荷的长度超过这个值,该字段会置0,而有效 载荷的长度用逐跳选项扩展报头中的超大有效载荷选项来表示。

  • Next Header:下一个报头,长度为8bit。该字段定义紧跟在IPv6报头后面的第一个 扩展报头(如果存在)的类型,或者上层协议数据单元中的协议类型。

  • Hop Limit:跳数限制,长度为8bit。该字段类似于IPv4中的Time to Live字段,它 定义了IP数据报所能经过的最大跳数。每经过一个设备,该数值减去1,当该字段 的值为0时,数据报将被丢弃。

  • Source Address:源地址,长度为128bit。表示发送方的地址。

  • Destination Address:目的地址,长度为128bit。表示接收方的地址。

通过上述描述可以知道,IPv6 的基本报头相比于 IPv4 的报头做了简化,去除了IHL、identifiers、Flags、Fragment Offset、Header Checksum、 Options、Paddiing域,只增了流标签域。这样的设计可以提升路由设备对数据的处理性能。

在IPv4中,IPv4报头包含可选字段Options,内容涉及security、Timestamp、Record route 等,这些Options可以将IPv4报头长度从20字节扩充到60字节。在转发过程中,处理携带这些Options的IPv4报文会占用设备很大的资源,因此实际中也很少使用。

IPv6将这些Options从IPv6基本报头中剥离,放到了扩展报头中,扩展报头被置于IPv6 报头和上层协议数据单元之间。一个IPv6报文可以包含0个、1个或多个扩展报头,仅 当需要设备或目的节点做某些特殊处理时,才由发送方添加一个或多个扩展头。与 IPv4不同,IPv6扩展头长度任意,不受40字节限制,这样便于日后扩充新增选项,这一特征加上选项的处理方式使得IPv6选项能得以真正的利用。但是为了提高处理选项头 和传输层协议的性能,扩展报头总是8字节长度的整数倍。

当使用多个扩展报头时,前面报头的Next Header字段指明下一个扩展报头的类型,这 样就形成了链状的报头列表。目前,RFC 2460中定义了6个IPv6扩展头:逐跳选项报头、目的选项报头、路由报头、分段报头、认证报头、封装安全净载报头。

image-20220220194519027

ICMPv6

ICMPv6(Internet Control Message Protocol for the IPv6)是IPv6的基础协议之一。

在IPv4中,Internet控制报文协议ICMP(Internet Control Message Protocol)向源节点报 告关于向目的地传输IP数据包过程中的错误和信息。它为诊断、信息和管理目的定义 了一些消息,如:目的不可达、数据包超长、超时、回应请求和回应应答等。在IPv6 中,ICMPv6除了提供ICMPv4常用的功能之外,还是其它一些功能的基础,如邻接点 发现、无状态地址配置(包括重复地址检测)、PMTU发现等。

ICMPv6的协议类型号(即IPv6报文中的Next Header字段的值)为58。

image-20220220201051653

报文中字段解释如下:

  • Type:表明消息的类型,0至127表示差错报文类型,128至255表示消息报文类型。

  • Code:表示此消息类型细分的类型。

  • Checksum:表示ICMPv6报文的校验和。

邻居发现

邻居发现协议NDP(Neighbor Discovery Protocol)是IPv6协议体系中一个重要的基础协 议。邻居发现协议替代了IPv4的ARP(Address Resolution Protocol)和ICMP路由器发现 (Router Discovery),它定义了使用ICMPv6报文实现地址解析,跟踪邻居状态,重复 地址检测,路由器发现以及重定向等功能。

地址解析

邻居发现协议 NDP(Neighbor Discovery Protocol) 是基于 ICMPv6 的一个三层协议,用来取代 IPv4 的 ARP 协议。其以太网协议类型为 0x86DD。地址解析过程中使用了两种 ICMPv6 报文:邻居请求报文 NS(Neighbor Solicitation) 和邻居通告报文 NA(Neighbor Advertisement)。

  • NS 报文:Type 字段值为 135,Code 字段值为 0,在地址解析中的作用类似于 IPv4 中的 ARP 请求报文。
  • NA 报文:Type 字段值为 136,Code 字段值为0,在地址解析中的作用类似于 IPv4 中的 ARP 响应报文。

image-20220220202534018

Host A在向Host B发送报文之前它必须要解析出Host B的链路层地址,所以首先Host A 会发送一个NS报文,其中源地址为Host A的IPv6地址,目的地址为Host B的被请求节 点组播地址,需要解析的目标IP为Host B的IPv6地址,这就表示Host A想要知道Host B 的链路层地址。同时需要指出的是,在NS报文的Options字段中还携带了Host A的链路 层地址。

当Host B接收到了NS报文之后,就会回应NA报文,其中源地址为Host B的IPv6地址, 目的地址为Host A的IPv6地址(使用NS报文中的Host A的链路层地址进行单播),Host B的链路层地址被放在Options字段中。这样就完成了一个地址解析的过程。

跟踪邻居状态

通过邻居或到达邻居的通信,会因各种原因而中断,包括硬件故障、接口卡的热插入 等。如果目的地失效,则恢复是不可能的,通信失败;如果路径失效,则恢复是可能 的。 因此节点需要维护一张邻居表,每个邻居都有相应的状态,状态之间可以迁移。

RFC2461中定义了5种邻居状态,分别是:未完成(Incomplete)、可达 (Reachable)、陈旧(Stale)、延迟(Delay)、探查(Probe)

image-20220220202903445

下面以A、B两个邻居节点之间相互通信过程中A节点的邻居状态变化为例(假设A、B 之前从未通信),说明邻居状态迁移的过程。

  1. A先发送NS报文,并生成缓存条目,此时,邻居状态为Incomplete。
  2. 若B回复NA报文,则邻居状态由Incomplete变为Reachable,否则固定时间后邻居状态由Incomplete变为Empty,即删除表项。
  3. 经过邻居可达时间,邻居状态由Reachable变为Stale,即不确定邻居节点的可达性。
  4. 如果在Reachable状态,A收到B的非请求NA报文,且报文中携带的B的链路层地址和表项中不同,则邻居状态马上变为Stale。
  5. 在STALE状态到达老化时间后进入Delay状态。
  6. 在经过一段固定时间(5秒)后,邻居状态由Delay变为Probe,其间若有NA应答, 则邻居状态由Delay变为Reachable。
  7. 在Probe状态,A每隔一定时间间隔(1秒)发送单播NS,发送固定次数(3次) 后,有应答则邻居状态变为Reachable,否则邻居状态变为Empty,即删除表项。

重复地址检测

重复地址检测DAD(Duplicate Address Detect)是在接口使用某个IPv6单播地址之前进 行的,主要是为了探测是否有其它的节点使用了该地址。尤其是在地址自动配置的时 候,进行DAD检测是很必要的。 一个IPv6单播地址在分配给一个接口之后且通过重复 地址检测之前称为试验地址(Tentative Address)。此时该接口不能使用这个试验地址 进行单播通信,但是仍然会加入两个组播组:ALL-NODES组播组和试验地址所对应的 Solicited-Node组播组。

IPv6重复地址检测技术和IPv4中的免费ARP类似:节点向试验地址所对应的Solicited- Node组播组发送NS报文。NS报文中目标地址即为该试验地址。如果收到某个其他站点 回应的NA报文,就证明该地址已被网络上使用,节点将不能使用该试验地址通讯。

image-20220220203031376

Host A的IPv6地址FC00::1为新配置地址,即FC00::1为Host A的试验地址。Host A向 FC00::1的Solicited-Node组播组发送一个以FC00::1为请求的目标地址的NS报文进行重 复地址检测,由于FC00::1并未正式指定,所以NS报文的源地址为未指定地址。当Host B收到该NS报文后,有两种处理方法:

  • 如果Host B发现FC00::1是自身的一个试验地址,则Host B放弃使用这个地址作为 接口地址,并且不会发送NA报文。

  • 如果Host B发现FC00::1是一个已经正常使用的地址,Host B会向FF02::1发送一个 NA报文,该消息中会包含FC00::1。这样,Host A收到这个消息后就会发现自身的 试验地址是重复的。Host A上该试验地址不生效,被标识为duplicated状态。

路由器发现

路由器发现功能用来发现与本地链路相连的设备,并获取与地址自动配置相关的前缀和其他配置参数。

在IPv6中,IPv6地址可以支持无状态的自动配置,即主机通过某种机制获取网络前缀信 息,然后主机自己生成地址的接口标识部分。路由器发现功能是IPv6地址自动配置功 能的基础,主要通过以下两种报文实现:

  • 路由器通告RA(Router Advertisement)报文:每台设备为了让二层网络上的主机 和设备知道自己的存在,定时都会组播发送RA报文,RA报文中会带有网络前缀 信息,及其他一些标志位信息。RA报文的Type字段值为134。

  • 路由器请求RS(Router Solicitation)报文:很多情况下主机接入网络后希望尽快 获取网络前缀进行通信,此时主机可以立刻发送RS报文,网络上的设备将回应RA 报文。RS报文的Tpye字段值为133。

重定向

当网关设备发现报文从其它网关设备转发更好,它就会发送重定向报文告知报文的发 送者,让报文发送者选择另一个网关设备。重定向报文也承载在ICMPv6报文中,其 Type字段值为137,报文中会携带更好的路径下一跳地址和需要重定向转发的报文的目 的地址等信息。

image-20220220203223504

Host A需要和Host B通信,Host A的默认网关设备是Switch A,当Host A发送报文给 Host B时报文会被送到Switch A。Switch A接收到Host A发送的报文以后会发现实际上 Host A直接发送给Switch B更好,它将发送一个重定向报文给主机A,其中报文中更好 的路径下一跳地址为Switch B,Destination Address为Host B。Host A接收到了重定向报 文之后,会在默认路由表中添加一个主机路由,以后发往Host B的报文就直接发送给 Switch B。

当设备收到一个报文后,只有在如下情况下,设备会向报文发送者发送重定向报文:

  • 报文的目的地址不是一个组播地址。
  • 报文并非通过路由转发给设备。
  • 经过路由计算后,路由的下一跳出接口是接收报文的接口。
  • 设备发现报文的最佳下一跳IP地址和报文的源IP地址处于同一网段。
  • 设备检查报文的源地址,发现自身的邻居表项中有用该地址作为全球单播地址或链路本地地址的邻居存在。

Path MTU

在IPv4中,报文如果过大,必须要分片进行发送,所以在每个节点发送报文之前,设备都会根据发送接口的最大传输单元MTU(Maximum Transmission Unit)来对报文进 行分片。但是在IPv6中,为了减少中间转发设备的处理压力,中间转发设备不对IPv6报文进行分片,报文的分片将在源节点进行。当中间转发设备的接口收到一个报文后, 如果发现报文长度比转发接口的MTU值大,则会将其丢弃;同时将转发接口的MTU值 通过ICMPv6报文的“Packet Too Big”消息发给源端主机,源端主机以该值重新发送 IPv6报文,这样带来了额外流量开销。PMTU发现协议可以动态发现整条传输路径上各 链路的MTU值,减少由于重传带来的额外流量开销。

PMTU协议是通过ICMPv6的Packet Too Big报文来完成的。首先源节点假设PMTU就是 其出接口的MTU,发出一个试探性的报文,当转发路径上存在一个小于当前假设的 PMTU时,转发设备就会向源节点发送Packet Too Big报文,并且携带自己的MTU值, 此后源节点将PMTU的假设值更改为新收到的MTU值继续发送报文。如此反复,直到 报文到达目的地之后,源节点就能知道到达目的地的PMTU了。

image-20220220203415347

整条传输路径需要通过4条链路,每条链路的MTU分别是1500、1500、1400、1300,当 源节点发送一个分片报文的时候,首先按照PMTU为1500进行分片并发送分片报文,当 到达MTU为1400的出接口时,设备返回Packet Too Big错误,同时携带MTU值为1400的 信息。源节点接收到之后会将报文重新按照PMTU为1400进行分片并再次发送一个分片 报文,当分片报文到达MTU值为1300的出接口时,同样返回Packet Too Big错误,携带 MTU值为1300的信息。之后源节点重新按照PMTU为1300进行分片并发送分片报文, 最终到达目的地,这样就找到了该路径的PMTU。

Linux IPv6

条目 ipv4 ipv6
sysctl 配置项 net.ipv4.conf net.ipv6.conf
ip 地址 通过 ip a查看时可以看到 inet 后面的就是 ipv4 地址。 通过 ip a查看时可以看到 inet6 后面的就是 ipv6 地址。一般会有多个,scope global 的是全局唯一单播地址或唯一本地地址(fc或fd开头),scope link 是链路本地地址(fe80 开头)。
抓包 tcpdump icmp/ tcpdump ip tcpdump icmp6 / tcpdump ip6
ping ping ping6 或 ping -6
traceroute6 traceroute traceroute6
邻居地址解析 arping ndisc
路由表 ip r ip -6 r
邻居地址表 ip neigh 或 arp -n ip -6 neigh
DNS 解析 dig dig -6

Kubernetes 的 IPv4/IPv6 双栈

IPv4/IPv6 双栈是由 IPv4 向 IPv6 过渡阶段的一种解决方案,双栈即一个网络接口同时拥有 IPv4 和 IPv6 的地址,这样在和远端通信时,如果远端支持 IPv6,就使用 IPv6 进行通信,否则也可以使用 IPv4 进行通信。Kubernetes 在 1.20 后开始支持双栈。当然,除了对 Kubernetes 版本有要求外,CNI 插件也必须支持双栈才行。

要在 Kubernetes 中开启双栈,需要做以下配置:

  • kube-apiserver:
    • --service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR>
  • kube-controller-manager:
    • --cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR>
    • --service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR>
    • --node-cidr-mask-size-ipv4|--node-cidr-mask-size-ipv6 对于 IPv4 默认为 /24,对于 IPv6 默认为 /64
  • kube-proxy:
    • --cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR>

IPv6 地址速查

平常接触 IPv4 地址较多,因此一眼就大概知道某个地址代表什么含义,但是 IPv6 中往往比较难分辨,这里提供一个表格供对照参考。

地址类型 IPv4 IPv6
环回地址 127.0.0.1 ::1/128
私网地址 10.0.0.0 – 10.255.255.255, 172.16.0.0 – 172.31.255.255,192.168.0.0 – 192.168.255.255 前缀FC00::/7(1111 110),范围:FC~FD。
链路本地地址 169.254.0.0/16 fe80::/10
组播地址 被请求节点组播地址由前缀FF02::1:FF00:0/104和单播地址的最后24位组成。
广播地址 广播地址使用该网络范围内的最大地址。 即主机部分的各比特位全部为 1 的地址。在网络 10.1.1.0/24 中,其广播地址是 10.1.1.255。

参考

  • 华为 《IPv6 技术白皮书》。本文大多数内容都是参考或摘抄自该白皮书。

calico IPIP 分析

概述

当集群中所有的主机都在同一个二层时,calico cni 可以仅靠路由,使得所有的 Pod 网络互通。但是纯二层的环境在很多场景下都不一定能满足,因此当主机之间仅3层互通时,就可以使用 calico IPIP(全称 IP in IP) 模式。

IP in IP 是一种 IP 隧道协议,其核心技术点就是发送方将一个 IP 数据包封装到另一个 IP 数据包之中发送,接受方收到后,从外层 IP 数据包中解析出内部的 IP 数据包进行处理。常用在 VPN 等技术中,用来打通两个内网环境。

calico IPIP 流量分析

之前的文章proxy_arp在calico中的妙用简单讲了 calico 是如何通过路由打通不同主机上的 Pod 网络的,其实这个方案有一个前提,就是不同的主机之间需要二层互通。当网络环境满足不了时,就可以通过使用路由 + IPIP 的方式来打通网络。

这里可以通过一个简单的实验来验证一下该方案。

# node.sh
ip netns add n1
ip link add veth1 type veth peer name veth2
ip link set veth2 netns n1
ip netns exec n1 ip link set veth2 up
ip netns exec n1 ip route add 169.254.1.1 dev veth2 scope link
ip netns exec n1 ip route add default via 169.254.1.1
ip netns exec n1 ip addr add 172.19.1.10/24 dev veth2
ip link set veth1 up
ip route add 172.19.1.10 dev veth1 # 这个路由必须有
ip netns exec n1 ip route del 172.19.1.0/24 dev veth2 proto kernel scope link src 172.19.1.10
echo 1 > /proc/sys/net/ipv4/conf/veth1/proxy_arp
echo 1 > /proc/sys/net/ipv4/ip_forward

上面的脚本是用来创建一个虚拟的 Pod 的,可以在不同的主机上执行一下,这里要记得修改一下 IP 地址,来保证两个 Pod 的 IP 不同。

之后在宿主机上创建 IP 隧道。也是两台主机都要执行。

ip tunnel add mode ipip
ip link set tunl0 up
ip route add 172.19.1.0/24 via 192.168.105.135 dev tunl0 proto bird onlink

这里在创建 IP 隧道时,并没有指定隧道对端的地址,因为在实际的集群中,1对1的隧道是没使用场景的。而是使用路由告诉这个隧道的对端地址。这时候在 netns n1 内就可以 ping 通对端的 IP 了。

流程图如下

calico ipip

proxy_arp在calico中的妙用

概述

proxy_arp 是网卡的一个配置,在开启后,该网卡会使用自己的 MAC 地址应答非自身 IP 的 ARP Request。常见的用途就是当两台主机的 IP 在同一个网段内,二层却不通,就可以使用额外的一台主机作为 proxy,将这台主机的网卡开启 proxy_arp,来作为中间代理打通网络。如下图所示:

img

开启网卡的 proxy_arp 也很简单:

echo 1 > /proc/sys/net/ipv4/conf/veth1/proxy_arp

calico 是一个使用路由方案打通网络的网络插件,在作为 k8s cni 时,其也使用了 proxy_arp,作为打通路由的一个环节。在了解 calico 如何使用 proxy_arp 之前,我们先看一下 flannel 的 host-gw 是如何使用路由打通 pod 网络的。

flannel host-gw 路由方案

两台二层互通的主机上的 pod,如果要通过路由来互相访问,常见的方式是类似于 flannel 的 host-gw 模式。其流量路径如下:

  1. 每台主机上都有一个 bridge,pod 通过 veth pair 接入到 bridge 上。
  2. pod 将 bridge 的 ip 作为网关。这样 pod 访问其他网段的 IP 时,流量就会到达 bridge 上。
  3. 流量到达 bridge 后,就可以根据宿主机上的路由表转发到对端主机。
  4. 对端主机也会根据路由表,将流量从 bridge 转发到 pod 内。

flannel-host-gw

calico 的路由方案

相比于 flannel host-gw 模式,calico 采用了更巧妙的方法,省掉了 bridge。

其 veth pair 的一端在 Pod 内,设置为 pod 的 IP,另一端在宿主机中,没有设置 IP,也没有接入 bridge,但是设置了 proxy_arp=1。

pod 内有以下的路由表:

default via 169.254.1.1 dev veth2 
169.254.1.1 dev veth2 scope link 

169.254.0.0/16 是一个特殊的 IP 段,只会在主机内出现。不过这里这个 IP 并不重要,只是为了防止冲突才选择了这个特殊值。当 Pod 要访问其他 IP 时,如果该 IP 在同一个网段,那就需要获取该 IP 的 MAC 地址。如果不在一个网段,那么根据路由表,就要获取网关的 IP 地址。所以无论如何,arp 请求都会到达下图中的 veth1。

因为 veth1 设置了 proxy_arp=1,所以就会返回自己的 MAC 地址,然后 Pod 的流量就发到了主机的网络协议栈。到达网络协议栈之后,就和 flannel host-gw 一样,被转发到对端的主机上。

流量到达对端主机后,和 flannel host-gw 不一样的是,主机上直接设置了 pod 的路由:

172.19.2.10 dev veth1 scope link

也就是直接从 veth1 发到 pod 内。

proxy_arp

参考

2.2. Proxy ARP

戳穿 Calico 的谎言

linux 网络数据包接收流程(一)

概述

Linux 作为最流行的服务器操作系统,其提供的网络能力也是经过了各种各样场景的考验。因此如果经常和 linux server 打交道的话,了解 linux 的数据包处理流程也是很有必要的。

网络数据包的接收处理可以分成两个部分,一是从物理网卡进入到达 linux 内核的网络协议栈,二是经网络协议栈处理后交给上层应用或者转发出去。本篇文档主要说明第一部分,并且不会去深入细节点(因为我也不太熟)。

重要概念和数据结构

在说明网络数据包的处理流程之前,有必要提前讲一下一些相关的概念,因为这些概念决定了后面的内容是否能够理解。

硬中断

硬中断是由硬件在发生某些事件后发出的,称为中断请求(IRQ),CPU 会响应硬中断,并执行对应的 IRQ Handler。对于网卡来说,在有网络流量进入后,网卡会通过硬中断通知 CPU 有网络流量进来了,CPU 会调用对应网卡驱动中的处理函数。

硬中断在处理期间,是屏蔽外部中断的,所以硬中断的处理时间要尽可能的短。

软中断

软中断是由软件执行指令发出的,因为硬中断的特点不能处理耗时的任务,所以软中断往往用来替代硬中断来处理耗时任务。

比如网络流量的处理,网卡在发出硬中断通知 CPU 处理后,这次硬中断的处理方法中又会触发软中断,由软中断接着去处理网络流量数据。

网卡驱动

驱动是打通硬件和操作系统的通道,linux 通过网卡驱动,可以支持不同厂商,不同型号,不同特性的网卡。网卡驱动主要负责将从网卡中进来的流量解析并转换成 sk_buff,交给内核协议栈。

DMA

DMA是一种无需CPU的参与就可以让外设和系统内存之间进行双向数据传输的硬件机制。网卡会通过 DMA 直接将网络流量数据存储到一块提前申请好的内存区域中。

NAPI

全称 New API,因为没有更好的名字,所以就直接用 NAPI 了。这是用于支持高速网卡处理网络数据包的一种机制。非 NAPI 往往是只依靠硬中断的方式让 CPU 来处理数据包,NAPI 引入了硬中断+轮询的方式,有效的缓解了硬中断带来的性能问题。

sk_buff

sk_buff 是一个非常大而通用的 struct,可以用来表示2,3,4层的数据包。它被分成两个部分:head 和 data。

head 部分有单独的字段表示不同层的网络头:

  • transport_header:用来表示传输层(4层)的 header,包括 tcp, udp, icmp 等协议头
  • network_header:用来表示网络层(3层)的 header,包括 ip, ipv6, arp 等协议头
  • mac_header:用来表示链路层(2层)的 header。

当数据包进入网络协议栈之前,需要先被转换成 sk_buff。

流程梳理

数据包进入触发硬中断

流量进入到硬件中断

  1. 数据包进入网卡设备

  2. 网卡设备通过 DMA 直接写入的内存中。如果写不下就直接 drop 掉

  3. 网卡产生硬中断

  4. CPU 收到硬中断后,会直接提前注册好的该硬中断的 handler。这个 handler 是写在网卡驱动中的一个方法

  5. IRQ handler 禁用网卡的 IRQ。这是后面处理内存中的数据包是采用的 poll 模式。也就是说 cpu 会自己去内存中轮询数据包,直到一定时间/数量,或者全部处理完之后。这段时间内就不需要网卡通过硬中断来通知 CPU 了,并且硬中断会打断 CPU 的工作,带来一定的性能问题。

  6. 网卡驱动产生软中断。

软中断触发数据包的处理

软中断触发数据包的处理

这里为了方便表述,使用目前最常用的 NAPI 的处理流程进行说明。

  1. 在系统启动时,net_dev_init 方法中注册了 NET_RX_SOFTIRQ 对应的 handler 是 net_rx_action。上面触发软中断的方式是 __raise_softirq_irqoff(NET_RX_SOFTIRQ)。所以开始执行 net_rx_action
  2. net_rx_action 会从 poll_list 链表中获取第一个 poll,使用 napi_poll 轮询内存中的数据包。napi_poll 调用到网卡驱动提供的 poll 方法
  3. poll 方法中从内存中取出数据包
  4. 网卡驱动调用 napi_gro_receive 来处理数据包
  5. napi gro 会合并多个 skb 数据包,比如一个 IP 包会被分成多个 frame 这种。那么如果在接收的时候,在到达协议栈之前直接合并,会有一定的性能提升。这里最终会调用到 gro_normal_list 来批量处理 skb。
  6. 最终调用到 netif_receive_skb_list_internal,从 napi.rx_list 上处理 sk_buff 链表。
  7. 如果开启了 RPS,会根据 skb 的 hash 值找到对应的 cpu,将 skb 存储到该 cpu 上的 backlog 队列。backlog 队列是一种用软件方式将数据包处理负载均衡到多个 cpu 上的一种方法。
  8. 最终都会调用到 __netif_receive_skb_core。
  9. 如果有 AF_PACKET 的 socket,还会拷贝一份给它(tcpdump 的实现原理)。
  10. 最后递交给内核协议栈

参考

Linux协议栈–NAPI机制

Monitoring and Tuning the Linux Networking Stack: Receiving Data

linux kernel 网络协议栈之GRO(Generic receive offload)

flannel 的多种 backend 实现分析

一、概述

flannel 是一个较简单的网络插件,其支持多种网络方案,可以使用 etcd 或者 k8s 作为存储,来实现 docker 或者 k8s 的网络。其支持的网络方案 backend 有:

  • hostgw
  • udp
  • vxlan
  • ipip
  • ipsec

同时也支持了多家云厂商的网络环境:

  • AliVPC
  • AWS VPC
  • GCE

下面会简单介绍 flannel 的工作原理,并主要就标准环境下的网络方案 backend 做分析。

二、flannel 网络方案

flannel 的部署文件中包含一个 configmap,其中包含 flannel cni 的配置文件,以及 flannel 需要的 cluster-cidr 和使用的 backend 配置。flannel 通过 daemonset 调度到每个节点上。flannel 的 pod 有一个 init 容器,负责将 configmap 中的 cni-conf.json 复制到宿主机上的 /etc/cni/net.d/10-flannel.conflist。之后 flanneld 启动,其拥有 NET_ADMINNET_RAW 的 capabilities。

这里需要注意的是,如果你的默认路由对应的网卡不是 node 使用的网卡(比如使用 vagrant 部署 k8s 时,虚拟机的 eth0 是默认的 nat 网卡,但是不用在 k8s 集群中),应该使用 --iface=eth1 来指定使用的网卡。

flannel 会根据 cluster-cidr 来为每个 node 分配单独的子网,这样就能保证不同 node 上的 pod ip 不会冲突。然后根据配置文件中不同的 backend 来注册网络。下面就开始简单分析不同 backend 的工作原理。其中

  1. IPIP 和 VXLAN 类似,不过 VXLAN 封装的是二层的帧,IPIP 封装的是 IP 包。这里不做分析。

  2. UDP 使用的很少,也不做分析。

  3. IPSEC 关注的是通信安全方面。不是这里关注的重点。不做分析。

为了方便后续的描述,这里先列举出整个集群的概况:

cluster cidr: 172.10.0.0/16

master: 192.168.33.101,子网是 172.10.100.0/24

node1: 192.168.33.102,子网是 172.10.0.0/24

node2: 192.168.33.103,子网是 172.10.1.0/24

2.1 host-gw

host-gw 是最简单的 backend,所有的 pod 都会被接入到虚拟网桥 cni0 上,然后它通过监听 subnet 的更新,来动态的更新 host 上的路由表。通过路由来实现不同 node 上的 pod 间通信以及 node 和 pod 间的通信。如下图所示:

flannel-hostgw

  1. Node1 上的 pod A(172.10.0.134) 和 node2 上的 pod B(172.10.1.3) 通信时,A 根据 namespace 下的路由规则default via 172.10.0.1 dev eth0将流量发往网关 cni0 到达宿主机。
  2. 根据宿主机路由规则 172.10.1.0/24 via 192.168.33.103 dev eth1 ,通过网卡 eth1 发往 192.168.33.103 这个网关,而这个网关正好是 node2 的 eth1 网卡 ip。
  3. node2 此时扮演网关的角色,根据路由规则 172.10.1.0/24 dev cni0 proto kernel scope link src 172.10.1.1, 通过 cni0 发送。使用 arp 找到目标 ip 对应的 mac 地址。将二层的目标 mac 地址替换成 pod B 的 mac 地址。将二层的源 mac 地址替换成 cni0 的 mac 地址。
  4. cni0 是个 bridge 设备。根据 mac 表来将流量从对应端口发送到 pod B 中。

因为通信过程的第 2 步需要将其他 node 作为网关,因此 hostgw 需要所有 node 二层互通。

2.2 VXLAN

相比于 host-gw 必须要二层互通。VXLAN 是个 overlay 的网络实现,只需三层互通即可。在 flannel 的实现中,并没有使用 VXLAN 的全部能力,仅仅用它来做二层包的封装和解封装。其整个流程图如下:

flannel-vxlan

可以发现,相比于 host-gw,增加了 flannel.1 这个设备。这个 flannel 的进程在启动的时候创建的。同时它还会监听所有的子网,每个节点加入网络中,都会创建一个属于自己的子网。flannel 进程在监听到新的子网创建时,会在当前节点创建以下:

  1. 一条路由:172.10.0.0/24 via 172.10.0.0 dev flannel.1。172.10.0.0 的 IP 是其他节点的 flannel.1 地址。
  2. 一条 ARP: 172.10.0.0 ether ee:9a:f8:a5:3c:02 CM flannel.1。在包通过路由发出去前,需要知道 172.10.0.0 的二层地址。这时就会匹配这条 ARP 记录。
  3. 一条 FDB:ee:9a:f8:a5:3c:02 dev flannel.1 dst 192.168.33.102 self permanent。这条 FDB 记录会匹配二层的转发路径。

为了更好的理解 flannel 的 vxlan 实现,我们按照图中的步骤一步步分析。

  1. Pod B (172.10.1.3) 向 Pod A (172.10.0.134) 发送数据。因为 Pod A 和 Pod B 的 IP 不在一个子网,因此走默认路由表,发向 172.10.1.1。这个地址是 cni0 的地址,因此可以直接发过去。

  2. IP 包到达 cni0 网桥后,根据主机路由表 172.10.0.0/24 via 172.10.0.0 dev flannel.1,下一跳是 172.10.0.0,通过 flannel.1 发送。

  3. 此时需要知道 172.10.0.0 的 mac 地址,因此检查主机的 arp 表。发现 172.10.0.0 ether ee:9a:f8:a5:3c:02 CM flannel.1,因此要发送的帧如下:

    ethernet-frame

  4. 二层帧的转发需要查找主机的 fdb 表。这里匹配到 ee:9a:f8:a5:3c:02 dev flannel.1 dst 192.168.33.102 self permanent。封装成 vxlan 的包从 eth1 发出去。发出去的包如下:

    vxlan

  5. 对端的 eth1 网络收到包,发现是 vxlan,于是会对包解封装。二层地址是 flannel.1 设备的 mac 地址。因此发到 flannel.1 上。

    ethernet-frame

  6. 此时三层目标地址是 172.10.0.134,因此匹配主机的路由表 172.10.0.0/24 dev cni0 proto kernel scope link src 172.10.0.1。这个路由表没有写在上图中。

  7. cni0 和我们的 pod 是二层互通的。因此将包发给 pod。

  8. pod 收到包。三层的来源地址是 172.10.1.3,二层的来源地址是 cni0 的 mac 地址。

可以通过以下命令行,模拟整个流程。

# host1
br0_ip="10.20.1.1"
vtep_ip="10.20.1.0/32"
endpoint_ip="10.20.1.4/24"
sudo ip link add name br0 type bridge forward_delay 1500 hello_time 200 max_age 2000 vlan_protocol 802.1Q
sudo ip addr add br0_ip/24 dev br0
sudo ip link add name vtep0 type vxlan id 1 dev ens33 srcport 0 0 dstport 4789 nolearning proxy ageing 300
sudo ip addr addvtep_ip dev vtep0
sudo ip link add name veth0 type veth peer name veth1
sudo ip netns add n1
sudo ip link set veth1 netns n1
sudo ip link set veth0 master br0
sudo ip netns exec n1 ip addr add endpoint_ip dev veth1
sudo ip netns exec n1 ip link set veth1 up
sudo ip netns exec n1 ip route add default viabr0_ip dev veth1
sudo ip link set veth0 up
sudo ip link set br0 up
sudo ip link set vtep0 up

# host2
br0_ip="10.20.2.1"
vtep_ip="10.20.2.0/32"
endpoint_ip="10.20.2.4/24"
sudo ip link add name br0 type bridge forward_delay 1500 hello_time 200 max_age 2000 vlan_protocol 802.1Q
sudo ip addr add br0_ip/24 dev br0
sudo ip link add name vtep0 type vxlan id 1 dev ens33 srcport 0 0 dstport 4789 nolearning proxy ageing 300
sudo ip addr addvtep_ip dev vtep0
sudo ip link add name veth0 type veth peer name veth1
sudo ip netns add n1
sudo ip link set veth1 netns n1
sudo ip link set veth0 master br0
sudo ip netns exec n1 ip addr add endpoint_ip dev veth1
sudo ip netns exec n1 ip link set veth1 up
sudo ip netns exec n1 ip route add default viabr0_ip dev veth1
sudo ip link set veth0 up
sudo ip link set br0 up
sudo ip link set vtep0 up


# host1
host2_vtep_mac="f2:a4:1f:4e:5c:51"
host2_vtep_ip="10.20.2.0"
subnet_mask="24"
host2_ip="192.168.105.167"
# one route
sudo ip route add host2_vtep_ip/subnet_mask via host2_vtep_ip dev vtep0 onlink
# one arp
sudo arp -i vtep0 -shost2_vtep_ip host2_vtep_mac
# one fdb
sudo bridge fdb addhost2_vtep_mac dev vtep0 dst host2_ip

# host2
host1_vtep_mac="be:ae:0d:f3:da:77"
host1_vtep_ip="10.20.1.0"
subnet_mask="24"
host1_ip="192.168.105.166"
# one route
sudo ip route addhost1_vtep_ip/subnet_mask viahost1_vtep_ip dev vtep0 onlink
# one arp
sudo arp -i vtep0 -s host1_vtep_iphost1_vtep_mac
# one fdb
sudo bridge fdb add host1_vtep_mac dev vtep0 dsthost1_ip

# host1 and host2
echo "1" > /proc/sys/net/ipv4/ip_forward

# host1
ip netns exec n1 ping 10.20.2.4

ARP 协议笔记

ARP 协议是什么

在具体学习 ARP(Address Resolution Protocol) 协议之前,我们应该先了解 ARP 协议的使用场景。大多数人对 ARP 协议可能和我一样,都有一个大概的印象。比如它是在已知 IP 地址的情况下,用来查找 MAC 地址的协议。这里也试着将 wikipedia 上的定义翻译过来,给出一个较为全面准确的定义:

ARP 协议是一种通信协议,用来发现网络层地址(比如 IPv4地址)关联的链路层地址(通常是 MAC 地址)。

以下的内容都来自于 wikipedia: https://en.wikipedia.org/wiki/Address_Resolution_Protocol

ARP 报文

ARP 协议使用一种格式来表示地址解析的请求或响应。ARP 消息的大小取决于链路层或者网络层地址的大小。报文头指明了每一层使用的网络类型以及地址的大小。报文头以 operation code(op) 结束,code 为 1 时表示请求,为 2 时表示响应。报文内容部分由四个地址组成,分别为发送者的硬件地址(Sender hardware address,简称 SHA)、发送者的网络层地址(Sender protocol address,简称 SPA)、目标的硬件地址(Target hardware address,简称 THA)、目标的网络层地址(Target protocol address,简称 TPA)。

arp package

上图是以 IPv4 为例。这种情况下,SHA 和 THA 的大小为 48bit,SPA 和 TPA 的大小的 32bit。报文头的大小固定是 8 个字节。在 IPv4 的情况下就是总共有 28 个字节。下面,也以 IPv4 为例分别对 ARP 报文的每个字段进行解释。

1~2,Hardware type(HTYPE): 指明链路层协议类型,以太网是1。

3~4,Protocol type (PTYPE):指明网络层协议类型。对于 IPv4 来说,值是 0x0800。

5,Hardware address length(HLEN):硬件地址的长度。以太网地址的长度是6。

6,Protocol length(PLEN):网络层地址的长度。IPv4 地址长度是 4。

7~8,Operation(OP):指明发送方执行的操作,1是请求,2是响应。

到此,ARP 的报文头结束。

9~14,Sender hardware address(SHA):发送方的 MAC 地址。在 ARP 请求中,它代表的是发送请求方的地址。在 ARP 响应中,它代表的是这次 ARP 请求查找的主机地址。

15~18,Sender protocol address(SPA):发送方的网络层地址。

19~24,Target hardware address(THA):接收方的 MAC 地址。在 ARP 请求中,这个字段是被忽略的。在 ARP 响应中,这个字段用来表示 ARP 请求源主机的地址。

25~28,Target protocol address(TPA):接收方的网络层地址。

ARP 的以太网帧类型是 0x0806。

例子

在一个办公室中的两台电脑 c1(192.168.1.100) 和 c2(192.168.1.101) ,在局域网内通过以太网接口和交换机连接,中间没有网关和路由器。

下面我通过 linux 的网桥和 network namespace 来模拟这一场景:

# 准备交换机
ip link add name switch type bridge
# 准备一根网线,一头连接电脑c1,一头连接交换机
ip link add name veth_c10 type veth peer name veth_c11
# 准备一根网线,一头连接电脑c1,一头连接交换机
ip link add name veth_c20 type veth peer name veth_c21
# 准备电脑c1
ip netns add c1
# 准备电脑c2
ip netns add c2
# 将网线插入 c1
ip link set veth_c11 netns c1
# 将网线插入 c2
ip link set veth_c21 netns c2
# 将两根网线都插到交换机
ip link set veth_c10 master switch
ip link set veth_c20 master switch
# 启动交换机
ip link set switch up
# 启动c1
ip link set veth_c10 up
# 启动c2
ip link set veth_c20 up
# 为c1和c2分配ip
ip netns exec c1 ip addr add 192.168.1.100/24 dev veth_c11
ip netns exec c2 ip addr add 192.168.1.101/24 dev veth_c21
ip netns exec c1 ip link set veth_c11 up
ip netns exec c2 ip link set veth_c21 up

环境准备好了之后,c1 想要跟 c2 通信,此时 c1 需要知道 c2 的 MAC 地址。首先它会查找本地是否有缓存的 ARP 表。因为我们的环境刚刚创建好,所以肯定是没有缓存的,那么这个时候,c1 就会发送 ARP 请求,来查找 c2 的 MAC 地址。为了看到 c1 和 c2 之间的所有通信,我们可以用 tcpdump 或 wireshark 来抓交换机上的包,我这里为了展示的更清晰,采用 wireshark 来抓包。

从 c1 向 c2 发送一次 ping。

ip netns exec c1 ping -c 1 192.168.1.101

wireshark 抓包截图如下:

wireshark

第一条是 ARP 请求。它是封装在以太网帧中的。

arp request

以太网帧的广播地址是 ff:ff:ff:ff:ff:ff,源地址是 2e:ee:58:76:59:fc。类型是 ARP。ARP 请求中因为不知道目标的 MAC 地址,所以是 00:00:00:00:00:00。十六进制表示如下:

arp request hex

ARP 响应报文如下:

arp reply。通过这个响应我们也能知道 c1 的 MAC 地址是 2e:ee:58:76:59:fc,c2 的 MAC 地址是 76:cb:15:06:92:87。这个时候,我们也可以看一下 arp 表的情况。

ip netns exec c1 arp -a
? (192.168.1.101) at 76:cb:15:06:92:87 [ether] on veth_c11

ARP 探针(ARP probe)

ARP 探针是一种 SPA 全为 0 的请求。在使用一个 IPv4 地址之前,实现了这个规范的主机必须检查这个地址是否已经在使用了。就是通过这样一个请求来检查的。

为什么要 SPA 全为 0 呢?这是为了防止如果存在冲突,这个请求可能会污染其他主机的 arp 表。

ARP 通告(ARP announcements)

ARP 可以用来作为一种简单的通告协议。当发送方的 IP 地址或者 MAC 地址发生改变后,用来更新其他主机的 MAC 表映射。ARP 通告请求在 target 字段上包含了 SPA 的值(TPA=SPA),THA 为 0,然后广播出去。因为 TPA 为自己的网络层地址,所以不会有其他主机的 ARP 响应。但是其他主机都会收到发送方的 MAC 地址和 IP 地址,那么就可以更新自己的缓存。

ARP 欺骗(ARP spoofing)和 代理 ARP(proxy ARP)

ARP 欺骗很好理解,就是让 ARP 请求的发送方收到错误的 ARP 响应。比如现在我们有三台电脑:

  • c1: 192.168.1.100(2e:ee:58:76:59:fc)
  • c2: 192.168.1.101(76:cb:15:06:92:87)
  • c3: 192.168.1.102(12:07:6b:be:20:d2)

c1 想给 c2 发送数据,在 c1 发 ARP 请求的时候,我们将 ARP 响应中 c2 的 MAC 地址改为 c3 的 MAC地址。然后 c1 的数据就都会发给 c3 了,但是 c1 仍然认为自己在和 c2 通信,这就是 ARP 欺骗了。

代理 ARP 和 ARP 欺骗很像,只是目的不太一样。代理 ARP 的使用场景一般是两台主机不在同一个二层网内,这样通过代理 ARP 的方式来做流量转发。