
最近有一个docker container在部署的时候好好的,跑了一阵子发现他好像连不出去外网。一开始怀疑是container内的逻辑问题,排查了一通发现并不是。因为网络请求使用的是域名,报错跟getaddrinfo相关,于是又怀疑是DNS问题。
于是进入docker container内进行Debug
docker exec -it <container-name-or-id> /bin/bash
ping 8.8.8.8 # timeout
curl http://example.com # failed
在contaienr中尝试ping与curl均失败,所以肯定是container网络不通的问题。
开始检查docker-compose yml文件,确认network是否存在:\
docker network inspect <network-name>
检查network也是存在的,删除重建,问题依然存在。有点麻烦。
1. Docker的默认网络模型


本图使用Nano Banana Pro生成
- NAT (iptables MASQUERADE) is omitted for simplicity*
可以看到container中的网络请求,会通过docker内的eth0通过veth转发到host machine的docker0 bridge network。接着在forward到host machine的eth0,最终把请求发出去。
所以现在container中发不了请求,但host machine网络通畅,有可能是ip forwarding没打开。
sysctl net.ipv4.ip_forward
// net.ipv4.ip_forward = 0
确认问题是Linux内核的ip forwarding被关了。手动打开ip forwarding
sudo sysctl -w net.ipv4.ip_forward=1
再尝试从container中发送请求,一切正常,problem sovled!
2. 谁把IP Forwarding关闭了?
不少云服务厂商提供的主机都会自带一些安全功能,可能会主动关闭IP Forwarding,我们可以检查一下sysctl的配置:
/etc/sysctl.conf
/etc/sysctl.d/*.conf
我发现/etc/sysctl.conf中有net.ipv4.ip_forward = 0的配置。这样的话,只要机器重启,ip forwarding还是会被自动关闭。
所以我们还需要手动将这些配置文件修改,可以将对应行注释掉。
3. sysctl
经常使用macOS的小伙伴们对sysctl这个工具一定不陌生,这是从BSD时代就有的东西,Linux的sysctl设计理念也是源自BSD Unix。macOS的XNU内核里就包含了BSD内核代码(Mach+BSD),所以在macOS上查看ip forwarding可以这样:
sysctl net.inet.ip.forwarding
# net.inet.ip.forwarding: 1
无论BSD还是Linux内核,这个sysctl工具都只是一个内核配置的读写工具。
那么Linux内核中真正执行ip forwarding的地方在哪里呢?让我们打开Linux源码仓库👉https://github.com/torvalds/linux
上面说过sysctl只是读写配置,其实它也是由多个部分组成,我们在terminal直接操作的sysctl命令多是由用户态工具提供的,标准Linux发行版都有。至于内核,则由各个子系统自行读取对应的配置。
具体到ip forwarding这个例子,我们需要看net子系统。最终的ip forwarding逻辑的代码在net/ipv4/ip_forward.c,其中的int ip_forward(struct sk_buff *skb)函数就是把包转发到下一跳的逻辑。里面进行了大量的检查,最终执行forward,但是这个函数本身并不检查sysctl配置,所以我们且按下不表。
真正检查配置并执行该方法的地方,位于net/ipv4/route.c文件中。在这个函数:
static enum skb_drop_reason
ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
dscp_t dscp, struct net_device *dev,
struct fib_result *res)
使用IN_DEV_FORWARD(in_dev)来判断是否开启了ip forwarding。如果没有开启,会走到goto no_route,如果开启,则走到goto make_route。
no_route处理就很简单,把返回值置为RTN_UNREACHABLE,告诉上层此路不通,对应我们在docker container中的现象就是无网络。
make_route会调用如下函数:
static struct rtable *__mkroute_output(const struct fib_result *res,
const struct flowi4 *fl4, int orig_oif,
struct net_device *dev_out,
unsigned int flags)
然后把包转发给下一跳,这个函数里使用一个struct rtable记录ip_forward这个函数指针,最后在net/ipv4/ip_input.c的ip_rcv_finish()函数中,使用dst_input(skb)真正调用到ip_forward()这个函数。
rtable是Linux内核中常见的一个设计,把数据和行为封装到一个struct里面,后续执行的流程不需要做if-else判断,只需要统一使用调度方法即可。
dst_input(skb);
dst_output(net, sk, skb);
在我们排查的这个case中,我们只关注ip_forward。但是在net子系统中,该设计还能扩展支持ip_local_deliver, ip6_input, xfrm_input等等功能。很有意思。
4. What's next?
本来我查到docker container无网络,只要执行sudo sysctl -w net.ipv4.ip_forward=1就能临时解决这个问题了。但是研究了一下发现原来还有文件配置项,规避了系统重启让我的docker container再次无网络的问题。
顺便因为发现sysctl有点意思,就进一步研究Linux源码,发现它的设计与XNU有些相似又有很大不通。如今Linux内核已经是一个非常庞大的代码库,XNU其实也是的。rtable和sysctl的设计能很好地在多个子系统之间解耦,提供很高的可扩展性。当然,如果我们在一个足够简单的项目里是没必要使用这种级别的设计的,反而会引入更高的理解成本,造成过度设计。
但了解内核的代码总能带来新的惊喜,蛮有趣的。以前我阅读XNU源码的时候,只能通用我人工手动一点点搜索代码,判断调用入口与调用链来理解。现在有强大的LLM,这个过程快了很多。AI时代,阅读内核代码将变得无比简单。
