Skip to content

fix(sandbox): announce guest MAC changes via arp_notify - #409

Merged
yingdi-shan merged 2 commits into
kvcache-ai:mainfrom
dushulin:fix/fixed-guest-mac
Oct 22, 2026
Merged

yingdi-shan merged 2 commits into
kvcache-ai:mainfrom
dushulin:fix/fixed-guest-mac

Conversation

@dushulin

@dushulin dushulin commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

冷启动(start_fresh)时在 kernel cmdline 的 ip= 参数之后追加 sysctl.net.ipv4.conf.all.arp_notify=1,让 guest 内核在 eth0 的 MAC 变化时主动发 gratuitous ARP。同时在 docs/src/internals/networking.md 的 Address Plan 一节补充说明。

本 PR 最初的版本是给 eth0 配置固定的 guest_mac。按 #407 里维护者的建议改为 arp_notify,固定 MAC 的改动已撤回,网卡仍是 guest_mac = None。对 main 的净改动只有一个常量、一行 append_boot_arg 和一段文档。

Why

详见 #407,简述如下:

  • start_fresh 添加 eth0 时没有 guest_mac,guest 的 virtio_net 随机分配 MAC。
  • 用户镜像经 pivot-init 交给 systemd(>= 242)后,systemd-udevd 按自带的 99-default.link(MACAddressPolicy=persistent)把随机 MAC 改写掉,且不发 gratuitous ARP。
  • slot netns 里 tap0 上 169.254.0.21 的 neighbour 表项仍指向启动时的 MAC,host 发往 guest 的帧被丢弃。proxy 的连接一直等到 5s connect timeout 后返回 502,直到表项老化、重新解析为止(复现中约 43s)。
  • 是否触发取决于 guest 在 udev 改写之后多快主动发出单播流量,所以表现为"按镜像聚集、看起来随机"的 502。

arp_notify 的取值是 MAXCONF(all, dev),所以 all=1 对 eth0 生效。MAC 变化(NETDEV_CHANGEADDR)时 guest 内核发一个 gratuitous ARP,tap0 上的表项立即被覆盖成新 MAC,不必等老化。

相比固定 MAC:

  • 还能覆盖固定 MAC 挡不住的改写。systemd v255 起 MACAddressPolicy=random 也会改写内核报告为 NET_ADDR_PERM 的 MAC;.link 里显式写 MACAddress= 同样会改。
  • guest 可见的 MAC 和 snapshot 里的设备状态都不变。

Related issue

Closes #407

Scope and non-goals

Design and behavior changes

  • 参数位于 ip= 之后、custom extension extraBootArgs 之前,extension 仍可以追加或覆盖。
  • arp_notify 打开后,guest 在 link up 和 MAC 变化时各多发一个 gratuitous ARP。这些帧只出现在 slot 私有的 TAP 链路上。

Compatibility and operations

  • Public API or generated protocol:无变化。
  • Configuration or defaults:无新增配置项;对自定义了 firecracker.boot_args 的部署同样生效。
  • Snapshot manifest, artifact layout, or storage format:格式不变。变更之后拍的快照里,guest 内核的 arp_notify 为 1;已有快照不变。
  • Upgrade and rollback:只影响冷启动。已暂停的沙箱和已提交的快照按保存的状态恢复,与现在一致。回滚不需要任何迁移。
  • Host requirements, permissions, ports, or dependencies:无。sysctl.* 形式的 cmdline 需要 guest kernel >= 5.8(默认的 vmlinux-6.1.175 满足);更老的 guest 内核会忽略这个参数,行为与现在相同。

Validation

  • make fmt
  • make clippy
  • make test-unit(只跑了其中的 cargo test -p agentenv --lib,见下)
  • Relevant Rust integration tests
  • make -C services test (required when services/ changes)
  • Generated clients/server regenerated with the documented make target
  • Documentation updated
  • Benchmarks or performance comparison completed

Commands and results:

$ make fmt && make clippy
ok

$ cargo test -p agentenv --lib
test result: FAILED. 1013 passed; 3 failed; 4 ignored
  privileges::tests::terminate_unclaimed_child_kills_and_reaps_process      # also fails on main in this environment
  sandbox::envd::user::tests::passwd_append_survives_a_locked_concurrent_rewrite  # also fails on main in this environment
  sandbox::firecracker::sandbox::tests::enabled_firecracker_logging_captures_cold_and_warm_output
    # flaky under full parallel load (1s wait_for_ready on /bin/echo); passes 3/3 when run alone

线上验证:在一个下游部署里,把一台节点换成包含此改动的构建做 canary,环境与 #407 相同(KVM,Firecracker 1.15.1-patch-v1,guest kernel vmlinux-6.1.175)。使用 #407 中的 Image A(Ubuntu 24.04,systemd 255),方法与 #407 一致:在 slot netns 里运行 ip -timestamp monitor neigh,每 250 ms 请求一次 /proxy/health。

修改前(#407 复现) 修改后
冷启动次数 1 17(1 次单个,加 2 轮各 8 个并发)
proxy 探测 4×204 之后 154×502(每次 5.00 s,约 43 s) 4080 次全部 204,最大 9 ms
tap0 neighbour 启动时 MAC → STALE → FAILED → 新 MAC 启动时 MAC 在约 1 s 内被覆盖为新 MAC,之后没有 PROBE / FAILED
guest arp_notify(all / default / eth0) 0 / 0 / 0 1 / 0 / 0
guest eth0 addr_assign_type 3(NET_ADDR_SET,已被 udev 改写) 3(udev 照常改写,符合预期)

修改后,guest 的 /proc/cmdline 末尾为 ... ip=169.254.0.21::169.254.0.22:255.255.255.252:instance:eth0:off:<dns> sysctl.net.ipv4.conf.all.arp_notify=1。16 个并发沙箱的 tap0 表项都是同一模式:随机 MAC → udev 改写后的 MAC,切换间隔约 1 s。

另外在同一台节点上做了一组只改 arp_notify 的对照:在 guest 内用 ip link set eth0 address ...(与 udev 同样走 rtnetlink)改 MAC。arp_notify=0 时 39 s 内 156 次 502(每次 5 s),表项经历 STALE → PROBE → FAILED 后才切到新 MAC;只把 all 设为 1、eth0 保持 0 时,240 次全部 204,最大 5 ms,表项立即被覆盖。

Skipped checks and reasons:

  • make test-unit 中需要 capability runner 的 --ignored 测试,以及 Rust 集成测试:本地没有运行(需要 root / KVM 环境)。改动只是一个 boot arg,已用上面的 canary 做了端到端验证。
  • 没有改动 services/,也没有生成代码;不涉及性能。

Risks and reviewer notes

  • 如果用户镜像通过 sysctl.d 把 net.ipv4.conf.all.arp_notify 显式设回 0,并且在 udev 改写 MAC 之前生效,这个修复就不起作用。默认的 systemd 发行版不会这样配置。
  • 重点看 src/sandbox/firecracker/sandbox.rs 中 start_fresh 的 append_boot_arg 调用位置。

Checklist

  • The PR contains one coherent change and no unrelated formatting or refactoring.
  • New behavior is covered by tests, or I explained why testing is impractical.(行为依赖 Firecracker + 带 systemd 的 guest,单元测试无法覆盖,用上面的 canary 验证)
  • Logs and examples contain no credentials, tokens, or private registry information.
  • I did not manually edit generated code without updating its source and regenerating it.

Cold boots attached eth0 without guest_mac, so virtio_net picked a random
MAC. Guest images running systemd >= 242 with udevd rewrite such a MAC via
99-default.link (MACAddressPolicy=persistent) without a gratuitous ARP, and
the host tap0 neighbour entry kept the boot MAC until it aged out; proxied
connects hit the 5s timeout and returned 502 for up to ~40s.

A configured MAC is exposed via VIRTIO_NET_F_MAC and reported as
NET_ADDR_PERM, which udev leaves alone. Restore paths load the NIC from
vm_state.bin and are unchanged.

Closes kvcache-ai#407

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ OpenCodeReview: Review complete: 0 finding(s) across 1 selected item(s).

Replace the fixed guest MAC with sysctl.net.ipv4.conf.all.arp_notify=1 on
the cold-boot kernel command line. When guest userspace changes eth0's MAC
(systemd-udevd's MACAddressPolicy=persistent rewriting the random boot MAC,
but also MACAddressPolicy=random or an explicit MACAddress=, which a fixed
MAC does not prevent), the guest kernel now sends a gratuitous ARP and the
host tap0 neighbour entry follows the new MAC immediately instead of going
stale for ~40s.

arp_notify is MAXCONF(all, dev), so all=1 covers eth0. The argument is
appended in start_fresh rather than DEFAULT_BOOT_ARGS so it also applies
when firecracker.boot_args is overridden. Restore and fork are unchanged.

Closes kvcache-ai#407

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@dushulin dushulin changed the title fix(sandbox): set a fixed guest MAC so udev does not rewrite eth0 fix(sandbox): announce guest MAC changes via arp_notify Oct 21, 2026

@yingdi-shan yingdi-shan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for your contribution!

@yingdi-shan
yingdi-shan merged commit b4ee1b7 into kvcache-ai:main Oct 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(network): guest udev rewrites the random eth0 MAC on cold boot, stale host neighbour makes the proxy 502 for ~40s

2 participants