|
| 1 | +F-Stack 2.0 Preview 7: MTU Change Support — Breaking the 1500 Hard Limit for 9000 Jumbo Frames, and the Two Pitfalls Along the Way |
| 2 | + |
| 3 | +1. What this feature does and its key characteristics |
| 4 | + |
| 5 | +Let's get straight to the point: F-Stack's DPDK-controlled NIC was previously locked at MTU 1500. This project turned the status quo — "decreasing MTU works, increasing MTU (jumbo frames) is completely unsupported" — into full support: set any MTU between 1500 and 9000 at startup via configuration, change it at runtime through the standard SIOCSIFMTU ioctl, with the protocol stack and DPDK hardware kept in sync. |
| 6 | + |
| 7 | +First, what the pre-change baseline looked like. A conclusive research was done in mid-July 2026 (16 documents, `docs/mtu_change_spec/zh_cn/00~15`); the three-way cross-verified conclusion was "partial support": |
| 8 | + |
| 9 | +- Decreasing MTU (≤1500): works out of the box. `ff_ioctl(SIOCSIFMTU)` goes through FreeBSD `ether_ioctl`, which writes values ≤ ETHERMTU directly into `if_mtu`. |
| 10 | +- Increasing MTU (>1500, jumbo): double-blocked. At the protocol-stack layer, `ether_ioctl` hardcodes `EINVAL` for `ifr_mtu > ETHERMTU(1500)`; at the DPDK hardware layer there is no MTU wiring at all — no `rte_eth_dev_set_mtu` call, the mbuf pool is fixed at `RTE_MBUF_DEFAULT_BUF_SIZE` (2048 usable dataroom), and `rxmode` has no jumbo/scatter configuration. |
| 11 | + |
| 12 | +This conclusion matched F-Stack official issues #239/#490/#720 exactly: the jumbo support issue had been OPEN for a long time, and the maintainers explicitly replied "mtu cannot exceed 1500". So this project was not about applying patches — it was about connecting three broken layers: the protocol stack, the DPDK hardware layer, and the configuration system. The three keywords of this rework: |
| 13 | + |
| 14 | +- **Software-hardware linkage**: one MTU with three views — the protocol-stack `if_mtu`, the DPDK port MTU, and the mbuf pool capacity. All three must agree; startup fails if any mismatches. |
| 15 | +- **Multi-process division of labor**: the DPDK physical port is shared state; the primary alone owns the hardware (sets both soft and hard), while secondaries set only their own process's software MTU — zero IPC between processes. |
| 16 | +- **Zero-regression commitment**: the new `mtu_enable` master switch; when disabled, legacy configs keep the exact MTU 1500, 2176B mbufs, and ioctl behavior. |
| 17 | + |
| 18 | +Scale numbers: 16 research documents (landed 2026-07-17) → 7 implementation spec documents (07-21) → M1~M5 code implementation, 14 commits done in one day (07-21) → physical-machine validation + English translation (07-22). 59 unit tests passed (including 8 new MTU tests); IPv4/IPv6 8500-byte jumbo frames verified bidirectionally on a physical machine. |
| 19 | + |
| 20 | +2. Main applicable scenarios |
| 21 | + |
| 22 | +2.1 Large-packet throughput optimization |
| 23 | + |
| 24 | +This is the most direct motivation. The historical conclusion of issue #1033 already noted that the performance drop in large-packet scenarios is partly caused by IP fragmentation forced by MTU 1500. With jumbo frames enabled, an 8500-byte packet is no longer split into 6 fragments, and fragmentation/reassembly overhead drops to zero. Suitable for internal high-speed networks, large data-block transfers, and storage networks where the link MTU can be unified. |
| 25 | + |
| 26 | +2.2 Tunnel/overlay scenarios that need to flexibly reduce MTU |
| 27 | + |
| 28 | +Reducing MTU always worked, but after this rework all runtime changes go through the standard SIOCSIFMTU semantics — a single `ff_ifconfig f-stack-0 mtu <N>` changes both the protocol stack and the hardware. VXLAN/GRE/tunnel overlay scenarios often need MTU pushed down to the 1400 range to leave room for tunnel headers. |
| 29 | + |
| 30 | +2.3 Unified jumbo frames in multi-process production deployments |
| 31 | + |
| 32 | +Under the classic 1 primary + N secondary deployment, each process sets MTU once to stay consistent (primary manages hardware, secondaries manage their own software views) — no cross-process coordination mechanism needed. |
| 33 | + |
| 34 | +2.4 Scenarios that are not a good fit |
| 35 | + |
| 36 | +- The link peer does not support jumbo: PMDs like virtio have limited jumbo capability, and the underlying NIC/vSwitch/peer must all support it; otherwise large frames get dropped or fragmented mid-path — code support is useless if the link doesn't follow. |
| 37 | +- Deep MTU linkage between KNI and the kernel stack: KNI veth interface MTU linkage is out of scope for this phase (only verified that both work independently after the mutual exclusion was lifted). |
| 38 | +- Transaction-level cross-process MTU consistency: this phase explicitly skips IPC coordination; a process that never sets MTU keeps its old software value — a known usage constraint. |
| 39 | + |
| 40 | +3. Architectural characteristics |
| 41 | + |
| 42 | +3.1 The three broken layers before the rework |
| 43 | + |
| 44 | +The root-cause diagram drawn during the research phase — the starting point for understanding every change: |
| 45 | + |
| 46 | +``` |
| 47 | +┌────────────────────────────────────────────────┐ |
| 48 | +│ Application: ff_ifconfig f-stack-0 mtu 9000 │ |
| 49 | +│ ff_ioctl(SIOCSIFMTU) │ |
| 50 | +└───────────────────┬────────────────────────────┘ |
| 51 | + ▼ |
| 52 | +┌────────────────────────────────────────────────┐ |
| 53 | +│ Protocol stack (ff_veth.c + trimmed FreeBSD) │ |
| 54 | +│ ff_veth_ioctl → ether_ioctl │ |
| 55 | +│ if (ifr_mtu > ETHERMTU) → EINVAL ← hardcoded 1500│ |
| 56 | +│ if_mtu written only to ifnet, no HW linkage │ |
| 57 | +└───────────────────┬────────────────────────────┘ |
| 58 | + ▼(gap: even success is not pushed to HW) |
| 59 | +┌────────────────────────────────────────────────┐ |
| 60 | +│ DPDK hardware layer (ff_dpdk_if.c) │ |
| 61 | +│ No rte_eth_dev_set_mtu call │ |
| 62 | +│ rxmode has no mtu/max_rx_pkt_len/jumbo config │ |
| 63 | +│ mbuf pool fixed 2048 dataroom ← can't fit big frames│ |
| 64 | +└────────────────────────────────────────────────┘ |
| 65 | +``` |
| 66 | + |
| 67 | +3.2 The SIOCSIFMTU path after the rework (split by process role) |
| 68 | + |
| 69 | +``` |
| 70 | + ff_ifconfig mtu <N> |
| 71 | + │ |
| 72 | + ff_veth_ioctl(SIOCSIFMTU) ← intercepted, no longer delegates to ether_ioctl |
| 73 | + │ |
| 74 | + rte_eal_process_type() role check |
| 75 | + ┌──────────┴──────────┐ |
| 76 | + ▼ ▼ |
| 77 | + primary secondary |
| 78 | + ff_dpdk_if_set_mtu() if_setmtu() software-only |
| 79 | + ① -EBUSY → stop port │(never touches DPDK port control APIs) |
| 80 | + ② rte_eth_dev_set_mtu │ |
| 81 | + ③ rte_eth_dev_get_mtu readback│ |
| 82 | + ④ rollback on failure: │ |
| 83 | + restore old MTU │ |
| 84 | + + restart port │ |
| 85 | + ⑤ on success → if_setmtu(ifp)│ |
| 86 | + ⑥ if_notifymtu(ifp) │ |
| 87 | + ├ nd6_setmtu (IPv6 sync) │ |
| 88 | + └ rt_updatemtu (route sync)│ |
| 89 | +``` |
| 90 | + |
| 91 | +Key design constraint: `ff_veth.c` does not include `rte_ethdev.h`; all hardware operations go through the opaque interfaces `ff_dpdk_if_get_mtu/set_mtu/get_mtu_capability` in `ff_dpdk_if.h`, and DPDK negative errno values are converted to BSD positive errno via `ff_dpdk_errno_to_bsd()`. |
| 92 | + |
| 93 | +3.3 The two mbuf carrying modes |
| 94 | + |
| 95 | +``` |
| 96 | +large mode scatter mode |
| 97 | +┌──────────────────────────┐ ┌──────────┐┌──────────┐ |
| 98 | +│ single mbuf, data_room │ │ standard ││ standard │→ ... |
| 99 | +│ sized by HEADROOM+ │ │ mbuf ││ mbuf │ |
| 100 | +│ max_mtu+L2_overhead │ │ 2048B ││ 2048B │ |
| 101 | +└──────────────────────────┘ └────┬─────┘└────┬─────┘ |
| 102 | + more memory, simpler path │ RX_OFFLOAD_SCATTER |
| 103 | + │ multi-seg chaining |
| 104 | + ▼ |
| 105 | + less memory, but requires PMD |
| 106 | + scatter + multi_segs support, |
| 107 | + and the multi-seg mbuf |
| 108 | + conversion path must be verified |
| 109 | +``` |
| 110 | + |
| 111 | +In large mode `data_room_size = align(HEADROOM + max_mtu + L2_overhead)`; exceeding UINT16_MAX must fail deterministically, so `max_mtu=65535` is unavailable in large mode. Scatter mode keeps standard mbufs, configurable up to 65535 but subject to PMD capability. |
| 112 | + |
| 113 | +4. What was reworked and what problems were hit |
| 114 | + |
| 115 | +The rest is a bit dry; skip this section if you don't need the implementation details and jump straight to Section 5. |
| 116 | + |
| 117 | +4.1 Research conclusions turned directly into requirement decisions (D-MTU-01~06) |
| 118 | + |
| 119 | +The research wrapped up with 6 decisions, all carried through the implementation: |
| 120 | + |
| 121 | +| ID | Decision | |
| 122 | +|---|---| |
| 123 | +| D-MTU-01 | Implement both large and scatter mbuf modes, selected by configuration | |
| 124 | +| D-MTU-02 | `max_mtu` is configurable; defaults to 9000 when the feature is enabled | |
| 125 | +| D-MTU-05 | **Do not modify the freebsd/ tree**; intercept SIOCSIFMTU in lib/ff_veth.c to bypass ether_ioctl's 1500 hard check | |
| 126 | +| D-MTU-06 | With `mtu_enable=0`, behavior stays identical to the old version (MTU 1500, 2176B mbufs, ioctl unchanged) | |
| 127 | + |
| 128 | +D-MTU-05 deserves one more sentence: the ETHERMTU hard check in `ether_ioctl` is a general FreeBSD semantic. F-Stack's choice was not to change it inside the trimmed stack, but to intercept SIOCSIFMTU in the ff_veth driver layer and handle it there. This keeps the freebsd/ subtree untouched — no new patch maintenance burden when the FreeBSD baseline is upgraded. |
| 129 | + |
| 130 | +4.2 Code implementation (M1~M5, 14 commits in one day, 2026-07-21) |
| 131 | + |
| 132 | +| Milestone | commit | Content | |
| 133 | +|---|---|---| |
| 134 | +| M1 | 97452db34 | Config parsing & validation: enum ff_mbuf_mode, ff_port_cfg.mtu, dpdk.{mtu_enable,max_mtu,mbuf_mode}; strict strtoul parsing; cross-field validation; 8 unit tests + 7 fixtures | |
| 135 | +| M2 | eec178902 | DPDK layer: ff_mtu_data_room_size() alignment, large-mode data_room sizing, rxmode.mtu + PMD capability check + scatter offload, set/get_mtu readback | |
| 136 | +| M3 | 0849f9f3a | ff_veth integration: opaque MTU API, DPDK errno→BSD errno conversion, SIOCSIFMTU interception (primary soft+hard / secondary soft only), initial if_mtu read from hardware at startup | |
| 137 | +| M4 | ef20b1abf | EBUSY state machine: -EBUSY → stop/set_mtu/get_mtu/start; rollback restore old MTU + restart port on failure | |
| 138 | +| M5 | 314df8a0a | Integration tests + architecture doc updates | |
| 139 | +| Wrap-up | 0f8f6991e / 332abf997 / 4c30d118f | magic numbers → named macros, SIOCSIFMTU handler fall-through dedup, bool→uint8_t clean-build fix | |
| 140 | + |
| 141 | +One iron rule for config parsing: no `atoi()`. The new `ff_parse_u16/ff_parse_mbuf_mode` do strict parsing (strtoul + errno/endptr/range checks); any illegal configuration fails loudly at startup instead of silently falling back to 1500. `mtu_enable=0` combined with `mtu>1500` fails parsing outright with a hint to enable the feature first. |
| 142 | + |
| 143 | +4.3 Problem 1: IPv6 jumbo replies fragmented at 1448 bytes (the biggest pitfall) |
| 144 | + |
| 145 | +During physical-machine validation after the feature went live, a bizarre symptom appeared: **IPv4 jumbo worked bidirectionally, but IPv6 received an 8500-byte ping fine while the reply was split into 6 fragments of 1448 bytes**. |
| 146 | + |
| 147 | +Back-calculation from the capture: 6 fragments = 1448×5 + 1268 = 8508, matching the fragmentation formula `len = (mtu - 40 - 8) & ~7`, which solves to mtu=1500 — the stack had if_mtu set to 9000, yet IPv6 fragmentation still used MTU 1500. |
| 148 | + |
| 149 | +The root-cause chain spans 12 file:line references (full analysis in `15-ipv6-jumbo-frame-fragmentation-analysis.md`); the one-sentence summary: |
| 150 | + |
| 151 | +``` |
| 152 | +ether_ifattach(if_mtu=1500) → nd6_ifattach → nd6_setmtu0(ndi->maxmtu=1500) |
| 153 | + ↓ |
| 154 | +ff_veth if_setmtu(9000) → if_mtu=9000, but [nd6_setmtu not called] → ndi->maxmtu stuck at 1500 |
| 155 | + ↓ |
| 156 | +IN6_LINKMTU = min semantics → maxmtu(1500) < if_mtu(9000) → returns 1500 |
| 157 | + ↓ |
| 158 | +route nh_mtu=1500 → ip6_getpmtu → ip6_calcmtu → fragment len=(1500-48)&~7=1448 |
| 159 | +``` |
| 160 | + |
| 161 | +The real pitfall: `if_setmtu()` only writes `ifp->if_mtu` — **it does not notify protocol families**. The standard FreeBSD kernel path `ifhwioctl → if_setmtu → if_notifymtu → nd6_setmtu + rt_updatemtu` synchronizes IPv6's `ndi->maxmtu` and the route MTU, but F-Stack took the "call if_setmtu directly" shortcut to bypass ether_ioctl's 1500 hard check and dropped the `if_notifymtu` step. The IPv4 output path judges fragmentation with `ifp->if_mtu` directly, so it was unaffected; IPv6 goes through IN6_LINKMTU and got hit. |
| 162 | + |
| 163 | +The fix (commit 0f25ac495, +163/-1): both paths in `ff_veth.c` — startup initialization and runtime SIOCSIFMTU — now call `if_notifymtu(ifp)` after `if_setmtu`, aligning with standard kernel semantics. Physical-machine retest: `ping6 -M do -s 8500` replies are no longer fragmented; IPv6 jumbo works bidirectionally. |
| 164 | + |
| 165 | +[Note 1] This pitfall has general value: in FreeBSD, "changing if_mtu" and "notifying protocol families" are two separate things. Any code that bypasses the standard ifhwioctl path and calls if_setmtu directly (driver init, ioctl interception) must add if_notifymtu itself; otherwise IPv6's nd_ifinfo and route nh_mtu stay at the old values forever. Incidentally, the secondary cause — a router advertisement lowering linkmtu — was also investigated; runtime verification confirmed linkmtu=0 (not affected by RA), so the primary-cause fix closed the loop. |
| 166 | + |
| 167 | +4.4 Problem 2: the KNI/MTU mutual exclusion — banned first, lifted later |
| 168 | + |
| 169 | +The initial requirement R-MTU-009 agreed: when `mtu_enable=1` and KNI/kernel coexistence are both enabled, reject at configuration time with EOPNOTSUPP — the rationale being the risk of dual-stack MTU inconsistency. |
| 170 | + |
| 171 | +Physical-machine testing overturned this conservative decision (commit 989f1d2da, +1/-6): `mtu_enable=1` coexisting with `kni.enable=1` works perfectly — with veth0 MTU=1500 the kernel stack sends and receives normally with defragmentation, and with `ifconfig veth0 mtu 9000` the KNI path handles jumbo frames fine too. The exclusion was an unnecessary restriction; after lifting it, KNI users can also use jumbo frames. |
| 172 | + |
| 173 | +[Note 2] This back-and-forth shows that conservative spec-stage constraints deserve re-examination after real measurements: an EOPNOTSUPP rejection is the safest wording, but if measurements show coexistence is fine, lifting the exclusion is friendlier to users than maintaining a "paper risk". |
| 174 | + |
| 175 | +4.5 Other engineering details |
| 176 | + |
| 177 | +- **EBUSY state machine**: `rte_eth_dev_set_mtu` may return -EBUSY for a running port; M4 implements stop/set_mtu/get_mtu/start within the primary process, with rollback restoring the old MTU and restarting the port on failure; secondaries simply return 0 without touching hardware. |
| 178 | +- **Multi-process consistency by usage convention**: zero IPC, zero transactions, zero message rings between processes; each process triggers SIOCSIFMTU once. A process that never sets MTU keeps its old software value (a known constraint, explicitly stated in the spec). |
| 179 | +- **Gates**: -Werror build, git-diff-verified zero changes to the freebsd/ tree, no rte_eth* references in ff_veth.c, no atoi, no IPC residue, zero regression across all paths with mtu_enable=0 — all six PASS. |
| 180 | + |
| 181 | +5. How to use it, how to configure it, and the results |
| 182 | + |
| 183 | +5.1 Configuration |
| 184 | + |
| 185 | +```ini |
| 186 | +[dpdk] |
| 187 | +mtu_enable=1 # feature master switch, default 0 (behavior identical to the old version when off) |
| 188 | +max_mtu=9000 # runtime MTU upper bound + large-mode pool pre-allocation basis, default 9000 |
| 189 | +mbuf_mode=large # large: one mbuf carries jumbo; scatter: standard mbuf multi-seg chain |
| 190 | + |
| 191 | +[port0] |
| 192 | +mtu=9000 # protocol-stack + hardware MTU at port startup, default 1500 |
| 193 | +``` |
| 194 | + |
| 195 | +Constraint quick reference: |
| 196 | + |
| 197 | +- `mtu_enable=0` combined with `mtu>1500`: parsing fails, with a hint to enable the MTU feature first |
| 198 | +- In large mode `max_mtu` is bounded by `data_room_size <= UINT16_MAX` (65535 unavailable); scatter mode can go to 65535 but is subject to PMD capability |
| 199 | +- Illegal configurations fail at startup; there is no silent fallback |
| 200 | + |
| 201 | +5.2 Runtime changes |
| 202 | + |
| 203 | +```bash |
| 204 | +# primary process: changes soft + hardware together (including EBUSY → stop/set/start) |
| 205 | +ff_ifconfig -p 0 f-stack-0 mtu 9000 |
| 206 | + |
| 207 | +# query |
| 208 | +ff_ifconfig -p 0 f-stack-0 |
| 209 | +f-stack-0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 9000 |
| 210 | +``` |
| 211 | + |
| 212 | +Multi-process convention: a change on the primary updates both soft and hardware; each secondary must trigger its own change (updating only its own software view). This is a usage convention, not a bug; cross-process IPC coordination is a future milestone outside this scope. |
| 213 | + |
| 214 | +5.3 Results (physical-machine measurements) |
| 215 | + |
| 216 | +| Test item | Result | |
| 217 | +|---|---| |
| 218 | +| IPv4 jumbo | `ping -M do -s 8972` bidirectional 8500-byte traffic fine, MTU=9000 without fragmentation ✅ | |
| 219 | +| IPv6 jumbo | `ping6 -M do -s 8500` fine (before the fix, replies were split into 6 fragments of 1448; after adding if_notifymtu, no fragmentation) ✅ | |
| 220 | +| KNI/MTU coexistence | `mtu_enable=1` + `kni.enable=1` coexist fine; veth0 MTU 1500/9000 both send and receive correctly ✅ | |
| 221 | +| Unit tests | 59 passed (including 8 new MTU tests), all 12 test binaries PASS ✅ | |
| 222 | +| Gates | -Werror / freebsd tree untouched / no rte_eth leakage / no atoi / no IPC residue / zero regression — all PASS ✅ | |
| 223 | + |
| 224 | +Unit test coverage: 8 new config-parsing cases (UT-CFG-01..08) + 7 fixtures, focusing on config-validation boundaries (defaults, cross-field conflicts, illegal strings, overflow). |
| 225 | + |
| 226 | +5.4 Advice for users |
| 227 | + |
| 228 | +- Before enabling jumbo, verify the link: the physical NIC, vSwitch, and peer must all support jumbo; with an unsupporting link, mid-path drops of large frames are worse than 1500 |
| 229 | +- Memory-sensitive scenarios: choose scatter — large mode pre-allocates the mbuf pool by max_mtu with significant memory cost; scatter saves memory but requires PMD RX_SCATTER + TX_MULTI_SEGS support |
| 230 | +- Just need a smaller MTU: no need to enable mtu_enable — `ff_ifconfig ... mtu 1400` works directly |
| 231 | + |
| 232 | +Further reading: |
| 233 | + |
| 234 | +- Research conclusions overview with three-way evidence: docs/mtu_change_spec/00-overview-index.md |
| 235 | +- IPv6 jumbo fragmentation root-cause chain (12 file:line): docs/mtu_change_spec/15-ipv6-jumbo-frame-fragmentation-analysis.md |
| 236 | +- Implementation report (M0~M5 + Post-M5): docs/mtu_change_spec/14-implementation-report.md |
| 237 | +- Interface and config design: docs/mtu_change_spec/08-interface-and-config-design.md |
| 238 | +- Related issues: #239 (set MTU in example), #490 (Why MTU MAX CONF is 1500), #720 (Enabling jumbo frames), #1033 (large-packet performance and MTU) |
| 239 | +- Three-layer architecture docs: docs/01-LAYER1-ARCHITECTURE.md |
| 240 | +- Knowledge graph: docs/KNOWLEDGE_GRAPH_WIKI.md |
0 commit comments