Hi,欢迎来到 卡瓦邦噶!我是 laixintao,现在生活在新加坡。我的工作是 SRE,喜欢在终端完成大部分工作,对各种技术都感兴趣。我从 2013 年开始写这个博客,写的内容很广泛,运维的方法论,编程的思考,工作的感悟,除了技术内容之外,还会分享一些读书感想,旅行游记,电影和音乐等。欢迎留下你的评论。

声明:本博客内容仅代表本人观点,和我的雇主无关。本博客承诺不含有 AI 生成的内容,所有内容未加说明均为博主原创,一经发布自动进入公有领域,本人放弃所有权利。转载无需本人同意。但是依然建议在转载的时候留下本博客的链接,因为这里的很多内容在发布之后会还会不断地继续更新和追加内容。 Not By AI

iptables 拦截 bridge 包的问题排查

最近排查的一个网络问题,两个 IP 之间的网络不通,经过在 Linux 上一个一个 interface 上抓包,发现包丢在了本地的 bridge 上。

Bridge 就是一个简单的二层设备,虽然是虚拟的,但是应该逻辑也很简单,怎么会丢包呢?

经过一通乱查,发现 Bridge 的包跑到了 iptables 里面去,被 iptables 的 FORWARD chain DROP 了。

iptables dropped pakcet

说到这里跑个题,我有一个排查 iptables 是哪一条 rule 丢包的妙计,就是 watch -d "iptables -nvL | grep DROP,watch 会监控引号中的脚本,脚本会过滤出来所有会丢包的 rule,-d 参数很关键,它可以让 watch 每次对比和上一次命令的不通,然后高亮出来。一眼定位到问题。

话说回来,bridge 一个二层的设备怎么会跑到 iptables 里面去?iptables 可是 IP tables,这是三层呀。

在 Linux 中有一个机制,可以让 layer 2 的 bridge 代码调用 iptables, arptables, ip6tables 等。这样能做的事情就比 BROUTING chain (Bridge Routing1) 更多。可以在 bridge 上通过 iptables 做 dnat, stateful firewall, conntrack 等2

如果不需要 bridge 上的包跑到 iptables 上过一遍,可以通过 kernel 参数关闭:

sysctl -w net.bridge.bridge-nf-call-iptables=0

0 的意思是 bridge 的包不会去 iptables,1 就是会去 iptables,默认是 1. 也是执行完这行命令,网络果然就通了。

Bridge call iptables, 之前是在 kernel 实现的一个功能,但是显然这样会有性能问题。后来就独立出来作为一个独立的 kernel module 了 (br_netfilter3)。(如果使用 physdev 4就会自动启用这个 module)。

另外,nftablesiptables-nft 也会受到影响,layer violation 会有很多复杂的问题。新的 kernel module – nf_conntrack_bridge5 可以做到直接在 bridge layer 实现 connection track. nftables6 是下一代的 iptables。

前面说过这个功能是一个 kernel module,所以在关闭的时候有一个小小的问题。即我们关闭的时候,可能还没有这个 module load,所以会告诉我们无法设置这个参数:error: "net.bridge.bridge-nf-call-iptables" is an unknown key。如果后来创建一个 bridge,那么这个 module 会自动 load,那么包就又会跑到 iptables 里面去。

libvert 给出的解决方案7是,通过 udev 来创建一个事件,每次创建 bridge 就执行 sysctl 来 disable net.bridge.bridge-nf-call-iptables.

  1. ebtables https://linux.die.net/man/8/ebtables ↩︎
  2. ebtables/iptables interaction on a Linux-based bridge:https://ebtables.netfilter.org/br_fw_ia/br_fw_ia.html ↩︎
  3. https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-3.18.y&id=34666d467cbf1e2e3c7bb15a63eccfb582cdd71f ↩︎
  4. physdev: https://manpages.debian.org/bookworm/iptables/iptables-extensions.8.en.html#physdev ↩︎
  5. nf_conntrack_bridge: https://www.kernelconfig.io/config_nf_conntrack_bridge ↩︎
  6. nftables wiki: https://wiki.nftables.org/wiki-nftables/index.php/Main_Page ↩︎
  7. https://wiki.libvirt.org/Net.bridge.bridge-nf-call_and_sysctl.conf.html ↩︎
 

Docker 命令行小技巧:runlike

事情要从上周的一次事故说起,我们用 docker 部署的程序有一点问题,要马上回滚到上一个版本。

这个 docker 是一个比较复杂的和 BPF 有关的程序,启动时候需要设置很多 mount 和 environments,docker run 的命令特别长。所以我用 Ansible 来配置好这些变量,然后启动 docker,一个实例要花费 3~5 分钟才能启动。

同事突然说,某实例他手动启动了,当时我就震惊了,怎么手速这么快?!

请教了一下,原来是用的 runlike 工具,项目地址是:https://github.com/lavie/runlike

这个工具的原理是,docker inspect <container-name> | runlike --stdin ,就会生成这个容器的 docker run 命令。这个思路简直太棒了。就和 Chrome 的 copy as cURL 功能一样好用!

 

ARP Flux 问题和解决方法

我们写程序不需要写 ARP request 和 response 相关的逻辑,因为这部分是操作系统帮我们做的。

Linux 处理 ARP 请求的逻辑是:

如果操作系统收到了一个 ARP reuqest,并且本机中某一个接口配置了 ARP request 中请求的 IP,那就回复这个 ARP request,回复的 ARP response 中,使用收到此 ARP request 的 interface 的 MAC 地址作为答案。

这个逻辑看起来没有问题:从 Linux 的视角,既然我能从一个 interface 收到 ARP 请求,那么对方向这个 interface 发送数据包,我也可以从这个 interface 收到,所以,回复 interface 的 MAC 地址即可。

但是,在下面这种情况就会出问题:

  1. Linux 有两个 interface 接入到了同一个交换机上;
  2. 这两个 interface 配置的 IP 地址在同一个广播域;

满足这两个条件的话,当有 LAN 中有主机发送 ARP request 广播包,交换机会转发到自己的所有端口,Linux 会从这两个端口都收到此 ARP request 广播包。按照以上处理逻辑,Linux 会回复 2 个 ARP reply,因为收到了两个 request,两个 interface 各回复一次,并且这两个 ARP reply 分别是两个 interface 的 MAC 地址。

实验环境如下图所示:

ubuntu-1 有两条线接到同一个交换机

配置好 IP 之后,我们在 ubuntu-2 主机发送 ARP request。

一共发送了 3 个 request,却收到了 6 个 ARP reply。并且有两种 MAC 地址。

这个就是 ARP Flux 问题。

解决方法0: 配置不同的 IP subnet

因为 ARP 是在 LAN 内的,如果两个 interface 分别配置在不同的 subnet,就不会有这个问题了。

但是有时候我们的软件守交换机网段的限制,只能配置在一个网段。并且有些场景需要两个线路和两个 IP,比如管理和数据面分开(带外管理),就又会遇到这个问题。

解决方法1: 隐藏其中一个接口

使用 ip link set dev eth0 arp off 可以禁用 interface 的 ARP 恢复,这样,在 LAN 上就相当于把这个 interface 隐藏了,因为没有人可以发现它的 MAC 地址了。在四层负载均衡中,如果使用 DSR 的模式并且不加隧道的话,就需要对 RS 的 VIP 禁用 ARP 回复1

这里有一个有意思的现象:假设 192.168.1.2 配置在 eth0 上,但是我们把 eth0 的 ARP 禁用了。

192.168.1.2 所在的 eth0 ARP禁用

这时候如果请求 192.168.1.2 的地址,会拿到 eth1 的 MAC 地址。参考上文讨论过的 Linux ARP 工作原理。

这个方法只能通过 interface 级别来设置,如果一个 interface 上有多个 IP 地址,那么只能全部禁用或者开启。

如果要 by IP 来禁用 ARP,可以使用 arptables(8)2 3.

解决方法 2: arp_ignore

直接禁用 ARP 差不多等于把这个 interface 关闭了,大部分情况下不是我们想要的结果。

我们希望的效果是对于 ARP request 只回复一次,并且只有 IP 在自己的 interface 上才回复,不要代替其他 interface 回复。

通过 arp_ignore4 可以改变 Linux 的 ARP 行为:

  • 0 – 默认,只要本地有的 IP 就会回复,无论是哪一个 interface;
  • 1 – 只有当 ARP request 询问的 IP 配置在收到 ARP 的接口上时,才会回复;
  • 2 – 同 1,并且 sender 的 IP 在本地 IP 的相同 subnet 内,才会回复;
  • 3scope host 不会回复,只有 scope linkscope global 才会回复 (可以通过 ip address 命令查看配置的 IP scope);
  • 47 – 保留字段;
  • 8 – 所有 local address 都不回复;

配置方法:通过 sysctl -w 可以修改配置。arp_ignore 是 per interface 的配置。

其中 all 是一个全局变量,最终在 interface 上生效的值是 max(all, eth0)。以最大的为准。

default 是模板变量,在新建一个 interface 的时候,会自动使用这个值。

我们可以给所有的 interface 都设置为 1.

然后再发送 arp 请求,就会发现回复之后一个了,并且是正确的那个。

解决方法3: arp_filter

另一个方法是使用 arp_filter 参数,这个参数是一个 bool,默认是 0 ,如果开启为 1,就意味着,如果收到 ARP request,ARP 请求的 IP 为 A,收到 ARP 的 interface 为 X 和 Y,那么 kernel 就测试路由,假设要发出去 source IP 为 A 的包,从 X 发出去还是从 Y 发出去。如果从 X 发出去,那么只有 X 会回复。(source based rotuing).

举例来说,我们现在有两个接口和两个 IP,回复 192.168.1.10 的 ARP 时,可以测试当前的路由情况:

可以看到无论 source IP 是哪一个,都会从 eth0 口出。

这是因为我们的 ip route 配置:

会永远走第一条路由。

所以我们如果发送 ARP request,对 .2.3 两个地址,得到的结果会是一样的,因为都是 eth0 在回复。

如果我们把第一条路由删了,那么现在就都走 eth1 了,这时候 ARP 结果就都是 eth1 了。

Lookback 接口是否会回复 ARP?

答案是是的(虽然听起来很没道理)。这个主要是跟路由有关。

当我们在给 lo 绑定一个地址的时候,kernel 会默认添加 2 条路由,lo 所在网段和 lo 的地址会被标记成 dev lo 的本地路由。

其实,当我们给 lo 地址添加 192.168.1.2/24 的时候,由于自动添加的 192.168.1.0/24 的 route (第5行),导致我们其他的接口也不会回复这个网段的 ARP 请求了。这个时候从 ubuntu-2 上 arping 192.168.1.10192.168.1.11 都会 timeout。

这个是默认的行为。但是如果我们改一下,删除这两条路由,添加另一条从 eth0 口出的路由的话,就会发现,arping lo 接口的 IP,也会收到 ARP response 了。

在 ubuntu-2 可以 ping 通。

所以说 lo 接口是否回复 ARP request,主要和 ip route 的设置有关。

  1. 四层负载均衡漫谈 ↩︎
  2. arptables(8) – Linux man page ↩︎
  3. Using arptables to disable ARP ↩︎
  4. Kernel 的 ip sysctl 文档 ↩︎
 

特殊的 ARP 用法:Gratuitous ARP, ARP Probe 和 ARP Announce

在 Ethernet 环境中,所有的数据最终都要以二层 Ethernet Frame 的形式发送,要添加 Src MAC, Dst MAC, 以及其他的 header,比如 CRC 等,Ethernet 就会把数据送达到目的地。

我们在编程的时候经常指定 IP 和 Port,但是几乎从没有指定过 MAC 地址,那么 Frame 是怎么发出去的呢?这是操作系统帮我做了这件事。如果系统不知道一个 IP 对应的 MAC 地址是什么,系统会发送一个广播的 ARP 请求,(由于这个请求也是要通过 Ethernet 发送出去的,所以也要填充 Dst MAC,Dst MAC 就是 ff:ff:ff:ff:ff:ff。)询问谁有目的 IP,如果有,请回复给 MAC-B。广播域中所有的 Host 都会收到这个请求,只有拥有这个 IP 的 Host 才会回复给 MAC-B 自己的 MAC 地址,这样,B 就知道了这个 IP 对应的 MAC 地址。

这就是普通的 ARP request 和 response 的过程。

其中,里面的几个值得注意的 field 如下:

对于 ARP request 来说:

  • Operation: 1 (1表示 request, 2 表示 response);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:全1,即广播给所有主机;
  • Dst IP:是要询问的 IP;

对于 ARP Response 来说:

  • Operation: 2
  • Src MAC: sender(sender 在这里是响应者) 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:receiver (在这里是 ARP 问题的发出者)的 MAC;
  • Dst IP:receiver IP;

ARP 是一个很简单的协议,但是人们基于交换机和主机的工作原理,设计了其他的用法。这些用法其实并不复杂,最重要的就是理解这些设备最基本的工作原理,这样就可以理解这些特殊的 ARP 设计意图了。

Gratuitous ARP

在之前的博客中介绍过两次使用 Gratuitous ARP 的例子。Gratuitous ARP 的目的是更新其他设备的 ARP cache,或者交换机 mac-address table。

在 VRRP1 中,目的主要是切换网关,更新的对象是 mac-address table。交换机的 mac-address table 主要是通过收到的包的 Src MAC 来学习 MAC 对应的物理 port 的,所以最重要的是 Src MAC,其他的都不重要。

所以在 VRRP 中的 Gratuitous ARP 是这样的:

  • Operation: 1 (request);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:全1,即广播给所有主机;
  • Dst IP:全1;

在 Linux 的 balance-alb bonding 模式2中,ARP 的目的是为了刷新其他主机的 ARP Cache,并且希望不同的主机拿到不一样的 MAC 地址,所以使用的 ARP 是 response,并且是 unicast。

  • Operation: 2 (response);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:bond driver 记录的 MAC;
  • Dst IP:bond driver 记录的 IP;

取决于使用场景,Gratuitous ARP 也是 broadcast 的 reply,将 response 信息广播给所有的主机3

ARP Probe

ARP Probe 的作用是,在分配到一个 IP 之后,但是在 IP 使用之前,可以先用 ARP 协议来检查一下当前的 LAN 内有没有人在使用这个 IP。

检查的方式就是用 ARP 询问这个 IP 的 MAC 地址,如果有主机正在使用,那么就会收到 ARP reply。

但是这里有一个问题。一个主机如果收到了 ARP request,询问谁有 IP X?请发送回复给 IP 地址 Y at MAC 地址 Z。即使这个主机没有 IP X,也不会单纯丢弃这个 ARP request,而是会从这个 ARP request 中学习到,IP Y 对应 MAC Z,会将它放到自己的 ARP cache 中。

这里我们只是想检查一个 IP 是否正在被使用,我们还没有真正地开始使用这个 IP。如果用普通的 ARP 请求来询问,假设这个 IP 已经被使用,那么发出去的 ARP request 就会传达错误的信息,其他主机就会根据这个 ARP request 来更新自己的 ARP cache。

为了解决这个问题,ARP Probe 使用的 request 将 Src IP 设置为 0.0.0.0,这样,这个 ARP request 就完全无害了。

ARP 请求如下(加粗部分是和普通的 ARP request 不一样的部分)。

  • Operation: 1 (request);
  • Src MAC: sender 的 MAC;
  • Src IP: 0.0.0.0
  • Dst MAC:全1,即广播给所有主机;
  • 询问的 IP:自己将要使用的 IP;

ARP Announce

ARP Announce 用于在 ARP Probe 确定没有问题之后,决定使用这个 IP,但是再一次确定唯一性。

ARP Announce 发送一个普通的 ARP request, 在 LAN 内询问自己将要使用的 IP,正常情况下不会收到任何回复,因为这个 IP 是自己的。异常情况下收到回复,那么说明 LAN 已经有人在使用这个 IP 了,需要终止继续使用这个 IP。

ARP Announce 是普通的 ARP 请求,和 Gratuitous ARP 很像。询问的目标 IP 是自己的 IP。

ARP Announce 又叫做 Unsolictied ARP。

ARP Announce 和 ARP Probe 的唯一区别就是 Src IP 不同,ARP Announce 使用自己的 IP 来发送,所以也会更新 LAN 内其他设备的 ARP cache。

  • Operation: 1 (request);
  • Src MAC: sender 的 MAC;
  • Src IP: sender 的 IP;
  • Dst MAC:全1,即广播给所有主机;
  • Dst IP:自己即将使用的 IP;

参考资料:

  1. 首跳冗余协议 VRRP ↩︎
  2. 数据中心网络高可用技术之从服务器到交换机:balance-tlb 和 balance-alb ↩︎
  3. Gratuitous ARP https://www.practicalnetworking.net/series/arp/gratuitous-arp/ ↩︎
 

裕廊坊杂菜饭

裕廊坊新开了一家杂菜饭,欣很喜欢。

杂菜饭,我又叫它「这个,那个」。因为大部分食客点菜的时候都只会说「这个」,「那个」。有一些杂菜饭店干脆取名字就叫做这个那个,英文名字叫 This and That.

杂菜饭多开在食阁里面,价格实惠,菜品很多。一餐大概在人民币 30 元到 45 元。

杂菜饭的价格一直是一个迷。食客用「这个」、「那个」来交流,点完菜之后,老板会脱口而出一个数字,然后我们按照这个数字付钱。老板说多少,就付多少,反正也便宜。(其中,鱼类一般是杂菜饭刺客,点上3条油炸小黄鱼或者一片水煮鱼之类的,一餐可能要到 50元。)

欣执迷想知道每一个菜品的价格,这就比较难了。

虽然店里面挂着菜单和价格,但是看这些名字和菜也对不上。

欣试图靠记忆对比这一顿和上一顿菜的差别试图解出每一个变量的值,却发现这次得到一个值,下次和我点的一比较又矛盾了。

我说,「别忙活了,我们公司旁边有一家,我和我的同事们吃了 3 年了,还没摸透每一个菜多少钱呢!」

依我看,杂菜饭老板会依照当天天气和自己心情来定价,加入一定的随机变量,避免定价秘密泄漏出去。