肯特岗

肯特岗,一定是我在新加坡度过时间最多的地方,因为我在这里上班。

这里地形很奇特,坐了三次电梯上楼,可以从另一个门出来,居然还是一楼,对得起“岗”这个名字了。

去吃午饭的路上,可以望到山岗脚下的一些楼,有希捷,Grab,得意洋洋地把招牌挂在大楼最高的地方。还有一个没有窗户的奇怪建筑,是一个数据中心。

公司每个月都会搞一次大型促销,时间定在 1月1日,2月2日,3月3日,以此类推,非常有创意。像我们这种工作就要苦哈哈地值班,以免系统出现异常。

有一天,凌晨2点下班,打车回家。司机是一个老安哥。

安哥问我,“怎么才能在你们公司的网站上买东西呢?需要到什么地方填表格吗?”

我震惊,好像从没有听说过报纸是何物的 00 后一样。我说,“不需要填表,只要下载一个 app 就可以了,很简单的。”

安哥说,“安哥老了,跟不上时代了,搞不懂了。”

我决定继续解释一下,“只需要下载一个 App,然后填上你的手机号码,收到验证码,填上验证码,然后将商品加入购物车,选择 checkout,然后填上你的银行卡,银行可能会给你发验证码,完成付款,就可以等待收货了。”说完这些,我发现这个过程对一个老人来说可能不简单,也不好意思重复再说很简单的话了。

安哥继续抱怨说,出租车公司发过来一个要上网完成的东西,自己按照说明却操作不好。让我想起来我的母亲,虽然我的工作就是天天跟这些电脑打交道,但是却不擅长教会别人用手机。在诺基亚流行的时代,我教我的母亲怎么把别人的手机号记到手机里,怎么打开新的通讯录,什么键是确认,怎么退出。我的母亲总是喜欢记:按这个键,再按这个键,最后按这个,就能打开通讯录。全不管屏幕在显示什么。

安哥询问我家乡,得知我是中国北方人,又开始兴致勃勃地跟我聊起来北方的冬天,跟我确认他听说的有关冬天的寒冷是不是真的。然后又不知怎的,开始跟我确认一些北方的习俗是不是真的,真相是,他的信息,我大部分都是第一次听说。

生活小技巧

在肯特岗地铁站下车,从唯一的扶梯上楼,右拐进入到一个小商区,在右手边扫码支付 1.7 元,阿姨问你,你就说,要一个 Plain Waffle. 当当,就会收获一个世界上最好吃的华夫饼。

 

真实世界中的 PMTUD

上回书说到被 MTU 问题小小坑了一下,问题最后解决了,但是留了一个疑问点没有证实:为什么在 MSS 协商失败的情况下,curl https://x.com 可以,但是 curl https://accounts.google.com 不可以?

本文的实验代码都是在虚拟机中做的,所以没有隐藏 IP,直接粘贴的 tcpdump 结果。代码太宽,可以通过代码块右上角的工具栏配合阅读,比如点击 <-> 按钮来展开,或者在新窗口浏览。读本文之前,最好先读一下这篇介绍 MTU 介绍的比较好的博客:有关 MTU 和 MSS 的一切 (即本博客)。

上文中的猜想是这些网站实现了 PMTUD,这一点比较容易证明。

PMTUD 测试

TCP 握手的时候双方协商 MSS,是根据本地的网卡信息协商的。比如网卡的 MTU 是 1500,那么 MS S 就会是 1460,如果网卡 MTU 是 1450,那么 MSS 就是 1410. 这个过程,TCP 的双方都对中间网络设备的 MTU 没有概念,中间设备能转发的 MTU 很可能比两边都小(尤其是在有 VPN 或者有隧道的情况)。PMTUD 就是处理这种情况的:它的原理很简单,当有丢包的时候,我尝试发送小包,看能不能收到 ACK,如果能,说明链路 path 的 MTU 比我想的要小,等用小一点的包发送。PMTUD 的全称是 Path MTU Discovery。

验证方法很简单,我们只要创造一个环境,假设这个环境能接受的 MTU 最大是 800,超过 800 bytes 的都会直接丢包,并且不会发回去 ICMP 消息。

我们用 iptables 直接 DROP 掉超过 800 bytes 的包。实验环境我习惯将 DROP 打印出来。

然后,我们还要将 Generic Receive Offload 关闭(以及其他的 offload 也一起关了吧,方便查看)。如果不关的话,即使对方发过来小包,网卡也会帮我们合并成大包,导致被 iptables 丢弃。

最后,我们打开 tcpump,并且发送请求:curl -v https://accounts.google.com。抓包结果如下:

果然,对方一直尝试发给我们大小是 1400 的包,不断被我们丢弃,不断重发,非常锲而不舍,可惜是无用功。

还记得我们当时 MTU 设置错误,还是可以访问通 x.com,我们再拿它来试一下。

以下是 curl https://x.com 的抓包结果:

可以看到,在 server 端发送了 4 个长度为 1448 的包之后,就开始发送了一个长度为 512 的包,发现能够收到 ACK,就加大到 936 尝试扩大 MTU 发送,然后失败了,就退回到 512,可以看到后面还有大包的尝试,同样也失败了。不过最终的结果是发送成功的。

具体 PMTUD 的行为不太一样,比如 facebook.com 的第一次尝试是 1024,然后退到 512.

ICMP type 3 code 4 测试

ICMP 专门有一种消息是处理这种不可达的错误的。ICMP 的 type 3 意思是 Destination Unreachable,但是 Destination Unreachable 的原因有很多,对于每一种原因都有一种 Code,Code 4 意思就是 Fragmentation Needed and Don’t Fragment was Set。(即,包太大,需要拆成多个 IP 包,但是你有设置了不要拆包,所以我只能丢弃,并且用此 ICMP 来告知你。)

在上面的测试中,我们并没有发送任何的 ICMP 消息,而只是丢包。现在,我们添加一步,在丢包的时候,发回去一个 ICMP 消息。我们用 scapy 来做这个。代码非常简单,它抓所有超过 800 bytes 的包,对这些包的来源都发送一个 ICMP。还是 iptables 负责丢包,scapy 脚本只负责发 ICMP。

为什么 filter 是 greater 815 呢?因为 libpcapgreater 是 Ethernet 层的大小,Ethernet 的 header 是 14 bytes,所以我们要的条件是 >= 815 bytesgreater 是大于等于。(是,我也觉得很奇怪)

保存上文件为 a.py。运行方式是 python3 a.py

然后使用这台服务器进行测试。发现…… 结果和上文完全一样,我都告诉他们 next hop mtu 是 800 了,但是他们有自己的想法,从 512 开始尝试之类的。仿佛 ICMP 从来没发送到他们的服务器上。不知道是我构造包的问题,还是他们的服务器没有处理好 ICMP 的问题。比如之前看过 cloudflare 的这篇文章,就是说因为 ECMP 的问题,ICMP 消息会被路由到错误的负载均衡器上去,导致 PMTUD 失败。解决办法是将 ICMP type 3 code 4 广播到所有的负载均衡器上去。

ICMP type 3 code 4 虚拟机测试

为了试试看是不是我的脚本有问题,我在本地搭建了一个非常简单的网络环境。

抓包结构如下,可以看到,8000 端口尝试发送 1448 bytes 的包一直被忽略。当收到 ICMP 消息,server 端就立即改用 800 bytes (MSS 是 748 bytes)来发送了。所以,感觉还是公网发送 ICMP 黑洞的问题。

而且这个 path MTU 信息会在 route 的 cache 中,后续的发送会默认这个 path 的 MTU 就是 800,不会使用更高的尝试。

相关链接:

  1. nmap 有 Path MTU 探测功能:https://nmap.org/nsedoc/scripts/path-mtu.html
  2. Path MTU discovery in practice
  3. Iptables Tutorial 1.2.2 by Oskar Andreasson 一份不错的 iptables 教程
  4. RFC 5508 NAT Behavioral Requirements for ICMP
  5. Resolve IPv4 Fragmentation, MTU, MSS, and PMTUD Issues with GRE and IPsec
 

一次网络问题排查

故事起因

我们需要在一个新的环境搭建 Jenkins (一个 Java 程序)。因为我们不想自己维护 Java 运行环境,所以是将 Jenkins 运行在 Docker 里面的。

需要我去申请了 VM,然后在 VM 里面安装好 Docker,用 Jenkins 官方的 Docker 镜像启动 Docker 容器,一切正常。然后回到浏览器登录,发现这时候 Jenkins 报错了。

Jenkins 打开报错

查看 Jenkins 的日志,错误是:java.net.SocketTimeoutException: Read timed out。但是不知道具体是要访问什么服务报错的。

收集信息

首先,我们先找到具体是访问哪一个服务不通。在 Jenkins 启动的过程中进行 tcpdump 可以发现,确实是访问一个 IP 的 443 端口会卡住。但是这个 IP 是可以 ping 通的,说明 3 层没问题,问题是出在 4 层及以上。tcpdump 发现对这个 IP 的 443 端口建立 TCP 连接是没有问题的,但是在数据交换的过程中会卡住。那很可能是应用层的问题。

要知道这个服务是干啥的,我们要找到这个 IP 对应的 domain。那这个 IP 是怎么拿到的呢——对了,是 DNS,用 tcpdump 对 DNS 协议抓包,可以发现这个 IP 对应的 domain。

tcpdump port 53

可以发现这个域名是 accounts.google.com

我在容器中做了一些简单的测试:

  • 在容器里面是可以 ping 通 accounts.google.com 的。
  • 在容器里面可以正常 curl http://accounts.google.com 并且拿到 response。
  • 但是 curl https://accounts.google.com 必定会超时。
  • 问题出在 https? 容器内可以访问其他的一些 https 网站,比如 curl https://x.com,但是速度很慢
  • 我去 VM(容器所在的宿主机)curl https://accounts.google.com 发现是正常的。说明 Host 网络是 OK 的,也说明不是 Google 挂了(废话)。
在容器内访问 https://x.com 的抓包

到这里就是收集到的所有的信息了。其实答案就在本站的其他博文中,嘻嘻。读者可以推断出原因了吗?

答案

回顾 Docker (容器) 的原理 一文中网络的部分,容器发送包到 WAN 大致的路径是:容器内的 eth0 -> Host 对应的 veth 的另一头 -> docker0 bridge -> Host eth0 -> WAN。而因为在 Host 上的网络没有问题,所以最后一段 Host eth0 -> WAN 是没问题的。

通过抓包的现象可以看到,在 TCP 正常建立连接之后,如果是 HTTP 就很顺喜,如果是 HTTPS,在建立连接之后会卡住。从上图抓包可以看到(虽然 IP 部分打了码,但是可以通过 Flags 前面的字看出来是谁发回来的),从 443 端口发回来的包,只有 length = 0 的。所以猜测(只能猜吗?),从 443 端口发回来的包都因为 MTU 太大的原因被丢弃了。参考 有关 MTU 和 MSS 的一切

只能猜测吗?因为如果超过了 MTU 1500 的大小,那么丢包的可能是任何中间设备,所以我们看不到被丢弃的包的,现象就是对方的 443 端口没有发送任何东西回来导致超时。但是我们可以有以下理由这么猜:

  • 因为 HTTP 访问相同的 IP port 可以拿到正常响应,说明 4 层网络是通的,至少不是因为防火墙之类的问题;
  • HTTP 响应相对较小,HTTPS 和 HTTP 相对于 4 层来说有啥不同呢?只是中间多了一层 TLS 而已,TLS 在握手的过程中要交换很多信息,包括证书等。
  • 访问某些 HTTPS 网站是可以的,但是从抓包可以看出,中间也卡了很久,过了一段时间,对方才从 443 端口发回来数据。而且奇怪的是,明明对方要发送一连串的数据,却没有用 length 很长的 segment 发回来,而是发了几个很小的 segment。说明这些网站可能实现了 PMTUD.

证明

为什么会导致 MTU 太大,进而导致丢包呢?肯定是 TCP 的 MSS 协商出了问题。

既然 Host 上的网络没有问题,我就对比了 Docker 中的 interface 配置和 Host 上的 interface 配置。发现 Host 上的 MTU 设置为 1450,而 Docker 里面是默认的 1500. 于是就明白了:我们的 Host (即 virtual machine)运行在一个 Overlay 网络中。简单可以理解为,Host 收发的网络包,中间的网络设备要在上面添加额外的信息,添加之后,MTU 就会超过 1500,为了避免这个问题,就调小 interface 上的 MTU 值,这样,为“额外信息”预留出来空间,保证网络中的任何包大小都不超过 1500 bytes。但是我们自己搭建的 docker,没有单独去配置 interface 的 MTU,于是就会让 Docker 内的程序在建立 TCP 连接的时候,错误地认为自己的 MTU 是 1500,导致最终产生 MTU 大于 1500 bytes 的包。

Docker interface 和 Host interface MTU 对比

解决办法

我们可以用 MSS clamping 来解决这个问题:通过 iptables 将 TCP 握手的包中的 MSS 值强制修改成 1450 – 40:

然后就可以在 Docker 容器中正常访问这个 HTTPS 服务了。

另一个解决办法是,让 dockerd 启动的时候,指定 interface 的 mtu

 

CVE-2024-21626 从容器内逃逸到宿主机文件系统

最近很火的一个 CVE,核心问题是 docker (runc) 在运行用户的代码之前,会 O_CLOEXEC 关闭所有的 fd——这是正确的——但是运行用户代码之前,在 setcwd(2) 的时候,fd 还没有被关闭。这就导致 docker rundocker exec 的时候,去通过 -w 参数设置 working directory, 并且设置成一个还没有关闭的 fd ,就能拿到宿主机上的文件路径,从而进入到宿主机。

这个攻击有两个依赖:

  • 能够在容器内部执行代码;
  • 能够设置容器的 working directory (docker run, docker exec, 甚至 docker build 都可以)

演示

在一个全新的 Linux 机器上复现这个攻击。

环境准备

准备一个新的 VM,需要安装的依赖有:

  1. 依赖 golang 1.22 和 libseccomp-dev 来编译指定版本的 runc;
  2. 依赖 build-essential 编译 runc;
  3. 依赖 docker engine,指定版本的 runc;

第一步:按照官方文档安装最新版本的 docker。

第二步:替换 runc (最新版已经解决这个问题了)到旧版本,这里我们使用 v1.0.0-rc10. 编译脚本如下:

安装完成旧版本的 runc 之后需要重启 docker engine:sudo systemctl restart docker.

攻击演示

创建一个 Dockerfile:

编译这个 docker image: docker build . -t test

最后运行这个 docker image: docker run --rm -ti test.

可能一次运行不会成功,多运行几次会成功。

进入 container,此时 cwd 显示 .

通过相对路径,我们可以回到 Host 上面的 / 了:

打开 Host 上面的文件

如果我们安装运行 htop,会发现只有自己的容器里面的进程:

htop 只显示自己容器的 pid

但是如果我们改变当前容器的 fs root: chroot . ,再次运行 htop,就可以看到所有的进程了。

chroot ps 可以显示所有的 pid
htop 也可以显示所有的 pid

但是试了下发送 signal 开 kill 进程是不行的,我猜是因为 pid namespace 仍然是对进程隔离的?

甚至可以在容器内运行docker 命令,看到所有的 container。因为有了 docker binary 的路径(和权限,因为容器进程也是 root)和 docker socket 的路径。

在容器内 docker ps

相关链接:

 

推荐新加坡的餐厅:Ma Maison

没想到我会专门写一篇博客来推荐一家餐厅,哈哈。今天想写的餐厅叫 Ma Maison,经常和同事朋友去,食物比较好吃,价格也相对优惠。所以想专门推荐一下。

Ma Maison 是一家日本餐厅,但是有两种风格:Tonkatsu Ma Maison, 顾名思义,专门做炸猪排;Ma Maison 洋食屋,顾名思义是西餐风格。

Tonkatsu Ma Maison, 各种各样的炸猪排,非常好吃。套餐包括饮料(茶),米饭(白米饭或者糙米饭),猪排,卷心菜沙拉,味增汤。猪排炸的恰到好处,外酥里嫩。味增汤也很好喝。除了猪排之外,其他的食物都可以 refill,管饱。最早听说这家餐厅是同事 YX 告诉我的,他的评价是:回中国之后最想念的就是这里的炸猪排,在中国还没吃到这么好吃的。

Ma Maison 洋食屋,我们最早去的就是这家。也算是比较正宗的西餐:按照前菜(一般是汤),主菜(主菜配面包 or 米饭,还有一小团意大利面),甜品的顺序上菜。我们最喜欢吃的是牛排,如果当前的 Daily Lunch Set 是牛排的话,可以透过玻璃看到厨房里面火舌四起,很有观赏性。

一餐的价格包括服务费和消费税在 20新币 – 30新币左右,水(或者咖啡,茶)免费,湿巾免费(细节很好),在新加坡算是非常实惠的价格了。

全岛现在有很多分店了,樟宜机场也有。除了新加坡,马来西亚新山也有一家,在 Southkey: Tonkatsu by Ma Maison – The Mall Mid Valley Southkey。

Westgate 门店,图片来自官网 (但是其他图片是我自己拍的)
Tonkatsu 门店
新加坡 Ma Maison 位置

食物

洋食屋
Tonkatsu 猪扒饭

优惠

优惠策略很有意思,有好几种:

  • Daily Lunch Set: 工作日午餐可以以比较便宜的价格吃到一份套餐,每日的套餐不一样,可以从官网查看,比如 AnchorPoint 分店的每日午餐列表可见这里
  • 盖章:注册会员之后会有两张卡片和一个 $10 off 的优惠券。卡片使用来集章的,分成午餐和晚餐两种:
    • 午餐:每吃一顿午餐可以得到一个章,收集齐 10 个章可以兑换一张免费午餐券;很多店有 Double Chop Day,消费一顿午餐可以获得两个章。这样的话,相当于买5送1.
    • 晚餐:每吃一顿晚餐可以得到一个章,收集齐 10 个章可以兑换 10 张 $10 off 的券。
  • Lady’s Night: Group 里面有一位女生,可以享受 20% off 优惠;
  • Early Bird 优惠:午餐去的比较早可以享受。

每一家分店的政策可能不同。我觉得最核心的就是 Daily Lunch 和集章,已经兑换了 30 张 $10 off 晚餐券以及 4 张午餐券了(说明我光顾太多次啦,哈哈)。

集章的卡片和优惠券
兑换的免费午餐券
还是优惠券
过生日的时候邮寄给我们的贺卡,很贴心,其实也是一张优惠券