[PR #409] [MERGED] fix(sandbox): announce guest MAC changes via arp_notify #412

Closed
opened 2026-10-29 14:23:06 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/kvcache-ai/AgentENV/pull/409
Author: @dushulin
Created: 10/21/2026
Status: ✅ Merged
Merged: 10/22/2026
Merged by: @yingdi-shan

Base: main ← Head: fix/fixed-guest-mac


📝 Commits (2)

  • e9cc9a9 fix(sandbox): set a fixed guest MAC so udev does not rewrite eth0
  • 1898468 fix(sandbox): announce guest MAC changes via arp_notify

📊 Changes

2 files changed (+6 additions, -0 deletions)

View changed files

📝 docs/src/internals/networking.md (+2 -0)
📝 src/sandbox/firecracker/sandbox.rs (+4 -0)

📄 Description

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 里的设备状态都不变。

Closes #407

Scope and non-goals

  • 只在 start_fresh 追加一个 boot arg。
  • 写在代码里而不是 DEFAULT_BOOT_ARGS:配置文件不会叠加 config/default.toml,自定义了 firecracker.boot_args 的部署也需要这个参数。
  • 不改 restore / fork 路径。restore 不会改 MAC,TAP 重建后 host 会重新 ARP,所以不存在过期表项。
  • 不调 tap0 的 neighbour 参数,不往用户 rootfs 注入 .link 文件,也不加静态 neighbour 表项(理由见 #407 的 Alternatives considered)。

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.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/kvcache-ai/AgentENV/pull/409 **Author:** [@dushulin](https://github.com/dushulin) **Created:** 10/21/2026 **Status:** ✅ Merged **Merged:** 10/22/2026 **Merged by:** [@yingdi-shan](https://github.com/yingdi-shan) **Base:** `main` ← **Head:** `fix/fixed-guest-mac` --- ### 📝 Commits (2) - [`e9cc9a9`](https://github.com/kvcache-ai/AgentENV/commit/e9cc9a98a4c88900a86b8bbd20a99a38da6ce503) fix(sandbox): set a fixed guest MAC so udev does not rewrite eth0 - [`1898468`](https://github.com/kvcache-ai/AgentENV/commit/189846885b8727e650b66a0991425e7dbfe103dc) fix(sandbox): announce guest MAC changes via arp_notify ### 📊 Changes **2 files changed** (+6 additions, -0 deletions) <details> <summary>View changed files</summary> 📝 `docs/src/internals/networking.md` (+2 -0) 📝 `src/sandbox/firecracker/sandbox.rs` (+4 -0) </details> ### 📄 Description ## 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 - 只在 `start_fresh` 追加一个 boot arg。 - 写在代码里而不是 `DEFAULT_BOOT_ARGS`:配置文件不会叠加 `config/default.toml`,自定义了 `firecracker.boot_args` 的部署也需要这个参数。 - 不改 restore / fork 路径。restore 不会改 MAC,TAP 重建后 host 会重新 ARP,所以不存在过期表项。 - 不调 `tap0` 的 neighbour 参数,不往用户 rootfs 注入 `.link` 文件,也不加静态 neighbour 表项(理由见 #407 的 Alternatives considered)。 ## 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 - [x] `make fmt` - [x] `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 - [x] Documentation updated - [ ] Benchmarks or performance comparison completed Commands and results: ```text $ 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 - [x] The PR contains one coherent change and no unrelated formatting or refactoring. - [x] New behavior is covered by tests, or I explained why testing is impractical.(行为依赖 Firecracker + 带 systemd 的 guest,单元测试无法覆盖,用上面的 canary 验证) - [x] Logs and examples contain no credentials, tokens, or private registry information. - [x] I did not manually edit generated code without updating its source and regenerating it. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-29 14:23:06 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kvcache-ai/AgentENV#412
No description provided.