diff --git a/Cargo.lock b/Cargo.lock index 056d071c..41d9dfd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2345,7 +2345,7 @@ dependencies = [ "paste", "percent-encoding", "pin-project-lite", - "pnet", + "pnet_datalink", "prost 0.14.3", "quanta", "quinn", @@ -2362,6 +2362,7 @@ dependencies = [ "serial_test", "service-manager", "shellexpand", + "smoltcp", "socket2 0.5.10", "strum 0.27.2", "stun_codec", @@ -2445,7 +2446,6 @@ dependencies = [ "percent-encoding", "petgraph", "pin-project-lite", - "pnet_packet", "prefix-trie", "prost 0.14.3", "prost-types 0.14.3", @@ -2486,6 +2486,7 @@ dependencies = [ "once_cell", "serde", "serde_json", + "thunk-rs", "tokio", "uuid", ] @@ -6573,20 +6574,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "pnet" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" -dependencies = [ - "ipnetwork", - "pnet_base", - "pnet_datalink", - "pnet_packet", - "pnet_sys", - "pnet_transport", -] - [[package]] name = "pnet_base" version = "0.35.0" @@ -6594,7 +6581,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" dependencies = [ "no-std-net", - "serde", ] [[package]] @@ -6607,43 +6593,9 @@ dependencies = [ "libc", "pnet_base", "pnet_sys", - "serde", "winapi", ] -[[package]] -name = "pnet_macros" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.117", -] - -[[package]] -name = "pnet_macros_support" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" -dependencies = [ - "pnet_base", -] - -[[package]] -name = "pnet_packet" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" -dependencies = [ - "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", -] - [[package]] name = "pnet_sys" version = "0.35.0" @@ -6654,18 +6606,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "pnet_transport" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" -dependencies = [ - "libc", - "pnet_base", - "pnet_packet", - "pnet_sys", -] - [[package]] name = "png" version = "0.17.16" diff --git a/docs/quic-proxy-memory-benchmark-2026-07-27.md b/docs/quic-proxy-memory-benchmark-2026-07-27.md new file mode 100644 index 00000000..39725711 --- /dev/null +++ b/docs/quic-proxy-memory-benchmark-2026-07-27.md @@ -0,0 +1,176 @@ +# QUIC TCP Proxy 内存对比(2026-07-27) + +## 结论 + +在相同的双节点 network namespace 环境中,当前分支相对 2.6.4: + +- 空闲且未建立 TCP proxy 连接时,两端合计 USS 从 15.95 MiB + 降至 11.66 MiB,下降 26.9%; +- 66 条空闲 TCP proxy 连接时,两端合计 USS 从 19.45 MiB + 降至 13.85 MiB,下降 28.8%; +- 固定 1 Gbit/s 的单流 TCP proxy 传输中,两端平均 USS 从 + 20.69 MiB 降至 14.75 MiB,下降 28.7%,同步峰值从 + 21.50 MiB 降至 15.02 MiB; +- 从 0 增长到 66 条空闲连接推算,每条连接在两个 core 上合计 + 增加约 33.9 KiB USS;2.6.4 为 54.2 KiB,下降 37.4%; +- 当前分支的匿名内存下降约 40% 至 44%,说明堆和连接缓冲区开销 + 确实降低。 + +当前分支的 RSS 比 2.6.4 高约 5% 至 11%,但这部分差异没有出现在 +Anonymous 中,主要体现为非匿名或共享驻留页。PSS 在高连接数及 +固定吞吐场景基本持平,USS 和 Anonymous 则显著更低。因此不能 +只根据 RSS 判断发生了内存回退。 + +## 测试对象 + +| 版本 | 标识 | 二进制 | +|---|---|---| +| 当前分支 | commit `9e2ed33aeb37`,版本 `2.6.4-9e2ed33a` | `target/x86_64-unknown-linux-musl/release/easytier-core` | +| 2.6.4 | 版本 `2.6.4-8428a89d` | `/data/tickets/easytier/easytier-linux-x86_64/easytier-core` | + +当前分支使用以下命令重新构建,确保被测二进制准确对应 HEAD: + +```console +cargo build --release \ + --target x86_64-unknown-linux-musl \ + -p easytier \ + --features jemalloc \ + --bin easytier-core \ + --bin easytier-cli +``` + +两个二进制均为 stripped static PIE。当前分支明确使用 musl 和 +jemalloc。 + +## 测试拓扑 + +- 两个 `easytier-core` 分别运行在独立的 network namespace; +- namespace 通过 Linux bridge 和 veth 连接; +- underlay 地址为 `10.251.89.10/24` 和 `10.251.89.11/24`; +- EasyTier 虚拟地址为 `10.144.144.1/24` 和 + `10.144.144.2/24`; +- 两个节点之间使用 UDP listener 建立 EasyTier peer 连接; +- 源节点启用 `--enable-quic-proxy true`; +- 两个节点均保留默认 QUIC input; +- TCP client 从 `10.144.144.1` 访问绑定在 + `10.144.144.2` 上的 server; +- `tcp_proxy_connect` 指标的 `protocol` 标签确认为 `QUIC`; +- 66 条连接场景通过两端各 132 个 established TCP socket 条目 + 确认当前连接数。 + +## 采样口径 + +数据读取自 `/proc//smaps_rollup`: + +- RSS:进程映射的全部驻留页,包含共享代码页; +- PSS:共享页按共享进程数量分摊后的驻留内存; +- USS:`Private_Clean + Private_Dirty`,表示进程独占内存; +- Anonymous:匿名页,主要反映堆、栈和运行时缓冲区。 + +空闲场景每隔 2 秒采样一次,共 5 次,表格记录均值。固定吞吐场景 +持续 20 秒,每隔 2 秒采样一次,共 8 次,同时记录均值和峰值。 +所有容量单位均为 MiB。 + +## 空闲连接结果 + +以下数据均为两个 EasyTier core 的合计值: + +| 当前连接数 | 当前 RSS | 2.6.4 RSS | 当前 PSS | 2.6.4 PSS | 当前 USS | 2.6.4 USS | USS 变化 | 当前 Anonymous | 2.6.4 Anonymous | +|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 0 | 40.42 | 36.33 | 26.03 | 24.50 | 11.66 | 15.95 | -26.9% | 8.89 | 15.62 | +| 1 | 41.52 | 38.46 | 26.49 | 25.98 | 11.46 | 16.82 | -31.9% | 9.08 | 16.23 | +| 10 | 41.63 | 38.79 | 26.59 | 26.32 | 11.56 | 17.15 | -32.6% | 9.19 | 16.55 | +| 66 | 43.68 | 41.11 | 28.76 | 28.63 | 13.85 | 19.45 | -28.8% | 11.22 | 18.79 | + +0 条和 1 条连接之间的小幅反向波动属于分配器回收和采样时序噪声, +不能解释为连接产生负开销。使用 0 到 66 条连接的跨度估算单位 +连接成本更稳定。 + +### 分节点 USS + +| 当前连接数 | 当前源端 | 当前目的端 | 2.6.4 源端 | 2.6.4 目的端 | +|---:|---:|---:|---:|---:| +| 0 | 5.45 | 6.21 | 8.07 | 7.88 | +| 1 | 5.43 | 6.03 | 8.68 | 8.14 | +| 10 | 5.49 | 6.07 | 8.90 | 8.25 | +| 66 | 6.60 | 7.25 | 10.19 | 9.25 | + +### 单位连接增量 + +以 0 到 66 条连接的 USS 增量计算: + +| 版本 | 两端 USS 增量 | 每连接两端合计 | 每连接单端平均 | +|---|---:|---:|---:| +| 当前分支 | 2.19 MiB | 33.9 KiB | 17.0 KiB | +| 2.6.4 | 3.50 MiB | 54.2 KiB | 27.1 KiB | + +当前分支的每连接独占内存增量下降约 37.4%。 + +## 固定 1 Gbit/s 活跃流量 + +为排除两个版本最大吞吐不同造成的缓冲区差异,使用 +`iperf3 -b 1G -P 1 -t 20` 将两个版本都限制为 1 Gbit/s。 +两次测试均实际完成 2.33 GiB 传输,接收端报告 1000 Mbit/s。 + +### 平均值 + +| 版本 | 节点 | RSS | PSS | USS | Anonymous | +|---|---|---:|---:|---:|---:| +| 当前分支 | 源端 | 22.89 | 15.41 | 7.94 | 6.88 | +| 当前分支 | 目的端 | 21.77 | 14.29 | 6.82 | 5.27 | +| 当前分支 | 两端合计 | 44.67 | 29.70 | 14.75 | 12.14 | +| 2.6.4 | 源端 | 22.59 | 16.34 | 11.70 | 11.35 | +| 2.6.4 | 目的端 | 19.95 | 13.67 | 8.99 | 8.77 | +| 2.6.4 | 两端合计 | 42.53 | 30.01 | 20.69 | 20.13 | + +### 对比 + +| 指标 | 当前分支 | 2.6.4 | 变化 | +|---|---:|---:|---:| +| 两端平均 RSS | 44.67 | 42.53 | +5.0% | +| 两端平均 PSS | 29.70 | 30.01 | -1.0% | +| 两端平均 USS | 14.75 | 20.69 | -28.7% | +| 两端平均 Anonymous | 12.14 | 20.13 | -39.7% | +| 两端同步峰值 USS | 15.02 | 21.50 | -30.2% | + +## 分节点原始统计 + +下表保留各场景所有样本计算出的均值;`max_uss` 是该节点采样期间 +的最大 USS。 + +| 版本 | 场景 | 节点 | 样本数 | mean_rss | mean_pss | mean_uss | mean_anon | max_uss | +|---|---|---|---:|---:|---:|---:|---:|---:| +| 当前 | 0 连接 | 源端 | 5 | 19.830 | 12.636 | 5.451 | 4.314 | 5.582 | +| 当前 | 0 连接 | 目的端 | 5 | 20.587 | 13.393 | 6.208 | 4.579 | 6.320 | +| 当前 | 1 连接 | 源端 | 5 | 20.463 | 12.944 | 5.432 | 4.401 | 5.465 | +| 当前 | 1 连接 | 目的端 | 5 | 21.061 | 13.541 | 6.030 | 4.682 | 6.051 | +| 当前 | 10 连接 | 源端 | 5 | 20.522 | 13.002 | 5.491 | 4.459 | 5.496 | +| 当前 | 10 连接 | 目的端 | 5 | 21.105 | 13.585 | 6.073 | 4.726 | 6.086 | +| 当前 | 66 连接 | 源端 | 5 | 21.513 | 14.050 | 6.595 | 5.498 | 6.672 | +| 当前 | 66 连接 | 目的端 | 5 | 22.169 | 14.706 | 7.251 | 5.723 | 7.375 | +| 当前 | 1 Gbit/s | 源端 | 8 | 22.893 | 15.410 | 7.936 | 6.877 | 8.188 | +| 当前 | 1 Gbit/s | 目的端 | 8 | 21.773 | 14.291 | 6.816 | 5.266 | 6.832 | +| 2.6.4 | 0 连接 | 源端 | 5 | 18.278 | 12.355 | 8.071 | 7.876 | 8.328 | +| 2.6.4 | 0 连接 | 目的端 | 5 | 18.048 | 12.144 | 7.880 | 7.747 | 8.203 | +| 2.6.4 | 1 连接 | 源端 | 5 | 19.535 | 13.279 | 8.676 | 8.262 | 8.727 | +| 2.6.4 | 1 连接 | 目的端 | 5 | 18.920 | 12.705 | 8.143 | 7.971 | 8.191 | +| 2.6.4 | 10 连接 | 源端 | 5 | 19.762 | 13.506 | 8.902 | 8.473 | 8.910 | +| 2.6.4 | 10 连接 | 目的端 | 5 | 19.027 | 12.812 | 8.250 | 8.078 | 8.297 | +| 2.6.4 | 66 连接 | 源端 | 5 | 21.069 | 14.805 | 10.194 | 9.702 | 10.320 | +| 2.6.4 | 66 连接 | 目的端 | 5 | 20.045 | 13.822 | 9.252 | 9.088 | 9.293 | +| 2.6.4 | 1 Gbit/s | 源端 | 8 | 22.588 | 16.343 | 11.697 | 11.354 | 12.258 | +| 2.6.4 | 1 Gbit/s | 目的端 | 8 | 19.946 | 13.670 | 8.993 | 8.774 | 9.277 | + +## 解释和限制 + +1. 以固定 1 Gbit/s 场景为例,当前分支 RSS 增加 5.0%,但 + Anonymous 下降 39.7%,PSS 下降 1.0%。这说明差异主要体现 + 在非匿名或共享驻留页;本次没有保存逐 VMA 数据,因此不进一步 + 将它归因到某一个具体映射。 +2. 两个相同版本进程运行在同一宿主机时会共享可执行文件代码页, + 所以 PSS 比 RSS 更适合估算该测试拓扑的宿主机总成本,USS 和 + Anonymous 更适合判断 EasyTier 私有堆及缓冲区的变化。 +3. 这是一轮受控 A/B 测试,而不是长期统计分布。数值可用于确认 + 差异方向和量级;若作为发布门禁,应固定机器负载并增加多轮重复。 +4. 本文只比较 QUIC TCP proxy 内存,不使用未限速吞吐结果推断性能, + 避免吞吐差异污染内存结论。 diff --git a/easytier-contrib/easytier-ohrs/Cargo.lock b/easytier-contrib/easytier-ohrs/Cargo.lock index b257be62..70ed9afe 100644 --- a/easytier-contrib/easytier-ohrs/Cargo.lock +++ b/easytier-contrib/easytier-ohrs/Cargo.lock @@ -981,17 +981,6 @@ dependencies = [ "thiserror 2.0.16", ] -[[package]] -name = "delegate" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "deranged" version = "0.5.3" @@ -1001,17 +990,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -1151,7 +1129,6 @@ version = "2.6.4" dependencies = [ "anyhow", "arc-swap", - "ariadne", "async-recursion", "async-trait", "atomic-shim", @@ -1192,11 +1169,10 @@ dependencies = [ "igd-next", "indoc", "kcp-sys", + "log", "machine-uid", "moka", "natpmp", - "netlink-packet-core", - "netlink-packet-route 0.21.0", "netlink-sys", "network-interface", "nix 0.29.0", @@ -1205,16 +1181,14 @@ dependencies = [ "paste", "percent-encoding", "pin-project-lite", - "pnet", + "pnet_datalink", "prost 0.14.3", - "prost-reflect 0.16.4", "quanta", "quinn", "quinn-proto", "rand 0.8.5", "rcgen", "regex", - "ring", "rust-i18n", "rustls", "seahash", @@ -1222,6 +1196,7 @@ dependencies = [ "serde_json", "service-manager", "shellexpand", + "smoltcp", "socket2 0.5.10", "strum", "sys-locale", @@ -1229,14 +1204,12 @@ dependencies = [ "terminal_size", "thiserror 1.0.69", "thunk-rs", - "time", "tokio", "tokio-rustls", "tokio-util", "tokio-websockets", "toml", "tracing", - "tracing-subscriber", "tun-easytier", "unicode-width 0.1.11", "url", @@ -1253,7 +1226,6 @@ dependencies = [ name = "easytier-core" version = "2.6.4" dependencies = [ - "aes-gcm", "anyhow", "arc-swap", "ariadne", @@ -1265,7 +1237,7 @@ dependencies = [ "bitflags 2.9.4", "bytecodec", "bytes", - "chacha20poly1305", + "chrono", "cidr", "crossbeam", "dashmap", @@ -1283,13 +1255,12 @@ dependencies = [ "percent-encoding", "petgraph", "pin-project-lite", - "pnet_packet", "prefix-trie", "prost 0.14.3", - "prost-reflect 0.16.4", - "prost-wkt-types", + "prost-types 0.14.3", "quanta", "rand 0.8.5", + "ring", "rustls", "serde", "serde_json", @@ -1327,7 +1298,7 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "once_cell", - "prost-reflect 0.14.7", + "prost-reflect", "rusqlite", "serde", "serde_json", @@ -1350,9 +1321,6 @@ dependencies = [ "bytes", "chrono", "cidr", - "delegate", - "derivative", - "derive_more", "hmac", "indoc", "pbjson", @@ -1360,8 +1328,7 @@ dependencies = [ "proc-macro2", "prost 0.14.3", "prost-build", - "prost-reflect 0.16.4", - "prost-reflect-build", + "prost-types 0.14.3", "prost-wkt-types", "quote", "reqwest", @@ -2989,7 +2956,7 @@ dependencies = [ "ipnet", "libc", "netlink-packet-core", - "netlink-packet-route 0.17.1", + "netlink-packet-route", "netlink-sys", "once_cell", "system-configuration", @@ -3021,21 +2988,6 @@ dependencies = [ "netlink-packet-utils", ] -[[package]] -name = "netlink-packet-route" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" -dependencies = [ - "anyhow", - "bitflags 2.9.4", - "byteorder", - "libc", - "log", - "netlink-packet-core", - "netlink-packet-utils", -] - [[package]] name = "netlink-packet-utils" version = "0.5.2" @@ -3061,9 +3013,9 @@ dependencies = [ [[package]] name = "network-interface" -version = "2.0.3" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745" +checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" dependencies = [ "cc", "libc", @@ -3166,15 +3118,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -3247,15 +3190,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered_hash_map" version = "0.5.0" @@ -3402,20 +3336,6 @@ dependencies = [ "time", ] -[[package]] -name = "pnet" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" -dependencies = [ - "ipnetwork", - "pnet_base", - "pnet_datalink", - "pnet_packet", - "pnet_sys", - "pnet_transport", -] - [[package]] name = "pnet_base" version = "0.35.0" @@ -3423,7 +3343,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" dependencies = [ "no-std-net", - "serde", ] [[package]] @@ -3436,43 +3355,9 @@ dependencies = [ "libc", "pnet_base", "pnet_sys", - "serde", "winapi", ] -[[package]] -name = "pnet_macros" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.106", -] - -[[package]] -name = "pnet_macros_support" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" -dependencies = [ - "pnet_base", -] - -[[package]] -name = "pnet_packet" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" -dependencies = [ - "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", -] - [[package]] name = "pnet_sys" version = "0.35.0" @@ -3483,18 +3368,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "pnet_transport" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" -dependencies = [ - "libc", - "pnet_base", - "pnet_packet", - "pnet_sys", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -3721,34 +3594,10 @@ checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2" dependencies = [ "once_cell", "prost 0.13.5", - "prost-reflect-derive 0.14.0", + "prost-reflect-derive", "prost-types 0.13.5", ] -[[package]] -name = "prost-reflect" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590aa145fee8f7a26b5a6055365e7c5e89a5c1caae9869de76ec0ee73181a2f9" -dependencies = [ - "base64 0.22.1", - "prost 0.14.3", - "prost-reflect-derive 0.16.0", - "prost-types 0.14.3", - "serde", - "serde-value", -] - -[[package]] -name = "prost-reflect-build" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8214ae2c30bbac390db0134d08300e770ef89b6d4e5abf855e8d300eded87e28" -dependencies = [ - "prost-build", - "prost-reflect 0.16.4", -] - [[package]] name = "prost-reflect-derive" version = "0.14.0" @@ -3760,17 +3609,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "prost-reflect-derive" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b6d90e29fa6c0d13c2c19ba5e4b3fb0efbf5975d27bcf4e260b7b15455bcabe" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "prost-types" version = "0.13.5" @@ -4449,16 +4287,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_core" version = "1.0.226" @@ -4935,9 +4763,7 @@ checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde", "time-core", @@ -5252,7 +5078,6 @@ dependencies = [ "sharded-slab", "smallvec", "thread_local", - "time", "tracing", "tracing-core", "tracing-log", diff --git a/easytier-core/Cargo.toml b/easytier-core/Cargo.toml index c4465008..b923dc54 100644 --- a/easytier-core/Cargo.toml +++ b/easytier-core/Cargo.toml @@ -44,7 +44,6 @@ parking_lot = "0.12.1" percent-encoding = "2.3.1" petgraph = "0.8.1" pin-project-lite = "0.2.13" -pnet_packet = { version = "0.35.0", optional = true } prefix-trie = { version = "0.7.0", features = ["cidr"] } prost = "0.14.3" prost-types = "0.14.3" @@ -116,7 +115,6 @@ rich-config-errors = ["dep:ariadne"] tcp-hole-punch = [] proxy-packet = [ "wrapped-transport", - "dep:pnet_packet", "dep:smoltcp", "smoltcp/std", "smoltcp/proto-ipv4", diff --git a/easytier-core/src/gateway/dataplane/mod.rs b/easytier-core/src/gateway/dataplane/mod.rs index 9fa3dc76..91709b7a 100644 --- a/easytier-core/src/gateway/dataplane/mod.rs +++ b/easytier-core/src/gateway/dataplane/mod.rs @@ -23,7 +23,7 @@ use std::{ time::Duration, }; -use pnet_packet::{Packet, ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket}; +use smoltcp::wire::{IpProtocol, Ipv4Packet, TcpPacket}; use tokio::{ select, sync::{Mutex, mpsc}, @@ -74,7 +74,7 @@ use self::{ deadline::{DataPlaneDeadline, DataPlaneIoDeadline}, error::DataPlaneResult, flow::{FlowKey, FlowKind, FlowLease, FlowTable}, - packet::PeerPacketRoute, + packet::{PeerPacketRoute, tcp_flags}, resource::{DataPlaneConsumers, DataPlaneIoGuard, DataPlaneLease}, route::{ DataPlaneRoutePolicy, DataPlaneTcpRoute, DataPlaneTcpRouteInput, @@ -218,15 +218,16 @@ where || x == PacketType::DataWithQuicSrcModified as u8 ) { - if let Some(ipv4) = Ipv4Packet::new(packet.payload()) { + if let Ok(ipv4) = Ipv4Packet::new_checked(packet.payload()) { let (tcp_src_port, tcp_dst_port, tcp_flags) = - if ipv4.get_next_level_protocol() == IpNextHeaderProtocols::Tcp { - TcpPacket::new(ipv4.payload()) + if ipv4.next_header() == IpProtocol::Tcp { + TcpPacket::new_checked(ipv4.payload()) + .ok() .map(|tcp| { ( - Some(tcp.get_source()), - Some(tcp.get_destination()), - Some(tcp.get_flags()), + Some(tcp.src_port()), + Some(tcp.dst_port()), + Some(tcp_flags(&tcp)), ) }) .unwrap_or((None, None, None)) @@ -237,9 +238,9 @@ where packet_type = hdr.packet_type, from_peer_id = hdr.from_peer_id.get(), to_peer_id = hdr.to_peer_id.get(), - ipv4_src = %ipv4.get_source(), - ipv4_dst = %ipv4.get_destination(), - next_protocol = ?ipv4.get_next_level_protocol(), + ipv4_src = %ipv4.src_addr(), + ipv4_dst = %ipv4.dst_addr(), + next_protocol = ?ipv4.next_header(), ?tcp_src_port, ?tcp_dst_port, ?tcp_flags, diff --git a/easytier-core/src/gateway/dataplane/packet.rs b/easytier-core/src/gateway/dataplane/packet.rs index 1d78f4a0..d122b634 100644 --- a/easytier-core/src/gateway/dataplane/packet.rs +++ b/easytier-core/src/gateway/dataplane/packet.rs @@ -2,12 +2,10 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use pnet_packet::{ - Packet, ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket, udp::UdpPacket, -}; +use smoltcp::wire::{IPV4_HEADER_LEN, IpProtocol, Ipv4Packet, TcpPacket, UdpPacket}; use crate::{ - gateway::proxy::ip_reassembler::{IpReassembler, SmolIpv4Packet}, + gateway::proxy::ip_reassembler::IpReassembler, packet::{PacketType, ZCPacket}, }; @@ -45,21 +43,21 @@ pub(crate) enum PeerPacketRoute { }, } fn classify_peer_ipv4_payload(payload: &[u8]) -> ClassifiedPeerPacket { - let Some(ipv4) = Ipv4Packet::new(payload) else { + let Ok(ipv4) = Ipv4Packet::new_checked(payload) else { return ClassifiedPeerPacket::Unsupported; }; - if ipv4.get_version() != 4 { + if ipv4.version() != 4 || usize::from(ipv4.header_len()) < IPV4_HEADER_LEN { return ClassifiedPeerPacket::Unsupported; } - match ipv4.get_next_level_protocol() { - IpNextHeaderProtocols::Tcp => { - let Some(tcp) = TcpPacket::new(ipv4.payload()) else { + match ipv4.next_header() { + IpProtocol::Tcp => { + let Ok(tcp) = TcpPacket::new_checked(ipv4.payload()) else { return ClassifiedPeerPacket::Unsupported; }; let entry = FlowKey { - dst: SocketAddr::new(ipv4.get_source().into(), tcp.get_source()), - src: SocketAddr::new(ipv4.get_destination().into(), tcp.get_destination()), + dst: SocketAddr::new(ipv4.src_addr().into(), tcp.src_port()), + src: SocketAddr::new(ipv4.dst_addr().into(), tcp.dst_port()), kind: FlowKind::Tcp, }; let listen_entry = FlowKey { @@ -70,23 +68,22 @@ fn classify_peer_ipv4_payload(payload: &[u8]) -> ClassifiedPeerPacket { ClassifiedPeerPacket::Tcp { entry, listen_entry, - flags: tcp.get_flags(), + flags: tcp_flags(&tcp), } } - IpNextHeaderProtocols::Udp => { - let smol_ipv4 = SmolIpv4Packet::new_unchecked(ipv4.packet()); - if IpReassembler::is_packet_fragmented(&smol_ipv4) { + IpProtocol::Udp => { + if IpReassembler::is_packet_fragmented(&ipv4) { return ClassifiedPeerPacket::FragmentedUdp { - source: ipv4.get_source(), + source: ipv4.src_addr(), }; } - let Some(udp) = UdpPacket::new(ipv4.payload()) else { + let Ok(udp) = UdpPacket::new_checked(ipv4.payload()) else { return ClassifiedPeerPacket::Unsupported; }; ClassifiedPeerPacket::Udp { entry: FlowKey { - dst: SocketAddr::new(ipv4.get_source().into(), udp.get_source()), - src: SocketAddr::new(ipv4.get_destination().into(), udp.get_destination()), + dst: SocketAddr::new(ipv4.src_addr().into(), udp.src_port()), + src: SocketAddr::new(ipv4.dst_addr().into(), udp.dst_port()), kind: FlowKind::Udp, }, } @@ -94,6 +91,17 @@ fn classify_peer_ipv4_payload(payload: &[u8]) -> ClassifiedPeerPacket { _ => ClassifiedPeerPacket::Unsupported, } } + +pub(super) fn tcp_flags>(tcp: &TcpPacket) -> u8 { + u8::from(tcp.fin()) + | (u8::from(tcp.syn()) << 1) + | (u8::from(tcp.rst()) << 2) + | (u8::from(tcp.psh()) << 3) + | (u8::from(tcp.ack()) << 4) + | (u8::from(tcp.urg()) << 5) + | (u8::from(tcp.ece()) << 6) + | (u8::from(tcp.cwr()) << 7) +} impl FlowTable { pub fn route_peer_packet( &self, @@ -158,37 +166,32 @@ impl FlowTable { mod tests { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - use pnet_packet::{ - MutablePacket, - ip::IpNextHeaderProtocols, - ipv4::MutableIpv4Packet, - tcp::{MutableTcpPacket, TcpFlags}, - udp::MutableUdpPacket, - }; - use super::*; use crate::packet::{PacketType, ZCPacket}; - fn ipv4_packet(protocol: pnet_packet::ip::IpNextHeaderProtocol, payload_len: usize) -> Vec { + const TCP_SYN: u8 = 0x02; + + fn ipv4_packet(protocol: IpProtocol, payload_len: usize) -> Vec { let mut packet = vec![0; 20 + payload_len]; let packet_len = packet.len() as u16; - let mut ipv4 = MutableIpv4Packet::new(&mut packet).unwrap(); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut packet); ipv4.set_version(4); - ipv4.set_header_length(5); - ipv4.set_total_length(packet_len); - ipv4.set_source(Ipv4Addr::new(10, 1, 1, 2)); - ipv4.set_destination(Ipv4Addr::new(10, 2, 2, 3)); - ipv4.set_next_level_protocol(protocol); + ipv4.set_header_len(20); + ipv4.set_total_len(packet_len); + ipv4.set_src_addr(Ipv4Addr::new(10, 1, 1, 2)); + ipv4.set_dst_addr(Ipv4Addr::new(10, 2, 2, 3)); + ipv4.set_next_header(protocol); packet } #[test] fn classifies_tcp_and_listen_keys() { - let mut packet = ipv4_packet(IpNextHeaderProtocols::Tcp, 20); - let mut ipv4 = MutableIpv4Packet::new(&mut packet).unwrap(); - let mut tcp = MutableTcpPacket::new(ipv4.payload_mut()).unwrap(); - tcp.set_source(1234); - tcp.set_destination(4321); - tcp.set_flags(TcpFlags::SYN); + let mut packet = ipv4_packet(IpProtocol::Tcp, 20); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut packet); + let mut tcp = TcpPacket::new_unchecked(ipv4.payload_mut()); + tcp.set_src_port(1234); + tcp.set_dst_port(4321); + tcp.set_header_len(20); + tcp.set_syn(true); assert_eq!( classify_peer_ipv4_payload(&packet), @@ -203,17 +206,18 @@ mod tests { dst: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), kind: FlowKind::TcpListen, }, - flags: TcpFlags::SYN, + flags: TCP_SYN, } ); } #[test] fn classifies_udp_and_fragmented_udp() { - let mut packet = ipv4_packet(IpNextHeaderProtocols::Udp, 8); - let mut ipv4 = MutableIpv4Packet::new(&mut packet).unwrap(); - let mut udp = MutableUdpPacket::new(ipv4.payload_mut()).unwrap(); - udp.set_source(1234); - udp.set_destination(4321); + let mut packet = ipv4_packet(IpProtocol::Udp, 8); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut packet); + let mut udp = UdpPacket::new_unchecked(ipv4.payload_mut()); + udp.set_src_port(1234); + udp.set_dst_port(4321); + udp.set_len(8); assert_eq!( classify_peer_ipv4_payload(&packet), ClassifiedPeerPacket::Udp { @@ -225,10 +229,8 @@ mod tests { } ); - let mut fragmented = ipv4_packet(IpNextHeaderProtocols::Udp, 8); - MutableIpv4Packet::new(&mut fragmented) - .unwrap() - .set_fragment_offset(1); + let mut fragmented = ipv4_packet(IpProtocol::Udp, 8); + Ipv4Packet::new_unchecked(&mut fragmented).set_frag_offset(8); assert_eq!( classify_peer_ipv4_payload(&fragmented), ClassifiedPeerPacket::FragmentedUdp { @@ -243,18 +245,35 @@ mod tests { ClassifiedPeerPacket::Unsupported ); assert_eq!( - classify_peer_ipv4_payload(&ipv4_packet(IpNextHeaderProtocols::Icmp, 8)), + classify_peer_ipv4_payload(&ipv4_packet(IpProtocol::Icmp, 8)), + ClassifiedPeerPacket::Unsupported + ); + } + + #[test] + fn rejects_ipv4_header_shorter_than_minimum() { + let mut packet = ipv4_packet(IpProtocol::Tcp, 20); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut packet); + ipv4.set_header_len(16); + let mut tcp = TcpPacket::new_unchecked(ipv4.payload_mut()); + tcp.set_src_port(1234); + tcp.set_dst_port(4321); + tcp.set_header_len(20); + + assert_eq!( + classify_peer_ipv4_payload(&packet), ClassifiedPeerPacket::Unsupported ); } #[test] fn flow_table_routes_tcp_exact_and_listen_fallback() { - let mut packet = ipv4_packet(IpNextHeaderProtocols::Tcp, 20); - let mut ipv4 = MutableIpv4Packet::new(&mut packet).unwrap(); - let mut tcp = MutableTcpPacket::new(ipv4.payload_mut()).unwrap(); - tcp.set_source(1234); - tcp.set_destination(4321); - tcp.set_flags(TcpFlags::SYN); + let mut packet = ipv4_packet(IpProtocol::Tcp, 20); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut packet); + let mut tcp = TcpPacket::new_unchecked(ipv4.payload_mut()); + tcp.set_src_port(1234); + tcp.set_dst_port(4321); + tcp.set_header_len(20); + tcp.set_syn(true); let exact = FlowKey { src: "10.2.2.3:4321".parse().unwrap(), @@ -272,7 +291,7 @@ mod tests { table.route_peer_ipv4_payload(&packet, false), PeerPacketRoute::Unmatched { entry: exact.clone(), - tcp_flags: Some(TcpFlags::SYN), + tcp_flags: Some(TCP_SYN), } ); @@ -281,7 +300,7 @@ mod tests { table.route_peer_ipv4_payload(&packet, true), PeerPacketRoute::Deliver { entry: listen, - tcp_flags: Some(TcpFlags::SYN), + tcp_flags: Some(TCP_SYN), } ); @@ -290,16 +309,14 @@ mod tests { table.route_peer_ipv4_payload(&packet, true), PeerPacketRoute::Deliver { entry: exact, - tcp_flags: Some(TcpFlags::SYN), + tcp_flags: Some(TCP_SYN), } ); } #[test] fn flow_table_routes_fragmented_udp_by_source_ip() { - let mut packet = ipv4_packet(IpNextHeaderProtocols::Udp, 8); - MutableIpv4Packet::new(&mut packet) - .unwrap() - .set_fragment_offset(1); + let mut packet = ipv4_packet(IpProtocol::Udp, 8); + Ipv4Packet::new_unchecked(&mut packet).set_frag_offset(8); let table = FlowTable::default(); assert_eq!( @@ -328,11 +345,12 @@ mod tests { } #[test] fn flow_table_routes_loopback_modified_source_packets() { - let mut payload = ipv4_packet(IpNextHeaderProtocols::Tcp, 20); - let mut ipv4 = MutableIpv4Packet::new(&mut payload).unwrap(); - let mut tcp = MutableTcpPacket::new(ipv4.payload_mut()).unwrap(); - tcp.set_source(1234); - tcp.set_destination(4321); + let mut payload = ipv4_packet(IpProtocol::Tcp, 20); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut payload); + let mut tcp = TcpPacket::new_unchecked(ipv4.payload_mut()); + tcp.set_src_port(1234); + tcp.set_dst_port(4321); + tcp.set_header_len(20); let entry = FlowKey { src: "10.2.2.3:4321".parse().unwrap(), dst: "10.1.1.2:1234".parse().unwrap(), @@ -359,8 +377,7 @@ mod tests { #[test] fn flow_table_passes_non_loopback_or_malformed_modified_source_packets() { let table = FlowTable::<()>::default(); - let mut non_loopback = - ZCPacket::new_with_payload(&ipv4_packet(IpNextHeaderProtocols::Tcp, 20)); + let mut non_loopback = ZCPacket::new_with_payload(&ipv4_packet(IpProtocol::Tcp, 20)); non_loopback.fill_peer_manager_hdr(7, 8, PacketType::DataWithKcpSrcModified as u8); assert_eq!( table.route_peer_packet(&non_loopback, false), diff --git a/easytier-core/src/gateway/dataplane/stack.rs b/easytier-core/src/gateway/dataplane/stack.rs index bc8a2386..81928fed 100644 --- a/easytier-core/src/gateway/dataplane/stack.rs +++ b/easytier-core/src/gateway/dataplane/stack.rs @@ -5,7 +5,7 @@ use std::{ sync::{Arc, Weak}, }; -use pnet_packet::ipv4::Ipv4Packet; +use smoltcp::wire::Ipv4Packet; use tokio::{ sync::{Mutex, mpsc}, task::JoinSet, @@ -54,11 +54,11 @@ impl SmoltcpPlane { forward_tasks.spawn(async move { while let Some(data) = stack_stream.recv().await { - let Some(ipv4) = Ipv4Packet::new(&data) else { + let Ok(ipv4) = Ipv4Packet::new_checked(&data) else { tracing::error!(?data, "smoltcp emitted a non-IPv4 packet"); continue; }; - let destination = ipv4.get_destination(); + let destination = ipv4.dst_addr(); let Some(peer_manager) = peer_manager.upgrade() else { tracing::debug!("smoltcp-to-peer bridge lost PeerManager"); return; diff --git a/easytier-core/src/gateway/dataplane/tests.rs b/easytier-core/src/gateway/dataplane/tests.rs index 377abb13..fcc5c2b7 100644 --- a/easytier-core/src/gateway/dataplane/tests.rs +++ b/easytier-core/src/gateway/dataplane/tests.rs @@ -1,11 +1,6 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use pnet_packet::{ - MutablePacket, - ip::IpNextHeaderProtocols, - ipv4::{self, MutableIpv4Packet}, - tcp::{self, MutableTcpPacket, TcpFlags}, -}; +use smoltcp::wire::{IpAddress, IpProtocol, Ipv4Packet, TcpPacket}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use super::*; @@ -177,28 +172,25 @@ fn build_tcp_packet(src: SocketAddr, dst: SocketAddr) -> Vec { }; { - let mut ip_packet = MutableIpv4Packet::new(&mut buf).unwrap(); + let mut ip_packet = Ipv4Packet::new_unchecked(&mut buf); ip_packet.set_version(4); - ip_packet.set_header_length(5); - ip_packet.set_total_length(40); - ip_packet.set_ttl(64); - ip_packet.set_next_level_protocol(IpNextHeaderProtocols::Tcp); - ip_packet.set_source(src_ip); - ip_packet.set_destination(dst_ip); + ip_packet.set_header_len(20); + ip_packet.set_total_len(40); + ip_packet.set_hop_limit(64); + ip_packet.set_next_header(IpProtocol::Tcp); + ip_packet.set_src_addr(src_ip); + ip_packet.set_dst_addr(dst_ip); - let mut tcp_packet = MutableTcpPacket::new(ip_packet.payload_mut()).unwrap(); - tcp_packet.set_source(src.port()); - tcp_packet.set_destination(dst.port()); - tcp_packet.set_data_offset(5); - tcp_packet.set_flags(TcpFlags::SYN | TcpFlags::ACK); - tcp_packet.set_window(65535); - tcp_packet.set_checksum(tcp::ipv4_checksum( - &tcp_packet.to_immutable(), - &src_ip, - &dst_ip, - )); + let mut tcp_packet = TcpPacket::new_unchecked(ip_packet.payload_mut()); + tcp_packet.set_src_port(src.port()); + tcp_packet.set_dst_port(dst.port()); + tcp_packet.set_header_len(20); + tcp_packet.set_syn(true); + tcp_packet.set_ack(true); + tcp_packet.set_window_len(65535); + tcp_packet.fill_checksum(&IpAddress::Ipv4(src_ip), &IpAddress::Ipv4(dst_ip)); - ip_packet.set_checksum(ipv4::checksum(&ip_packet.to_immutable())); + ip_packet.fill_checksum(); } buf @@ -207,20 +199,20 @@ fn build_tcp_packet(src: SocketAddr, dst: SocketAddr) -> Vec { fn build_udp_followup_fragment(src: Ipv4Addr, dst: Ipv4Addr) -> Vec { let mut buf = vec![0u8; 28]; { - let mut ip_packet = MutableIpv4Packet::new(&mut buf).unwrap(); + let mut ip_packet = Ipv4Packet::new_unchecked(&mut buf); ip_packet.set_version(4); - ip_packet.set_header_length(5); - ip_packet.set_total_length(28); - ip_packet.set_ttl(64); - ip_packet.set_next_level_protocol(IpNextHeaderProtocols::Udp); - ip_packet.set_fragment_offset(1); - ip_packet.set_source(src); - ip_packet.set_destination(dst); + ip_packet.set_header_len(20); + ip_packet.set_total_len(28); + ip_packet.set_hop_limit(64); + ip_packet.set_next_header(IpProtocol::Udp); + ip_packet.set_frag_offset(8); + ip_packet.set_src_addr(src); + ip_packet.set_dst_addr(dst); ip_packet .payload_mut() .copy_from_slice(&[0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe]); - ip_packet.set_checksum(ipv4::checksum(&ip_packet.to_immutable())); + ip_packet.fill_checksum(); } buf diff --git a/easytier-core/src/gateway/magic_dns/packet.rs b/easytier-core/src/gateway/magic_dns/packet.rs index 16bc05d9..80212c1d 100644 --- a/easytier-core/src/gateway/magic_dns/packet.rs +++ b/easytier-core/src/gateway/magic_dns/packet.rs @@ -1,12 +1,9 @@ use std::{future::Future, net::Ipv4Addr}; use async_trait::async_trait; -use pnet_packet::{ - MutablePacket, Packet, - icmp::{self, IcmpPacket, IcmpTypes, MutableIcmpPacket}, - ip::IpNextHeaderProtocols, - ipv4::{self, Ipv4Flags, Ipv4Packet, MutableIpv4Packet}, - udp::{self, MutableUdpPacket, UdpPacket}, +use smoltcp::wire::{ + IPV4_HEADER_LEN, Icmpv4Message, Icmpv4Packet, IpAddress, IpProtocol, Ipv4Packet, + UDP_HEADER_LEN, UdpPacket, }; use crate::{ @@ -119,43 +116,45 @@ where if packet.peer_manager_header().is_none() { return false; } - let Some(ip_packet) = Ipv4Packet::new(packet.payload()) else { + if packet.payload().len() < IPV4_HEADER_LEN { return false; - }; - if ip_packet.get_version() != 4 || ip_packet.get_destination() != fake_ip { + } + let ip_packet = Ipv4Packet::new_unchecked(packet.payload()); + if ip_packet.version() != 4 || ip_packet.dst_addr() != fake_ip { return false; } - let ip_header_length = ip_packet.get_header_length() as usize * 4; - let ip_total_length = ip_packet.get_total_length() as usize; - if ip_header_length < MutableIpv4Packet::minimum_packet_size() + let ip_header_length = ip_packet.header_len() as usize; + let ip_total_length = ip_packet.total_len() as usize; + if ip_header_length < IPV4_HEADER_LEN || ip_header_length > ip_total_length || ip_total_length != packet.payload().len() - || ip_packet.get_fragment_offset() != 0 - || ip_packet.get_flags() & Ipv4Flags::MoreFragments != 0 + || ip_packet.frag_offset() != 0 + || ip_packet.more_frags() { return false; } - let protocol = ip_packet.get_next_level_protocol(); - let source_ip = ip_packet.get_source(); - let destination_ip = ip_packet.get_destination(); + let protocol = ip_packet.next_header(); + let source_ip = ip_packet.src_addr(); + let destination_ip = ip_packet.dst_addr(); match protocol { - IpNextHeaderProtocols::Udp => { + IpProtocol::Udp => { let ip_payload = &packet.payload()[ip_header_length..ip_total_length]; - let Some(udp_packet) = UdpPacket::new(ip_payload) else { - return false; - }; - let udp_length = udp_packet.get_length() as usize; - if udp_length != ip_payload.len() || udp_length < UdpPacket::minimum_packet_size() { + if ip_payload.len() < UDP_HEADER_LEN { return false; } - if udp_packet.get_destination() != 53 { + let udp_packet = UdpPacket::new_unchecked(ip_payload); + let udp_length = udp_packet.len() as usize; + if udp_length != ip_payload.len() || udp_length < UDP_HEADER_LEN { return false; } - let source_port = udp_packet.get_source(); - let destination_port = udp_packet.get_destination(); + if udp_packet.dst_port() != 53 { + return false; + } + let source_port = udp_packet.src_port(); + let destination_port = udp_packet.dst_port(); let query = MagicDnsQuery { source: std::net::SocketAddr::from((source_ip, source_port)), payload: udp_packet.payload().to_vec(), @@ -175,30 +174,26 @@ where return false; } } - IpNextHeaderProtocols::Icmp => { - let Some(icmp_packet) = IcmpPacket::new(&packet.payload()[ip_header_length..]) else { - return false; - }; - if icmp_packet.get_icmp_type() != IcmpTypes::EchoRequest { - return false; - } - let Some(mut icmp_packet) = - MutableIcmpPacket::new(&mut packet.mut_payload()[ip_header_length..]) + IpProtocol::Icmp => { + let Ok(icmp_packet) = Icmpv4Packet::new_checked(&packet.payload()[ip_header_length..]) else { return false; }; - icmp_packet.set_icmp_type(IcmpTypes::EchoReply); - icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); + if icmp_packet.msg_type() != Icmpv4Message::EchoRequest { + return false; + } + let mut icmp_packet = + Icmpv4Packet::new_unchecked(&mut packet.mut_payload()[ip_header_length..]); + icmp_packet.set_msg_type(Icmpv4Message::EchoReply); + icmp_packet.fill_checksum(); } _ => return false, } - let Some(mut ip_packet) = MutableIpv4Packet::new(packet.mut_payload()) else { - return false; - }; - ip_packet.set_source(destination_ip); - ip_packet.set_destination(source_ip); - ip_packet.set_checksum(ipv4::checksum(&ip_packet.to_immutable())); + let mut ip_packet = Ipv4Packet::new_unchecked(packet.mut_payload()); + ip_packet.set_src_addr(destination_ip); + ip_packet.set_dst_addr(source_ip); + ip_packet.fill_checksum(); let payload_length = packet.payload().len() as u32; let Some(header) = packet.mut_peer_manager_header() else { return false; @@ -218,7 +213,7 @@ fn apply_udp_response( ip_header_length: usize, response: &[u8], ) -> bool { - let Some(udp_length) = UdpPacket::minimum_packet_size().checked_add(response.len()) else { + let Some(udp_length) = UDP_HEADER_LEN.checked_add(response.len()) else { return false; }; let Some(ip_length) = ip_header_length.checked_add(udp_length) else { @@ -237,26 +232,21 @@ fn apply_udp_response( if packet.mut_inner().capacity() < inner_length { packet .mut_inner() - .truncate(header_length + ip_header_length + UdpPacket::minimum_packet_size()); + .truncate(header_length + ip_header_length + UDP_HEADER_LEN); } packet.mut_inner().resize(inner_length, 0); - let Some(mut ip_packet) = MutableIpv4Packet::new(packet.mut_payload()) else { - return false; - }; - ip_packet.set_total_length(ip_length as u16); - let Some(mut udp_packet) = MutableUdpPacket::new(ip_packet.payload_mut()) else { - return false; - }; - udp_packet.set_length(udp_length as u16); - udp_packet.set_source(destination_port); - udp_packet.set_destination(source_port); + let mut ip_packet = Ipv4Packet::new_unchecked(packet.mut_payload()); + ip_packet.set_total_len(ip_length as u16); + let mut udp_packet = UdpPacket::new_unchecked(ip_packet.payload_mut()); + udp_packet.set_len(udp_length as u16); + udp_packet.set_src_port(destination_port); + udp_packet.set_dst_port(source_port); udp_packet.payload_mut().copy_from_slice(response); - udp_packet.set_checksum(udp::ipv4_checksum( - &udp_packet.to_immutable(), - &destination_ip, - &source_ip, - )); + udp_packet.fill_checksum( + &IpAddress::Ipv4(destination_ip), + &IpAddress::Ipv4(source_ip), + ); true } @@ -267,17 +257,17 @@ mod tests { fn udp_query(payload: &[u8], destination_port: u16) -> ZCPacket { let mut bytes = vec![0; 20 + 8 + payload.len()]; { - let mut ip = MutableIpv4Packet::new(&mut bytes).unwrap(); + let mut ip = Ipv4Packet::new_unchecked(&mut bytes); ip.set_version(4); - ip.set_header_length(5); - ip.set_total_length((20 + 8 + payload.len()) as u16); - ip.set_next_level_protocol(IpNextHeaderProtocols::Udp); - ip.set_source("10.0.0.2".parse().unwrap()); - ip.set_destination("100.100.100.101".parse().unwrap()); - let mut udp = MutableUdpPacket::new(ip.payload_mut()).unwrap(); - udp.set_source(53000); - udp.set_destination(destination_port); - udp.set_length((8 + payload.len()) as u16); + ip.set_header_len(20); + ip.set_total_len((20 + 8 + payload.len()) as u16); + ip.set_next_header(IpProtocol::Udp); + ip.set_src_addr("10.0.0.2".parse().unwrap()); + ip.set_dst_addr("100.100.100.101".parse().unwrap()); + let mut udp = UdpPacket::new_unchecked(ip.payload_mut()); + udp.set_src_port(53000); + udp.set_dst_port(destination_port); + udp.set_len((8 + payload.len()) as u16); udp.payload_mut().copy_from_slice(payload); } ZCPacket::new_with_payload(&bytes) @@ -286,15 +276,15 @@ mod tests { fn icmp_echo_request() -> ZCPacket { let mut bytes = vec![0; 20 + 8]; { - let mut ip = MutableIpv4Packet::new(&mut bytes).unwrap(); + let mut ip = Ipv4Packet::new_unchecked(&mut bytes); ip.set_version(4); - ip.set_header_length(5); - ip.set_total_length(28); - ip.set_next_level_protocol(IpNextHeaderProtocols::Icmp); - ip.set_source("10.0.0.2".parse().unwrap()); - ip.set_destination("100.100.100.101".parse().unwrap()); - let mut icmp = MutableIcmpPacket::new(ip.payload_mut()).unwrap(); - icmp.set_icmp_type(IcmpTypes::EchoRequest); + ip.set_header_len(20); + ip.set_total_len(28); + ip.set_next_header(IpProtocol::Icmp); + ip.set_src_addr("10.0.0.2".parse().unwrap()); + ip.set_dst_addr("100.100.100.101".parse().unwrap()); + let mut icmp = Icmpv4Packet::new_unchecked(ip.payload_mut()); + icmp.set_msg_type(Icmpv4Message::EchoRequest); } ZCPacket::new_with_payload(&bytes) } @@ -314,18 +304,15 @@ mod tests { .await; assert!(handled); - let ip = Ipv4Packet::new(packet.payload()).unwrap(); + let ip = Ipv4Packet::new_checked(packet.payload()).unwrap(); assert_eq!( - ip.get_source(), + ip.src_addr(), "100.100.100.101".parse::().unwrap() ); - assert_eq!( - ip.get_destination(), - "10.0.0.2".parse::().unwrap() - ); - let udp = UdpPacket::new(ip.payload()).unwrap(); - assert_eq!(udp.get_source(), 53); - assert_eq!(udp.get_destination(), 53000); + assert_eq!(ip.dst_addr(), "10.0.0.2".parse::().unwrap()); + let udp = UdpPacket::new_checked(ip.payload()).unwrap(); + assert_eq!(udp.src_port(), 53); + assert_eq!(udp.dst_port(), 53000); assert_eq!(udp.payload(), b"response"); assert_eq!(packet.get_dst_peer_id(), Some(42)); assert_eq!( @@ -337,9 +324,7 @@ mod tests { #[tokio::test] async fn packet_engine_rejects_invalid_ipv4_header_without_mutation() { let mut packet = udp_query(b"query", 53); - MutableIpv4Packet::new(packet.mut_payload()) - .unwrap() - .set_header_length(15); + Ipv4Packet::new_unchecked(packet.mut_payload()).set_header_len(60); let original = packet.payload().to_vec(); assert!( @@ -373,10 +358,8 @@ mod tests { #[tokio::test] async fn packet_engine_rejects_inconsistent_udp_length_without_mutation() { let mut packet = udp_query(b"query", 53); - let mut ip = MutableIpv4Packet::new(packet.mut_payload()).unwrap(); - MutableUdpPacket::new(ip.payload_mut()) - .unwrap() - .set_length(8); + let mut ip = Ipv4Packet::new_unchecked(packet.mut_payload()); + UdpPacket::new_unchecked(ip.payload_mut()).set_len(8); let original = packet.payload().to_vec(); assert!( @@ -394,9 +377,7 @@ mod tests { #[tokio::test] async fn packet_engine_rejects_fragmented_packets_without_mutation() { let mut packet = udp_query(b"query", 53); - MutableIpv4Packet::new(packet.mut_payload()) - .unwrap() - .set_flags(Ipv4Flags::MoreFragments); + Ipv4Packet::new_unchecked(packet.mut_payload()).set_more_frags(true); let original = packet.payload().to_vec(); assert!( @@ -442,13 +423,13 @@ mod tests { .await; assert!(handled); - let ip = Ipv4Packet::new(packet.payload()).unwrap(); + let ip = Ipv4Packet::new_checked(packet.payload()).unwrap(); assert_eq!( - ip.get_source(), + ip.src_addr(), "100.100.100.101".parse::().unwrap() ); - let icmp = pnet_packet::icmp::IcmpPacket::new(ip.payload()).unwrap(); - assert_eq!(icmp.get_icmp_type(), IcmpTypes::EchoReply); + let icmp = Icmpv4Packet::new_checked(ip.payload()).unwrap(); + assert_eq!(icmp.msg_type(), Icmpv4Message::EchoReply); assert_eq!(packet.get_dst_peer_id(), Some(7)); } diff --git a/easytier-core/src/gateway/proxy/icmp_proxy_engine.rs b/easytier-core/src/gateway/proxy/icmp_proxy_engine.rs index a0e6f51b..fa066cba 100644 --- a/easytier-core/src/gateway/proxy/icmp_proxy_engine.rs +++ b/easytier-core/src/gateway/proxy/icmp_proxy_engine.rs @@ -1,21 +1,14 @@ use std::{net::Ipv4Addr, sync::Arc, time::Duration}; use dashmap::DashMap; -use pnet_packet::{ - Packet, - icmp::{self, IcmpCode, IcmpTypes, MutableIcmpPacket, echo_reply::MutableEchoReplyPacket}, - ip::IpNextHeaderProtocols, - ipv4::Ipv4Packet, -}; use quanta::Instant; +use smoltcp::wire::{IPV4_HEADER_LEN, Icmpv4Message, Icmpv4Packet, Ipv4Packet}; use crate::packet::{PacketType, ZCPacket}; use super::{ cidr_table::ProxyCidrTable, - ip_reassembler::{ - ComposeIpv4PacketArgs, IpProtocol, IpReassembler, SmolIpv4Packet, compose_ipv4_packet, - }, + ip_reassembler::{ComposeIpv4PacketArgs, IpProtocol, IpReassembler, compose_ipv4_packet}, }; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -51,6 +44,8 @@ struct IcmpNatEntry { started_at: Instant, } +const ICMP_ECHO_HEADER_LEN: usize = 8; + #[derive(Debug)] pub struct IcmpProxyEngine { cidr_table: Arc, @@ -84,15 +79,17 @@ impl IcmpProxyEngine { if header.packet_type != PacketType::Data as u8 || header.is_no_proxy() { return IcmpProxyAction::Pass; } - let Some(ipv4) = Ipv4Packet::new(packet.payload()) else { + let Ok(ipv4) = Ipv4Packet::new_checked(packet.payload()) else { return IcmpProxyAction::Pass; }; - if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Icmp + if ipv4.version() != 4 + || usize::from(ipv4.header_len()) < IPV4_HEADER_LEN + || ipv4.next_header() != IpProtocol::Icmp { return IcmpProxyAction::Pass; } - let mapped_destination = ipv4.get_destination(); + let mapped_destination = ipv4.dst_addr(); let real_destination = self.cidr_table.lookup_v4(mapped_destination); let is_local_no_tun = context.no_tun && mapped_destination == virtual_ipv4; if real_destination.is_none() && !header.is_exit_node() && !is_local_no_tun { @@ -100,33 +97,27 @@ impl IcmpProxyEngine { } let reassembled; - let smol_ipv4 = SmolIpv4Packet::new_unchecked(ipv4.packet()); - let request = if IpReassembler::is_packet_fragmented(&smol_ipv4) { - let Ok(smol_ipv4) = SmolIpv4Packet::new_checked(ipv4.packet()) else { - return IcmpProxyAction::Pass; - }; - reassembled = self.reassembler.add_fragment(&smol_ipv4); + let request_bytes = if IpReassembler::is_packet_fragmented(&ipv4) { + reassembled = self.reassembler.add_fragment(&ipv4); let Some(reassembled) = reassembled.as_ref() else { return IcmpProxyAction::Pass; }; - let Some(request) = icmp::echo_request::EchoRequestPacket::new(reassembled) else { - return IcmpProxyAction::Pass; - }; - request + reassembled.as_slice() } else { - let Some(request) = icmp::echo_request::EchoRequestPacket::new(ipv4.payload()) else { - return IcmpProxyAction::Pass; - }; - request + ipv4.payload() }; - if request.get_icmp_type() != IcmpTypes::EchoRequest { + if request_bytes.len() < ICMP_ECHO_HEADER_LEN { + return IcmpProxyAction::Pass; + } + let request = Icmpv4Packet::new_unchecked(request_bytes); + if request.msg_type() != Icmpv4Message::EchoRequest { return IcmpProxyAction::Pass; } if is_local_no_tun { return self.local_reply( mapped_destination, - ipv4.get_source(), + ipv4.src_addr(), header.to_peer_id.get(), header.from_peer_id.get(), &request, @@ -136,15 +127,15 @@ impl IcmpProxyEngine { let real_destination = real_destination.unwrap_or(mapped_destination); let key = IcmpNatKey { real_destination, - identifier: request.get_identifier(), - sequence: request.get_sequence_number(), + identifier: request.echo_ident(), + sequence: request.echo_seq_no(), }; self.nat_table.insert( key, IcmpNatEntry { source_peer_id: header.from_peer_id.get(), local_peer_id: header.to_peer_id.get(), - source_ip: ipv4.get_source(), + source_ip: ipv4.src_addr(), mapped_destination, started_at: Instant::now(), }, @@ -152,35 +143,35 @@ impl IcmpProxyEngine { IcmpProxyAction::SendToSocket { destination: real_destination, - packet: request.packet().to_vec(), + packet: request.as_ref().to_vec(), } } pub fn handle_socket_response(&self, peer_ip: Ipv4Addr, packet: &mut [u8]) -> Vec { - let Some(ipv4) = Ipv4Packet::new(packet) else { + let Ok(ipv4) = Ipv4Packet::new_checked(&*packet) else { return Vec::new(); }; - let Some(reply) = icmp::echo_reply::EchoReplyPacket::new(ipv4.payload()) else { + if usize::from(ipv4.header_len()) < IPV4_HEADER_LEN + || ipv4.payload().len() < ICMP_ECHO_HEADER_LEN + { return Vec::new(); - }; - if reply.get_icmp_type() != IcmpTypes::EchoReply { + } + let reply = Icmpv4Packet::new_unchecked(ipv4.payload()); + if reply.msg_type() != Icmpv4Message::EchoReply { return Vec::new(); } let key = IcmpNatKey { real_destination: peer_ip, - identifier: reply.get_identifier(), - sequence: reply.get_sequence_number(), + identifier: reply.echo_ident(), + sequence: reply.echo_seq_no(), }; let Some((_, entry)) = self.nat_table.remove(&key) else { return Vec::new(); }; - let Some(payload_len) = packet - .len() - .checked_sub(ipv4.get_header_length() as usize * 4) - else { + let Some(payload_len) = packet.len().checked_sub(ipv4.header_len() as usize) else { return Vec::new(); }; - let ip_id = ipv4.get_identification(); + let ip_id = ipv4.ident(); let mut responses = Vec::new(); let _ = compose_ipv4_packet( ComposeIpv4PacketArgs { @@ -226,17 +217,16 @@ impl IcmpProxyEngine { destination: Ipv4Addr, source_peer_id: u32, destination_peer_id: u32, - request: &icmp::echo_request::EchoRequestPacket<'_>, + request: &Icmpv4Packet<&[u8]>, ) -> IcmpProxyAction { - let mut buffer = vec![0_u8; request.packet().len() + 20]; - let mut reply = MutableEchoReplyPacket::new(&mut buffer[20..]).unwrap(); - reply.set_icmp_type(IcmpTypes::EchoReply); - reply.set_icmp_code(IcmpCode::new(0)); - reply.set_identifier(request.get_identifier()); - reply.set_sequence_number(request.get_sequence_number()); - reply.set_payload(request.payload()); - let mut reply = MutableIcmpPacket::new(&mut buffer[20..]).unwrap(); - reply.set_checksum(icmp::checksum(&reply.to_immutable())); + let mut buffer = vec![0_u8; request.as_ref().len() + 20]; + let mut reply = Icmpv4Packet::new_unchecked(&mut buffer[20..]); + reply.set_msg_type(Icmpv4Message::EchoReply); + reply.set_msg_code(0); + reply.set_echo_ident(request.echo_ident()); + reply.set_echo_seq_no(request.echo_seq_no()); + reply.data_mut().copy_from_slice(request.data()); + reply.fill_checksum(); let payload_len = buffer.len() - 20; let mut responses = Vec::new(); @@ -267,12 +257,6 @@ impl IcmpProxyEngine { #[cfg(test)] mod tests { - use pnet_packet::{ - MutablePacket as _, - icmp::{MutableIcmpPacket, echo_request::MutableEchoRequestPacket}, - ipv4::{self, MutableIpv4Packet}, - }; - use super::*; use crate::gateway::proxy::cidr_table::{ProxyCidrRule, ProxyCidrSnapshot}; @@ -283,25 +267,24 @@ mod tests { ) -> ZCPacket { let mut bytes = vec![0_u8; 20 + 8 + payload.len()]; { - let mut request = MutableEchoRequestPacket::new(&mut bytes[20..]).unwrap(); - request.set_icmp_type(IcmpTypes::EchoRequest); - request.set_identifier(7); - request.set_sequence_number(11); - request.set_payload(payload); - let mut icmp = MutableIcmpPacket::new(&mut bytes[20..]).unwrap(); - icmp.set_checksum(icmp::checksum(&icmp.to_immutable())); + let mut request = Icmpv4Packet::new_unchecked(&mut bytes[20..]); + request.set_msg_type(Icmpv4Message::EchoRequest); + request.set_echo_ident(7); + request.set_echo_seq_no(11); + request.data_mut().copy_from_slice(payload); + request.fill_checksum(); } { let packet_len = bytes.len() as u16; - let mut ipv4 = MutableIpv4Packet::new(&mut bytes).unwrap(); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut bytes); ipv4.set_version(4); - ipv4.set_header_length(5); - ipv4.set_total_length(packet_len); - ipv4.set_ttl(64); - ipv4.set_next_level_protocol(IpNextHeaderProtocols::Icmp); - ipv4.set_source(source); - ipv4.set_destination(destination); - ipv4.set_checksum(ipv4::checksum(&ipv4.to_immutable())); + ipv4.set_header_len(20); + ipv4.set_total_len(packet_len); + ipv4.set_hop_limit(64); + ipv4.set_next_header(IpProtocol::Icmp); + ipv4.set_src_addr(source); + ipv4.set_dst_addr(destination); + ipv4.fill_checksum(); } let mut packet = ZCPacket::new_with_payload(&bytes); packet.fill_peer_manager_hdr(101, 202, PacketType::Data as u8); @@ -357,16 +340,32 @@ mod tests { let header = reply.peer_manager_header().unwrap(); assert_eq!(header.from_peer_id.get(), 202); assert_eq!(header.to_peer_id.get(), 101); - let ipv4 = Ipv4Packet::new(reply.payload()).unwrap(); - assert_eq!(ipv4.get_source(), "10.0.0.1".parse::().unwrap()); - assert_eq!( - ipv4.get_destination(), - "10.0.0.2".parse::().unwrap() - ); - let reply = icmp::echo_reply::EchoReplyPacket::new(ipv4.payload()).unwrap(); - assert_eq!(reply.get_identifier(), 7); - assert_eq!(reply.get_sequence_number(), 11); - assert_eq!(reply.payload(), b"ping"); + let ipv4 = Ipv4Packet::new_checked(reply.payload()).unwrap(); + assert_eq!(ipv4.src_addr(), "10.0.0.1".parse::().unwrap()); + assert_eq!(ipv4.dst_addr(), "10.0.0.2".parse::().unwrap()); + let reply = Icmpv4Packet::new_checked(ipv4.payload()).unwrap(); + assert_eq!(reply.echo_ident(), 7); + assert_eq!(reply.echo_seq_no(), 11); + assert_eq!(reply.data(), b"ping"); + } + + #[test] + fn peer_packet_rejects_ipv4_header_shorter_than_minimum() { + let engine = engine(None); + let mut packet = echo_request("10.0.0.2".parse().unwrap(), "8.0.0.1".parse().unwrap()); + Ipv4Packet::new_unchecked(packet.mut_payload()).set_header_len(16); + + assert!(matches!( + engine.handle_peer_packet( + &packet, + IcmpProxyContext { + virtual_ipv4: Some("8.0.0.1".parse().unwrap()), + no_tun: true, + ..Default::default() + } + ), + IcmpProxyAction::Pass + )); } #[test] @@ -391,20 +390,19 @@ mod tests { panic!("expected socket request"); }; assert_eq!(destination, "127.0.0.42".parse::().unwrap()); - let request = icmp::echo_request::EchoRequestPacket::new(&request).unwrap(); - assert_eq!(request.payload(), b"ping"); + let request = Icmpv4Packet::new_checked(&request).unwrap(); + assert_eq!(request.data(), b"ping"); let mut response = echo_request(destination, "10.0.0.1".parse().unwrap()) .payload() .to_vec(); { - let mut ipv4 = MutableIpv4Packet::new(&mut response).unwrap(); - let mut reply = MutableEchoReplyPacket::new(ipv4.payload_mut()).unwrap(); - reply.set_icmp_type(IcmpTypes::EchoReply); - let mut icmp = MutableIcmpPacket::new(ipv4.payload_mut()).unwrap(); - icmp.set_checksum(icmp::checksum(&icmp.to_immutable())); - ipv4.set_source(destination); - ipv4.set_checksum(ipv4::checksum(&ipv4.to_immutable())); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut response); + let mut reply = Icmpv4Packet::new_unchecked(ipv4.payload_mut()); + reply.set_msg_type(Icmpv4Message::EchoReply); + reply.fill_checksum(); + ipv4.set_src_addr(destination); + ipv4.fill_checksum(); } let replies = engine.handle_socket_response(destination, &mut response); let [reply] = replies.as_slice() else { @@ -414,15 +412,54 @@ mod tests { assert_eq!(header.from_peer_id.get(), 202); assert_eq!(header.to_peer_id.get(), 101); assert!(header.is_no_proxy()); - let ipv4 = Ipv4Packet::new(reply.payload()).unwrap(); - assert_eq!( - ipv4.get_source(), - "10.10.10.42".parse::().unwrap() - ); - assert_eq!( - ipv4.get_destination(), - "10.0.0.2".parse::().unwrap() + let ipv4 = Ipv4Packet::new_checked(reply.payload()).unwrap(); + assert_eq!(ipv4.src_addr(), "10.10.10.42".parse::().unwrap()); + assert_eq!(ipv4.dst_addr(), "10.0.0.2".parse::().unwrap()); + } + + #[test] + fn socket_response_rejects_ipv4_header_shorter_than_minimum() { + let engine = engine(Some(ProxyCidrRule { + cidr: "127.0.0.0/24".parse().unwrap(), + mapped_cidr: Some("10.10.10.0/24".parse().unwrap()), + })); + let destination = "127.0.0.42".parse().unwrap(); + let request = echo_request("10.0.0.2".parse().unwrap(), "10.10.10.42".parse().unwrap()); + assert!(matches!( + engine.handle_peer_packet( + &request, + IcmpProxyContext { + virtual_ipv4: Some("10.0.0.1".parse().unwrap()), + ..Default::default() + }, + ), + IcmpProxyAction::SendToSocket { .. } + )); + + let key = IcmpNatKey { + real_destination: destination, + identifier: 7, + sequence: 11, + }; + let mut response = echo_request(destination, "10.0.0.1".parse().unwrap()) + .payload() + .to_vec(); + { + let mut ipv4 = Ipv4Packet::new_unchecked(&mut response); + ipv4.set_header_len(16); + let mut reply = Icmpv4Packet::new_unchecked(ipv4.payload_mut()); + reply.set_msg_type(Icmpv4Message::EchoReply); + reply.set_msg_code(0); + reply.set_echo_ident(7); + reply.set_echo_seq_no(11); + } + + assert!( + engine + .handle_socket_response(destination, &mut response) + .is_empty() ); + assert!(engine.nat_table.contains_key(&key)); } #[test] @@ -485,24 +522,23 @@ mod tests { .payload() .to_vec(); { - let mut ipv4 = MutableIpv4Packet::new(&mut response).unwrap(); - let mut reply = MutableEchoReplyPacket::new(ipv4.payload_mut()).unwrap(); - reply.set_icmp_type(IcmpTypes::EchoReply); - let mut icmp = MutableIcmpPacket::new(ipv4.payload_mut()).unwrap(); - icmp.set_checksum(icmp::checksum(&icmp.to_immutable())); - ipv4.set_source(destination); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut response); + let mut reply = Icmpv4Packet::new_unchecked(ipv4.payload_mut()); + reply.set_msg_type(Icmpv4Message::EchoReply); + reply.fill_checksum(); + ipv4.set_src_addr(destination); // Raw sockets may return a buffer with bytes beyond the IPv4 total // length. The native implementation composes from the received // buffer length, so keep that case covered without changing the // existing in-place composer in this refactor. - ipv4.set_total_length(1220); - ipv4.set_checksum(ipv4::checksum(&ipv4.to_immutable())); + ipv4.set_total_len(1220); + ipv4.fill_checksum(); } - let ipv4 = Ipv4Packet::new(&response).unwrap(); - let echo_reply = icmp::echo_reply::EchoReplyPacket::new(ipv4.payload()).unwrap(); - assert_eq!(echo_reply.get_icmp_type(), IcmpTypes::EchoReply); - assert_eq!(echo_reply.get_identifier(), 7); - assert_eq!(echo_reply.get_sequence_number(), 11); + let ipv4 = Ipv4Packet::new_checked(&response).unwrap(); + let echo_reply = Icmpv4Packet::new_checked(ipv4.payload()).unwrap(); + assert_eq!(echo_reply.msg_type(), Icmpv4Message::EchoReply); + assert_eq!(echo_reply.echo_ident(), 7); + assert_eq!(echo_reply.echo_seq_no(), 11); let replies = engine.handle_socket_response(destination, &mut response); assert_eq!(replies.len(), 3); diff --git a/easytier-core/src/gateway/proxy/ip_reassembler.rs b/easytier-core/src/gateway/proxy/ip_reassembler.rs index 875b60d5..8c8da483 100644 --- a/easytier-core/src/gateway/proxy/ip_reassembler.rs +++ b/easytier-core/src/gateway/proxy/ip_reassembler.rs @@ -4,8 +4,8 @@ use std::{ }; use dashmap::DashMap; +pub use smoltcp::wire::IpProtocol; use smoltcp::wire::Ipv4Packet; -pub use smoltcp::wire::{IpProtocol, Ipv4Packet as SmolIpv4Packet}; #[derive(Debug, Hash, PartialEq, Eq, Clone)] struct IpReassemblerKey { diff --git a/easytier-core/src/gateway/udp_broadcast.rs b/easytier-core/src/gateway/udp_broadcast.rs index ee95a92a..e293c55e 100644 --- a/easytier-core/src/gateway/udp_broadcast.rs +++ b/easytier-core/src/gateway/udp_broadcast.rs @@ -1,10 +1,8 @@ use std::net::Ipv4Addr; use cidr::Ipv4Inet; -use pnet_packet::{ - ip::IpNextHeaderProtocols, - ipv4::{self, Ipv4Flags, Ipv4Packet, MutableIpv4Packet}, - udp::{self, MutableUdpPacket, UdpPacket}, +use smoltcp::wire::{ + IPV4_HEADER_LEN, IpAddress, IpProtocol, Ipv4Packet, UDP_HEADER_LEN, UdpPacket, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -128,36 +126,37 @@ pub struct UdpPacketSummary { impl UdpPacketSummary { pub fn parse(packet: &[u8]) -> Option { - let ipv4_packet = Ipv4Packet::new(packet)?; - if ipv4_packet.get_version() != 4 - || ipv4_packet.get_next_level_protocol() != IpNextHeaderProtocols::Udp - { + if packet.len() < IPV4_HEADER_LEN { + return None; + } + let ipv4_packet = Ipv4Packet::new_unchecked(packet); + if ipv4_packet.version() != 4 || ipv4_packet.next_header() != IpProtocol::Udp { return None; } - let header_len = usize::from(ipv4_packet.get_header_length()) * 4; - let total_len = usize::from(ipv4_packet.get_total_length()); - if header_len < Ipv4Packet::minimum_packet_size() - || total_len < header_len + UdpPacket::minimum_packet_size() + let header_len = usize::from(ipv4_packet.header_len()); + let total_len = usize::from(ipv4_packet.total_len()); + if header_len < IPV4_HEADER_LEN + || total_len < header_len + UDP_HEADER_LEN || total_len > packet.len() { return None; } - let udp_packet = UdpPacket::new(&packet[header_len..total_len])?; - let udp_len = usize::from(udp_packet.get_length()); - if udp_len < UdpPacket::minimum_packet_size() || header_len + udp_len != total_len { + let udp_packet = UdpPacket::new_unchecked(&packet[header_len..total_len]); + let udp_len = usize::from(udp_packet.len()); + if udp_len < UDP_HEADER_LEN || header_len + udp_len != total_len { return None; } Some(Self { - src: ipv4_packet.get_source(), - dst: ipv4_packet.get_destination(), - src_port: udp_packet.get_source(), - dst_port: udp_packet.get_destination(), + src: ipv4_packet.src_addr(), + dst: ipv4_packet.dst_addr(), + src_port: udp_packet.src_port(), + dst_port: udp_packet.dst_port(), ip_len: total_len, udp_len, - payload_len: udp_len - UdpPacket::minimum_packet_size(), + payload_len: udp_len - UDP_HEADER_LEN, }) } } @@ -234,30 +233,29 @@ fn parse_udp_broadcast( packet: &[u8], config: &BroadcastRelayConfig, ) -> Result { - let ipv4_packet = Ipv4Packet::new(packet).ok_or(UdpBroadcastPacketRejection::MalformedIpv4)?; - if ipv4_packet.get_version() != 4 - || ipv4_packet.get_next_level_protocol() != IpNextHeaderProtocols::Udp - { + if packet.len() < IPV4_HEADER_LEN { + return Err(UdpBroadcastPacketRejection::MalformedIpv4); + } + let ipv4_packet = Ipv4Packet::new_unchecked(packet); + if ipv4_packet.version() != 4 || ipv4_packet.next_header() != IpProtocol::Udp { return Err(UdpBroadcastPacketRejection::NotUdpIpv4); } - if ipv4_packet.get_fragment_offset() != 0 - || ipv4_packet.get_flags() & Ipv4Flags::MoreFragments != 0 - { + if ipv4_packet.frag_offset() != 0 || ipv4_packet.more_frags() { return Err(UdpBroadcastPacketRejection::Fragmented); } - let header_len = usize::from(ipv4_packet.get_header_length()) * 4; - let total_len = usize::from(ipv4_packet.get_total_length()); - if header_len < Ipv4Packet::minimum_packet_size() - || total_len < header_len + UdpPacket::minimum_packet_size() + let header_len = usize::from(ipv4_packet.header_len()); + let total_len = usize::from(ipv4_packet.total_len()); + if header_len < IPV4_HEADER_LEN + || total_len < header_len + UDP_HEADER_LEN || total_len > packet.len() { return Err(UdpBroadcastPacketRejection::BadIpv4Length); } - let src = ipv4_packet.get_source(); - let dst = ipv4_packet.get_destination(); + let src = ipv4_packet.src_addr(); + let dst = ipv4_packet.dst_addr(); if should_ignore_interface_addr(src) { return Err(UdpBroadcastPacketRejection::IgnoredSource); } @@ -275,10 +273,9 @@ fn parse_udp_broadcast( return Err(UdpBroadcastPacketRejection::LoopbackDestination); } - let udp_packet = UdpPacket::new(&packet[header_len..total_len]) - .ok_or(UdpBroadcastPacketRejection::MalformedUdp)?; - let udp_len = usize::from(udp_packet.get_length()); - if udp_len < UdpPacket::minimum_packet_size() || header_len + udp_len != total_len { + let udp_packet = UdpPacket::new_unchecked(&packet[header_len..total_len]); + let udp_len = usize::from(udp_packet.len()); + if udp_len < UDP_HEADER_LEN || header_len + udp_len != total_len { return Err(UdpBroadcastPacketRejection::BadUdpLength); } @@ -301,27 +298,23 @@ pub fn normalize_udp_broadcast_packet( let mut normalized = packet[..packet_len].to_vec(); { - let mut ipv4_packet = MutableIpv4Packet::new(&mut normalized) - .ok_or(UdpBroadcastPacketRejection::MalformedIpv4)?; - ipv4_packet.set_source(virtual_ipv4); - ipv4_packet.set_destination(destination); - ipv4_packet.set_total_length(packet_len as u16); + let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut normalized); + ipv4_packet.set_src_addr(virtual_ipv4); + ipv4_packet.set_dst_addr(destination); + ipv4_packet.set_total_len(packet_len as u16); ipv4_packet.set_checksum(0); } { - let mut udp_packet = MutableUdpPacket::new(&mut normalized[header_len..packet_len]) - .ok_or(UdpBroadcastPacketRejection::MalformedUdp)?; - udp_packet.set_checksum(0); - let checksum = udp::ipv4_checksum(&udp_packet.to_immutable(), &virtual_ipv4, &destination); - udp_packet.set_checksum(checksum); + let mut udp_packet = UdpPacket::new_unchecked(&mut normalized[header_len..packet_len]); + udp_packet.fill_checksum( + &IpAddress::Ipv4(virtual_ipv4), + &IpAddress::Ipv4(destination), + ); } { - let mut ipv4_packet = MutableIpv4Packet::new(&mut normalized) - .ok_or(UdpBroadcastPacketRejection::MalformedIpv4)?; - let checksum = ipv4::checksum(&ipv4_packet.to_immutable()); - ipv4_packet.set_checksum(checksum); + Ipv4Packet::new_unchecked(&mut normalized).fill_checksum(); } Ok(NormalizedPacket { @@ -373,7 +366,6 @@ impl UdpBroadcastRelayStats { #[cfg(test)] mod tests { use super::*; - use pnet_packet::{MutablePacket, Packet}; fn config() -> BroadcastRelayConfig { BroadcastRelayConfig::new( @@ -385,47 +377,38 @@ mod tests { fn build_udp_packet(src: Ipv4Addr, dst: Ipv4Addr, payload: &[u8]) -> Vec { let mut packet = vec![0; 20 + 8 + payload.len()]; { - let mut ipv4_packet = MutableIpv4Packet::new(&mut packet).unwrap(); + let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut packet); ipv4_packet.set_version(4); - ipv4_packet.set_header_length(5); - ipv4_packet.set_total_length((20 + 8 + payload.len()) as u16); - ipv4_packet.set_ttl(64); - ipv4_packet.set_next_level_protocol(IpNextHeaderProtocols::Udp); - ipv4_packet.set_source(src); - ipv4_packet.set_destination(dst); + ipv4_packet.set_header_len(20); + ipv4_packet.set_total_len((20 + 8 + payload.len()) as u16); + ipv4_packet.set_hop_limit(64); + ipv4_packet.set_next_header(IpProtocol::Udp); + ipv4_packet.set_src_addr(src); + ipv4_packet.set_dst_addr(dst); } { - let mut udp_packet = MutableUdpPacket::new(&mut packet[20..]).unwrap(); - udp_packet.set_source(12345); - udp_packet.set_destination(37020); - udp_packet.set_length((8 + payload.len()) as u16); + let mut udp_packet = UdpPacket::new_unchecked(&mut packet[20..]); + udp_packet.set_src_port(12345); + udp_packet.set_dst_port(37020); + udp_packet.set_len((8 + payload.len()) as u16); udp_packet.payload_mut().copy_from_slice(payload); - let checksum = udp::ipv4_checksum(&udp_packet.to_immutable(), &src, &dst); - udp_packet.set_checksum(checksum); + udp_packet.fill_checksum(&IpAddress::Ipv4(src), &IpAddress::Ipv4(dst)); } - { - let mut ipv4_packet = MutableIpv4Packet::new(&mut packet).unwrap(); - let checksum = ipv4::checksum(&ipv4_packet.to_immutable()); - ipv4_packet.set_checksum(checksum); - } + Ipv4Packet::new_unchecked(&mut packet).fill_checksum(); packet } fn assert_valid_checksums(packet: &[u8]) { - let ipv4_packet = Ipv4Packet::new(packet).unwrap(); - assert_eq!(ipv4::checksum(&ipv4_packet), ipv4_packet.get_checksum()); - let udp_packet = UdpPacket::new(ipv4_packet.payload()).unwrap(); - assert_eq!( - udp::ipv4_checksum( - &udp_packet, - &ipv4_packet.get_source(), - &ipv4_packet.get_destination() - ), - udp_packet.get_checksum() - ); + let ipv4_packet = Ipv4Packet::new_checked(packet).unwrap(); + assert!(ipv4_packet.verify_checksum()); + let udp_packet = UdpPacket::new_checked(ipv4_packet.payload()).unwrap(); + assert!(udp_packet.verify_checksum( + &IpAddress::Ipv4(ipv4_packet.src_addr()), + &IpAddress::Ipv4(ipv4_packet.dst_addr()), + )); } #[test] @@ -433,11 +416,11 @@ mod tests { let packet = build_udp_packet(Ipv4Addr::new(192, 168, 1, 7), Ipv4Addr::BROADCAST, b"hello"); let normalized = normalize_udp_broadcast_packet(&packet, &config()).unwrap(); - let ipv4_packet = Ipv4Packet::new(&normalized.packet).unwrap(); + let ipv4_packet = Ipv4Packet::new_checked(&normalized.packet).unwrap(); assert_eq!(normalized.destination, Ipv4Addr::BROADCAST); - assert_eq!(ipv4_packet.get_source(), Ipv4Addr::new(10, 144, 144, 1)); - assert_eq!(ipv4_packet.get_destination(), Ipv4Addr::BROADCAST); + assert_eq!(ipv4_packet.src_addr(), Ipv4Addr::new(10, 144, 144, 1)); + assert_eq!(ipv4_packet.dst_addr(), Ipv4Addr::BROADCAST); assert_eq!(&ipv4_packet.payload()[8..], b"hello"); assert_valid_checksums(&normalized.packet); } @@ -451,14 +434,11 @@ mod tests { ); let normalized = normalize_udp_broadcast_packet(&packet, &config()).unwrap(); - let ipv4_packet = Ipv4Packet::new(&normalized.packet).unwrap(); + let ipv4_packet = Ipv4Packet::new_checked(&normalized.packet).unwrap(); assert_eq!(normalized.destination, Ipv4Addr::new(10, 144, 144, 255)); - assert_eq!(ipv4_packet.get_source(), Ipv4Addr::new(10, 144, 144, 1)); - assert_eq!( - ipv4_packet.get_destination(), - Ipv4Addr::new(10, 144, 144, 255) - ); + assert_eq!(ipv4_packet.src_addr(), Ipv4Addr::new(10, 144, 144, 1)); + assert_eq!(ipv4_packet.dst_addr(), Ipv4Addr::new(10, 144, 144, 255)); assert_eq!(&ipv4_packet.payload()[8..], b"directed"); assert_valid_checksums(&normalized.packet); } @@ -469,11 +449,11 @@ mod tests { let packet = build_udp_packet(Ipv4Addr::new(192, 168, 1, 7), multicast, b"multicast"); let normalized = normalize_udp_broadcast_packet(&packet, &config()).unwrap(); - let ipv4_packet = Ipv4Packet::new(&normalized.packet).unwrap(); + let ipv4_packet = Ipv4Packet::new_checked(&normalized.packet).unwrap(); assert_eq!(normalized.destination, multicast); - assert_eq!(ipv4_packet.get_source(), Ipv4Addr::new(10, 144, 144, 1)); - assert_eq!(ipv4_packet.get_destination(), multicast); + assert_eq!(ipv4_packet.src_addr(), Ipv4Addr::new(10, 144, 144, 1)); + assert_eq!(ipv4_packet.dst_addr(), multicast); assert_eq!(&ipv4_packet.payload()[8..], b"multicast"); assert_valid_checksums(&normalized.packet); } @@ -502,8 +482,8 @@ mod tests { b"fragment", ); { - let mut ipv4_packet = MutableIpv4Packet::new(&mut packet).unwrap(); - ipv4_packet.set_flags(Ipv4Flags::MoreFragments); + let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut packet); + ipv4_packet.set_more_frags(true); } assert_eq!( @@ -540,9 +520,7 @@ mod tests { fn rejects_non_udp_ipv4_packets() { let mut packet = build_udp_packet(Ipv4Addr::new(192, 168, 1, 7), Ipv4Addr::BROADCAST, b"tcp"); - MutableIpv4Packet::new(&mut packet) - .unwrap() - .set_next_level_protocol(IpNextHeaderProtocols::Tcp); + Ipv4Packet::new_unchecked(&mut packet).set_next_header(IpProtocol::Tcp); assert_eq!( normalize_udp_broadcast_packet(&packet, &config()), diff --git a/easytier-core/src/peers/route/peer_ospf_route.rs b/easytier-core/src/peers/route/peer_ospf_route.rs index a60fa205..7029c4f4 100644 --- a/easytier-core/src/peers/route/peer_ospf_route.rs +++ b/easytier-core/src/peers/route/peer_ospf_route.rs @@ -899,6 +899,9 @@ struct SyncedRouteInfo { foreign_network: DashMap, group_trust_map: DashMap>>, group_trust_map_cache: DashMap>>, // cache for group trust map, should sync with group_trust_map + // Serializes every read-modify-write of the derived group maps. Both + // proof verification and credential grants update these maps. + group_trust_update_lock: parking_lot::Mutex<()>, // Aggregated trusted credential pubkeys from all admin nodes // Maps pubkey bytes -> TrustedCredentialPubkey @@ -928,7 +931,8 @@ impl Debug for SyncedRouteInfo { #[allow(dead_code)] impl SyncedRouteInfo { - fn set_peer_groups(&self, peer_id: PeerId, groups: HashMap>) { + // Must be called with group_trust_update_lock held. + fn set_peer_groups_locked(&self, peer_id: PeerId, groups: HashMap>) { if groups.is_empty() { self.group_trust_map.remove(&peer_id); self.group_trust_map_cache.remove(&peer_id); @@ -941,7 +945,8 @@ impl SyncedRouteInfo { .insert(peer_id, Arc::new(group_names)); } - fn get_proof_groups(&self, peer_id: PeerId) -> HashMap> { + // Must be called with group_trust_update_lock held. + fn get_proof_groups_locked(&self, peer_id: PeerId) -> HashMap> { self.group_trust_map .get(&peer_id) .map(|groups| { @@ -1161,6 +1166,7 @@ impl SyncedRouteInfo { peer_infos: &OrderedHashMap, all_trusted: &HashMap, TrustedCredentialPubkey>, ) { + let _group_trust_lock = self.group_trust_update_lock.lock(); for (_, info) in peer_infos.iter() { if info.noise_static_pubkey.is_empty() { continue; @@ -1169,11 +1175,11 @@ impl SyncedRouteInfo { let Some(credential) = all_trusted.get(&info.noise_static_pubkey) else { continue; }; - let mut group_map = self.get_proof_groups(info.peer_id); + let mut group_map = self.get_proof_groups_locked(info.peer_id); for group in &credential.groups { group_map.entry(group.clone()).or_default(); } - self.set_peer_groups(info.peer_id, group_map); + self.set_peer_groups_locked(info.peer_id, group_map); } } @@ -1262,19 +1268,23 @@ impl SyncedRouteInfo { } } + { + let _group_trust_lock = self.group_trust_update_lock.lock(); + for peer_id in &peer_ids { + self.group_trust_map.remove(peer_id); + self.group_trust_map_cache.remove(peer_id); + } + shrink_dashmap(&self.group_trust_map, None); + shrink_dashmap(&self.group_trust_map_cache, None); + } for peer_id in &peer_ids { self.raw_peer_infos.remove(peer_id); - self.group_trust_map.remove(peer_id); - self.group_trust_map_cache.remove(peer_id); } self.foreign_network .retain(|k, _| !peer_ids.contains(&k.peer_id)); shrink_dashmap(&self.raw_peer_infos, None); shrink_dashmap(&self.foreign_network, None); - shrink_dashmap(&self.group_trust_map, None); - shrink_dashmap(&self.group_trust_map_cache, None); - self.version.inc(); } @@ -1652,6 +1662,7 @@ impl SyncedRouteInfo { local_group_declarations: &[PeerGroupIdentity], trust_admin_groups_without_proof: bool, ) { + let _group_trust_lock = self.group_trust_update_lock.lock(); let local_group_declarations = local_group_declarations .iter() .map(|g| (g.group_name.as_str(), g.group_secret.as_str())) @@ -1719,14 +1730,59 @@ impl SyncedRouteInfo { } } + fn verify_and_update_current_group_trusts( + &self, + received_peer_infos: &[RoutePeerInfo], + local_group_declarations: &[PeerGroupIdentity], + trust_admin_groups_without_proof: bool, + ) { + let peer_ids: HashSet<_> = received_peer_infos + .iter() + .map(|info| info.peer_id) + .collect(); + let peer_infos_guard = self.peer_infos.read(); + let current_peer_infos: Vec<_> = peer_ids + .iter() + .filter_map(|peer_id| peer_infos_guard.get(peer_id).cloned()) + .collect(); + self.verify_and_update_group_trusts( + ¤t_peer_infos, + local_group_declarations, + trust_admin_groups_without_proof, + ); + // Keep the authoritative peer-info snapshot locked until its derived + // ACL cache has been updated, so another sync cannot interleave a + // newer peer version and then be overwritten by this one. + drop(peer_infos_guard); + } + + fn verify_and_update_all_current_group_trusts( + &self, + local_group_declarations: &[PeerGroupIdentity], + trust_admin_groups_without_proof: bool, + ) { + let peer_infos_guard = self.peer_infos.read(); + let current_peer_infos: Vec<_> = peer_infos_guard + .iter() + .map(|(_, info)| info.clone()) + .collect(); + self.verify_and_update_group_trusts( + ¤t_peer_infos, + local_group_declarations, + trust_admin_groups_without_proof, + ); + drop(peer_infos_guard); + } + fn update_my_group_trusts(&self, my_peer_id: PeerId, groups: &[PeerGroupInfo]) { + let _group_trust_lock = self.group_trust_update_lock.lock(); let mut my_group_map = HashMap::new(); for group in groups.iter() { my_group_map.insert(group.group_name.clone(), group.group_proof.clone()); } - self.set_peer_groups(my_peer_id, my_group_map); + self.set_peer_groups_locked(my_peer_id, my_group_map); } /// Collect trusted credential pubkeys from admin nodes (network_secret holders) @@ -1835,6 +1891,13 @@ type SessionId = u64; type AtomicSessionId = atomic_shim::AtomicU64; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct SyncRequestSnapshot { + my_session_id: SessionId, + state_revision: u64, + is_initiator: bool, +} + struct SessionTask { my_peer_id: PeerId, task: Arc>>>, @@ -1921,6 +1984,13 @@ impl VersionAndTouchTime { } } +// A responder session sends no keepalives when there is no new route data, +// so the only sign that the initiator still owns the session is its +// periodic inbound syncs (forced at least every ~10s by session_task). +// Treat a longer silence as the initiator having lost this session (e.g. +// restarted) without telling us. +const INITIATOR_SESSION_LIVENESS_TIMEOUT: Duration = Duration::from_secs(45); + // if we need to sync route info with one peer, we create a SyncRouteSession with that peer. #[derive(Debug)] #[allow(dead_code)] @@ -1937,6 +2007,11 @@ struct SyncRouteSession { last_sync_succ_timestamp: AtomicCell>, + // Last time any sync interaction (inbound request or successful + // response) confirmed the peer still holds this session. Drives the + // responder liveness timeout; initialized at session creation. + last_contact_instant: AtomicCell, + my_session_id: AtomicSessionId, dst_session_id: AtomicSessionId, @@ -1944,6 +2019,11 @@ struct SyncRouteSession { we_are_initiator: AtomicBool, dst_is_initiator: AtomicBool, + // Serializes the compound session state observed by an outbound RPC. + // A response may only commit while this revision still matches the + // request snapshot captured before the await. + state_revision: AtomicU64, + need_sync_initiator_info: AtomicBool, rpc_tx_count: AtomicU32, @@ -1968,11 +2048,14 @@ impl SyncRouteSession { last_sync_succ_timestamp: AtomicCell::new(None), + last_contact_instant: AtomicCell::new(Instant::now()), + my_session_id: AtomicSessionId::new(rand::random()), dst_session_id: AtomicSessionId::new(0), we_are_initiator: AtomicBool::new(false), dst_is_initiator: AtomicBool::new(false), + state_revision: AtomicU64::new(0), need_sync_initiator_info: AtomicBool::new(false), @@ -2111,24 +2194,117 @@ impl SyncRouteSession { } fn update_initiator_flag(&self, is_initiator: bool) { - self.we_are_initiator.store(is_initiator, Ordering::Relaxed); + let _session_lock = self.lock.lock(); + if self.we_are_initiator.load(Ordering::Relaxed) != is_initiator { + self.we_are_initiator.store(is_initiator, Ordering::Relaxed); + self.state_revision.fetch_add(1, Ordering::Relaxed); + } self.need_sync_initiator_info.store(true, Ordering::Relaxed); } - // return whether session id is updated - fn update_dst_session_id(&self, session_id: SessionId) { - if session_id != self.dst_session_id.load(Ordering::Relaxed) { + // Must be called with the session lock held. + fn update_remote_state_locked(&self, session_id: SessionId, is_initiator: bool) { + let session_id_changed = session_id != self.dst_session_id.load(Ordering::Relaxed); + let initiator_changed = is_initiator != self.dst_is_initiator.load(Ordering::Relaxed); + + if session_id_changed { tracing::warn!(?self, ?session_id, "session id mismatch, clear saved info."); self.dst_session_id.store(session_id, Ordering::Relaxed); self.dst_saved_conn_info_version.clear(); self.dst_saved_peer_info_versions.clear(); + self.dst_saved_foreign_network_versions.clear(); - // update_dst_session_id is always called with session lock held, so clear - // last_sync_succ_timestamp and unreachable_peers non-atomic is safe. self.last_sync_succ_timestamp.store(None); self.unreachable_peers_for_peer_info.lock().clear(); self.unreachable_peers_for_conn_info.lock().clear(); } + + if initiator_changed { + self.dst_is_initiator.store(is_initiator, Ordering::Relaxed); + } + + if session_id_changed || initiator_changed { + self.state_revision.fetch_add(1, Ordering::Relaxed); + } + } + + // Must be called with the session lock held. A different generation may + // only take over through an initiator request after the previous remote + // initiator role has been relinquished or expired. + fn admit_inbound_locked(&self, session_id: SessionId, is_initiator: bool) -> bool { + let current_session_id = self.dst_session_id.load(Ordering::Relaxed); + if session_id == current_session_id { + self.update_remote_state_locked(session_id, is_initiator); + return true; + } + + if current_session_id == 0 && !is_initiator && self.we_are_initiator.load(Ordering::Relaxed) + { + // The responder can send its first reverse sync before our + // initiating RPC response arrives and teaches us its session ID. + self.update_remote_state_locked(session_id, false); + return true; + } + + if !is_initiator || self.dst_is_initiator.load(Ordering::Relaxed) { + return false; + } + + self.update_remote_state_locked(session_id, true); + true + } + + // Must be called with the session lock held. + fn request_snapshot_locked(&self) -> SyncRequestSnapshot { + SyncRequestSnapshot { + my_session_id: self.my_session_id.load(Ordering::Relaxed), + state_revision: self.state_revision.load(Ordering::Relaxed), + is_initiator: self.we_are_initiator.load(Ordering::Relaxed), + } + } + + // Must be called with the session lock held. + fn request_is_current_locked(&self, snapshot: SyncRequestSnapshot) -> bool { + snapshot.my_session_id == self.my_session_id.load(Ordering::Relaxed) + && snapshot.state_revision == self.state_revision.load(Ordering::Relaxed) + && snapshot.is_initiator == self.we_are_initiator.load(Ordering::Relaxed) + } + + // Must be called with the session lock held. + fn clear_dst_initiator_if_session_unchanged_locked( + &self, + expected_dst_session_id: SessionId, + ) -> bool { + if self.dst_session_id.load(Ordering::Relaxed) != expected_dst_session_id { + return false; + } + + self.update_remote_state_locked(expected_dst_session_id, false); + true + } + + // Responder sessions have no outbound keepalive, so the only sign that + // the initiator still owns the session is its periodic inbound syncs. + // If no sync interaction arrived within INITIATOR_SESSION_LIVENESS_TIMEOUT, + // assume the initiator lost this session without telling us and clear the + // stale role so the election loop can re-establish the edge. Only the + // flag is cleared; the election loop takes over from there. Returns true + // if the initiator role was cleared. + fn clear_stale_dst_initiator(&self) -> bool { + let _session_lock = self.lock.lock(); + if !self.dst_is_initiator.load(Ordering::Relaxed) + || self.we_are_initiator.load(Ordering::Relaxed) + { + return false; + } + + if self.last_contact_instant.load().elapsed() < INITIATOR_SESSION_LIVENESS_TIMEOUT { + return false; + } + + let session_id = self.dst_session_id.load(Ordering::Relaxed); + self.update_remote_state_locked(session_id, false); + true } fn clean_dst_saved_map(&self) { @@ -2256,6 +2432,7 @@ impl PeerRouteServiceImpl { foreign_network: DashMap::new(), group_trust_map: DashMap::new(), group_trust_map_cache: DashMap::new(), + group_trust_update_lock: parking_lot::Mutex::new(()), trusted_credential_pubkeys: DashMap::new(), non_reusable_credential_owners: DashMap::new(), suppressed_non_reusable_credential_peers: DashMap::new(), @@ -2312,8 +2489,32 @@ impl PeerRouteServiceImpl { self.sessions.get(&dst_peer_id).map(|x| x.value().clone()) } + fn is_current_session(&self, dst_peer_id: PeerId, expected: &Arc) -> bool { + self.sessions + .get(&dst_peer_id) + .is_some_and(|current| Arc::ptr_eq(current.value(), expected)) + } + + // Must be called with the expected session lock held. remove_session + // takes the same lock before changing the map, so this check remains + // valid until the caller finishes committing the RPC result. + fn sync_request_is_current_locked( + &self, + dst_peer_id: PeerId, + expected: &Arc, + snapshot: SyncRequestSnapshot, + ) -> bool { + self.is_current_session(dst_peer_id, expected) + && expected.request_is_current_locked(snapshot) + } + fn remove_session(&self, dst_peer_id: PeerId) { - self.sessions.remove(&dst_peer_id); + let Some(session) = self.get_session(dst_peer_id) else { + return; + }; + let _session_lock = session.lock.lock(); + self.sessions + .remove_if(&dst_peer_id, |_, current| Arc::ptr_eq(current, &session)); shrink_dashmap(&self.sessions, None); } @@ -2804,18 +3005,11 @@ impl PeerRouteServiceImpl { let trust_admin_groups_without_proof = self.context.network_identity().network_secret.is_none(); - let peer_infos: Vec<_> = self - .synced_route_info - .peer_infos - .read() - .iter() - .map(|(_, info)| info.clone()) - .collect(); - self.synced_route_info.verify_and_update_group_trusts( - &peer_infos, - &self.context.acl_group_declarations(), - trust_admin_groups_without_proof, - ); + self.synced_route_info + .verify_and_update_all_current_group_trusts( + &self.context.acl_group_declarations(), + trust_admin_groups_without_proof, + ); let untrusted = self.refresh_credential_trusts_with_current_topology(); self.disconnect_untrusted_peers(&untrusted).await; @@ -3028,6 +3222,8 @@ impl PeerRouteServiceImpl { let next_last_sync_succ_timestamp = self.synced_route_info.get_next_last_sync_succ_timestamp(); + let request_snapshot = session.request_snapshot_locked(); + let expected_dst_session_id = session.dst_session_id.load(Ordering::Relaxed); let (peer_infos, conn_info, foreign_network) = self.build_sync_request(&session, dst_peer_id); if peer_infos.is_none() @@ -3064,8 +3260,8 @@ impl PeerRouteServiceImpl { let sync_route_info_req = SyncRouteInfoRequest { my_peer_id, - my_session_id: session.my_session_id.load(Ordering::Relaxed), - is_initiator: session.we_are_initiator.load(Ordering::Relaxed), + my_session_id: request_snapshot.my_session_id, + is_initiator: request_snapshot.is_initiator, peer_infos: peer_infos.clone().map(|x| RoutePeerInfos { items: x }), conn_info: conn_info.clone(), foreign_network_infos: foreign_network.clone(), @@ -3100,6 +3296,16 @@ impl PeerRouteServiceImpl { next_last_sync_succ_timestamp ); + if !self.sync_request_is_current_locked(dst_peer_id, &session, request_snapshot) { + tracing::debug!( + ?my_peer_id, + ?dst_peer_id, + ?request_snapshot, + "discard stale route sync response" + ); + return false; + } + match ret.as_ref() { Err(e) => { tracing::error!( @@ -3115,7 +3321,17 @@ impl PeerRouteServiceImpl { } Ok(resp) => { if let Some(err) = resp.error { - if err == Error::DuplicatePeerId as i32 { + if err == Error::Stopped as i32 && !sync_route_info_req.is_initiator { + let cleared = session.clear_dst_initiator_if_session_unchanged_locked( + expected_dst_session_id, + ); + tracing::debug!( + ?my_peer_id, + ?dst_peer_id, + ?cleared, + "stale non-initiator route sync rejected" + ); + } else if err == Error::DuplicatePeerId as i32 { if !self.context.feature_flags().is_public_server { panic!("duplicate peer id"); } @@ -3127,12 +3343,9 @@ impl PeerRouteServiceImpl { } } else { session.rpc_tx_count.fetch_add(1, Ordering::Relaxed); + session.last_contact_instant.store(Instant::now()); - session - .dst_is_initiator - .store(resp.is_initiator, Ordering::Relaxed); - - session.update_dst_session_id(resp.session_id); + session.update_remote_state_locked(resp.session_id, resp.is_initiator); if let Some(peer_infos) = &peer_infos { session.update_dst_saved_peer_info_version(peer_infos, dst_peer_id); @@ -3399,6 +3612,23 @@ impl RouteSessionManager { } } + // Detect responder sessions whose initiator silently lost the + // session (e.g. restarted and elected someone else). Without + // this, a responder with no new route data could wait forever + // for an initiator that no longer syncs with us. Clearing the + // stale role makes the peer an initiator candidate again below. + for peer_id in session_peers.iter() { + if let Some(session) = service_impl.get_session(*peer_id) + && session.clear_stale_dst_initiator() + { + tracing::warn!( + ?peer_id, + my_peer_id = ?service_impl.my_peer_id, + "initiator route sync liveness timeout, clearing stale initiator role" + ); + } + } + // find peer_ids that are not initiators. let mut initiator_candidates = Vec::new(); for peer_id in peers.iter().copied() { @@ -3573,7 +3803,17 @@ impl RouteSessionManager { } let my_peer_id = service_impl.my_peer_id; - let session = self.get_or_start_session(from_peer_id)?; + let session = if let Some(session) = service_impl.get_session(from_peer_id) { + session + } else if is_initiator { + self.get_or_start_session(from_peer_id)? + } else { + tracing::debug!( + ?from_peer_id, + "ignore stale route sync from non-initiator without a session" + ); + return Err(Error::Stopped); + }; let from_identity_type = service_impl .get_peer_identity_type_from_interface(from_peer_id) @@ -3599,9 +3839,20 @@ impl RouteSessionManager { let _session_lock = session.lock.lock(); - session.rpc_rx_count.fetch_add(1, Ordering::Relaxed); + if !service_impl.is_current_session(from_peer_id, &session) + || !session.admit_inbound_locked(from_session_id, is_initiator) + { + tracing::debug!( + ?from_peer_id, + ?from_session_id, + ?is_initiator, + "ignore route sync from a stale session generation" + ); + return Err(Error::Stopped); + } - session.update_dst_session_id(from_session_id); + session.rpc_rx_count.fetch_add(1, Ordering::Relaxed); + session.last_contact_instant.store(Instant::now()); let mut need_update_route_table = false; let mut untrusted_peers = Vec::new(); @@ -3637,7 +3888,7 @@ impl RouteSessionManager { )?; service_impl .synced_route_info - .verify_and_update_group_trusts( + .verify_and_update_current_group_trusts( pi, &service_impl.context.acl_group_declarations(), trust_admin_groups_without_proof, @@ -3694,9 +3945,6 @@ impl RouteSessionManager { service_impl.route_table ); - session - .dst_is_initiator - .store(is_initiator, Ordering::Relaxed); let is_initiator = session.we_are_initiator.load(Ordering::Relaxed); let session_id = session.my_session_id.load(Ordering::Relaxed); @@ -4385,6 +4633,29 @@ mod tests { PeerRouteServiceImpl::new(my_peer_id, Arc::new(NoopPeerContext::default())) } + async fn test_route_with_admin_peer( + context: ArcPeerContext, + ) -> (Arc, Arc) { + let peer_rpc = Arc::new(PeerRpcManager::new(TestPeerRpcTransport)); + let route = PeerRoute::new( + 1, + context, + Arc::new(TestPublicIpv6Runtime), + peer_rpc.clone(), + ); + *route.service_impl.interface.lock().await = Some(Box::new(CountingInterface { + my_peer_id: 1, + peers: Arc::new(Mutex::new(vec![2])), + peer_identity_types: Arc::new(Mutex::new(HashMap::from([( + 2, + Some(PeerIdentityType::Admin), + )]))), + list_peers_calls: Arc::new(AtomicU32::new(0)), + get_peer_identity_type_calls: Arc::new(AtomicU32::new(0)), + })); + (route, peer_rpc) + } + fn peer(peer_id: PeerId) -> OspfPeerInfo { OspfPeerInfo { peer_id, @@ -4522,6 +4793,381 @@ mod tests { assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4); } + #[test] + fn stopped_sync_clears_matching_remote_initiator() { + let session = SyncRouteSession::new(1, 2); + let _session_lock = session.lock.lock(); + session.update_remote_state_locked(10, true); + + assert!(session.clear_dst_initiator_if_session_unchanged_locked(10)); + assert!(!session.dst_is_initiator.load(Ordering::Relaxed)); + } + + #[test] + fn stopped_sync_preserves_newer_remote_initiator() { + let session = SyncRouteSession::new(1, 2); + let _session_lock = session.lock.lock(); + session.update_remote_state_locked(10, true); + + session.update_remote_state_locked(11, true); + + assert!(!session.clear_dst_initiator_if_session_unchanged_locked(10)); + assert!(session.dst_is_initiator.load(Ordering::Relaxed)); + } + + #[test] + fn remote_state_change_invalidates_outbound_snapshot() { + let service_impl = Arc::new(test_service_impl(1)); + let session = service_impl.get_or_create_session(2); + let _session_lock = session.lock.lock(); + session.update_remote_state_locked(10, false); + let snapshot = session.request_snapshot_locked(); + + assert!(session.admit_inbound_locked(20, true)); + assert!(!service_impl.sync_request_is_current_locked(2, &session, snapshot)); + assert_eq!(session.dst_session_id.load(Ordering::Relaxed), 20); + assert!(session.dst_is_initiator.load(Ordering::Relaxed)); + assert_eq!(session.rpc_tx_count.load(Ordering::Relaxed), 0); + assert!(session.dst_saved_peer_info_versions.is_empty()); + } + + #[test] + fn replaced_session_invalidates_outbound_snapshot() { + let service_impl = Arc::new(test_service_impl(1)); + let old_session = service_impl.get_or_create_session(2); + let snapshot = { + let _session_lock = old_session.lock.lock(); + old_session.request_snapshot_locked() + }; + + service_impl.remove_session(2); + let new_session = service_impl.get_or_create_session(2); + let _old_session_lock = old_session.lock.lock(); + + assert!(!Arc::ptr_eq(&old_session, &new_session)); + assert!(!service_impl.sync_request_is_current_locked(2, &old_session, snapshot)); + } + + #[test] + fn active_remote_initiator_rejects_different_generation() { + let session = SyncRouteSession::new(1, 2); + { + let _session_lock = session.lock.lock(); + assert!(session.admit_inbound_locked(20, true)); + assert!(!session.admit_inbound_locked(10, true)); + assert_eq!(session.dst_session_id.load(Ordering::Relaxed), 20); + } + + session + .last_contact_instant + .store(Instant::now() - INITIATOR_SESSION_LIVENESS_TIMEOUT - Duration::from_secs(1)); + assert!(session.clear_stale_dst_initiator()); + + let _session_lock = session.lock.lock(); + assert!(session.admit_inbound_locked(10, true)); + assert_eq!(session.dst_session_id.load(Ordering::Relaxed), 10); + } + + #[test] + fn local_initiator_accepts_initial_responder_generation() { + let session = SyncRouteSession::new(1, 2); + session.update_initiator_flag(true); + + let _session_lock = session.lock.lock(); + assert!(session.admit_inbound_locked(20, false)); + assert_eq!(session.dst_session_id.load(Ordering::Relaxed), 20); + assert!(!session.dst_is_initiator.load(Ordering::Relaxed)); + } + + #[test] + fn stale_responder_session_clears_initiator_after_liveness_timeout() { + let session = SyncRouteSession::new(1, 2); + { + let _session_lock = session.lock.lock(); + session.update_remote_state_locked(10, true); + } + session + .last_contact_instant + .store(Instant::now() - INITIATOR_SESSION_LIVENESS_TIMEOUT - Duration::from_secs(1)); + + assert!(session.clear_stale_dst_initiator()); + assert!(!session.dst_is_initiator.load(Ordering::Relaxed)); + } + + #[test] + fn healthy_responder_session_keeps_initiator() { + let session = SyncRouteSession::new(1, 2); + { + let _session_lock = session.lock.lock(); + session.update_remote_state_locked(10, true); + } + + assert!(!session.clear_stale_dst_initiator()); + assert!(session.dst_is_initiator.load(Ordering::Relaxed)); + } + + #[test] + fn initiator_session_ignores_liveness_timeout() { + let session = SyncRouteSession::new(1, 2); + session.update_initiator_flag(true); + { + let _session_lock = session.lock.lock(); + session.update_remote_state_locked(10, true); + } + session + .last_contact_instant + .store(Instant::now() - INITIATOR_SESSION_LIVENESS_TIMEOUT - Duration::from_secs(1)); + + assert!(!session.clear_stale_dst_initiator()); + assert!(session.dst_is_initiator.load(Ordering::Relaxed)); + } + + #[tokio::test] + async fn initiator_sync_creates_session_and_marks_contact() { + let peer_rpc = Arc::new(PeerRpcManager::new(TestPeerRpcTransport)); + let route = PeerRoute::new( + 1, + Arc::new(NoopPeerContext::default()), + Arc::new(TestPublicIpv6Runtime), + peer_rpc, + ); + let peers = Arc::new(Mutex::new(vec![2])); + *route.service_impl.interface.lock().await = Some(Box::new(CountingInterface { + my_peer_id: 1, + peers, + peer_identity_types: Arc::new(Mutex::new(HashMap::from([( + 2, + Some(PeerIdentityType::Admin), + )]))), + list_peers_calls: Arc::new(AtomicU32::new(0)), + get_peer_identity_type_calls: Arc::new(AtomicU32::new(0)), + })); + + route + .session_mgr + .do_sync_route_info(2, 1, true, None, None, None, None) + .await + .expect("initiator sync should succeed"); + + let session = route + .service_impl + .get_session(2) + .expect("initiator sync should create the session"); + assert!(session.dst_is_initiator.load(Ordering::Relaxed)); + assert!( + session.last_contact_instant.load().elapsed() < Duration::from_secs(5), + "inbound sync should refresh the liveness timestamp" + ); + + route.stop().await; + assert!(route.service_impl.sessions.is_empty()); + assert_eq!(route.task_count(), 0); + } + + #[tokio::test] + async fn stale_non_initiator_sync_does_not_create_session() { + let peer_rpc = Arc::new(PeerRpcManager::new(TestPeerRpcTransport)); + let route = PeerRoute::new( + 1, + Arc::new(NoopPeerContext::default()), + Arc::new(TestPublicIpv6Runtime), + peer_rpc, + ); + let peers = Arc::new(Mutex::new(vec![2])); + *route.service_impl.interface.lock().await = Some(Box::new(CountingInterface { + my_peer_id: 1, + peers, + peer_identity_types: Arc::new(Mutex::new(HashMap::from([( + 2, + Some(PeerIdentityType::Admin), + )]))), + list_peers_calls: Arc::new(AtomicU32::new(0)), + get_peer_identity_type_calls: Arc::new(AtomicU32::new(0)), + })); + + let result = route + .session_mgr + .do_sync_route_info(2, 1, false, None, None, None, None) + .await; + + assert!(matches!(result, Err(Error::Stopped))); + assert!(route.service_impl.sessions.is_empty()); + assert_eq!(route.task_count(), 0); + } + + #[tokio::test] + async fn stale_non_initiator_sync_preserves_newer_session_generation() { + let (route, _peer_rpc) = + test_route_with_admin_peer(Arc::new(NoopPeerContext::default())).await; + let session = route.service_impl.get_or_create_session(2); + + route + .session_mgr + .do_sync_route_info(2, 22, true, None, None, None, None) + .await + .expect("new initiator generation should be accepted"); + let contact = session.last_contact_instant.load(); + let rx_count = session.rpc_rx_count.load(Ordering::Relaxed); + + let stale_peer_info = RoutePeerInfo { + peer_id: 3, + version: 1, + ..Default::default() + }; + let result = route + .session_mgr + .do_sync_route_info( + 2, + 11, + false, + Some(vec![stale_peer_info.clone()]), + Some(vec![raw_route_peer_info(&stale_peer_info)]), + None, + None, + ) + .await; + + assert!(matches!(result, Err(Error::Stopped))); + assert_eq!(session.dst_session_id.load(Ordering::Relaxed), 22); + assert!(session.dst_is_initiator.load(Ordering::Relaxed)); + assert_eq!(session.last_contact_instant.load(), contact); + assert_eq!(session.rpc_rx_count.load(Ordering::Relaxed), rx_count); + assert!( + !route + .service_impl + .synced_route_info + .peer_infos + .read() + .contains_key(&3) + ); + + route.stop().await; + } + + #[tokio::test] + async fn stale_peer_info_does_not_restore_removed_acl_group() { + let context = Arc::new(NoopPeerContext::new(CoreNetworkIdentity::new_credential( + "default".to_owned(), + ))); + let (route, _peer_rpc) = test_route_with_admin_peer(context).await; + route.service_impl.get_or_create_session(2); + + let peer_info = |version, groups| RoutePeerInfo { + peer_id: 3, + peer_route_id: 30, + version, + groups, + ..Default::default() + }; + let sync_peer_info = |info: RoutePeerInfo| { + let raw = raw_route_peer_info(&info); + (Some(vec![info]), Some(vec![raw])) + }; + + let v1 = peer_info( + 1, + vec![PeerGroupInfo { + group_name: "legacy".to_owned(), + group_proof: Vec::new(), + }], + ); + let (peer_infos, raw_peer_infos) = sync_peer_info(v1.clone()); + route + .session_mgr + .do_sync_route_info(2, 22, true, peer_infos, raw_peer_infos, None, None) + .await + .expect("v1 peer info should be accepted"); + assert_eq!(route.service_impl.get_peer_groups(3).as_ref(), &["legacy"]); + + let v2 = peer_info(2, Vec::new()); + let (peer_infos, raw_peer_infos) = sync_peer_info(v2); + route + .session_mgr + .do_sync_route_info(2, 22, true, peer_infos, raw_peer_infos, None, None) + .await + .expect("v2 peer info should be accepted"); + assert!(route.service_impl.get_peer_groups(3).is_empty()); + + let (peer_infos, raw_peer_infos) = sync_peer_info(v1); + route + .session_mgr + .do_sync_route_info(2, 22, true, peer_infos, raw_peer_infos, None, None) + .await + .expect("stale peer info should be ignored"); + + assert_eq!( + route + .service_impl + .synced_route_info + .peer_infos + .read() + .get(&3) + .expect("peer info should remain present") + .version, + 2 + ); + assert!(route.service_impl.get_peer_groups(3).is_empty()); + + route.stop().await; + } + + #[test] + fn credential_group_refresh_does_not_restore_removed_proof_group() { + let service_impl = Arc::new(test_service_impl(1)); + let mut peer_infos = OrderedHashMap::new(); + peer_infos.insert( + 3, + RoutePeerInfo { + peer_id: 3, + version: 3, + noise_static_pubkey: vec![3; 32], + ..Default::default() + }, + ); + let all_trusted = HashMap::from([( + vec![3; 32], + TrustedCredentialPubkey { + groups: vec!["credential".to_owned()], + ..Default::default() + }, + )]); + + let group_trust_lock = service_impl + .synced_route_info + .group_trust_update_lock + .lock(); + service_impl + .synced_route_info + .set_peer_groups_locked(3, HashMap::from([("legacy".to_owned(), vec![1])])); + + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let credential_refresh = std::thread::spawn({ + let service_impl = service_impl.clone(); + move || { + started_tx.send(()).unwrap(); + service_impl + .synced_route_info + .update_credential_groups(&peer_infos, &all_trusted); + } + }); + started_rx.recv().unwrap(); + + service_impl + .synced_route_info + .set_peer_groups_locked(3, HashMap::new()); + drop(group_trust_lock); + credential_refresh.join().unwrap(); + + assert_eq!( + service_impl + .synced_route_info + .group_trust_map + .get(&3) + .as_deref(), + Some(&HashMap::from([("credential".to_owned(), Vec::new())])) + ); + } + #[tokio::test] async fn stop_waits_for_in_flight_route_sync_before_draining_sessions() { let peer_rpc = Arc::new(PeerRpcManager::new(TestPeerRpcTransport)); @@ -4542,7 +5188,7 @@ mod tests { let session_mgr = route.session_mgr.clone(); async move { session_mgr - .do_sync_route_info(2, 1, false, None, None, None, None) + .do_sync_route_info(2, 1, true, None, None, None, None) .await } }); diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 512cc43b..e55b1cf4 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -152,7 +152,14 @@ socket2 = { version = "0.5.10", features = ["all"] } rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } -pnet = { version = "0.35.0", features = ["serde"] } +pnet_datalink = { version = "0.35.0", optional = true } +smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp.git", rev = "0a926767a68bc88d5512afefa7529c5ecdade4ea", optional = true, default-features = false, features = [ + "std", + "medium-ethernet", + "proto-ipv4", + "proto-ipv6", + "socket-raw", +] } serde_json = "1" clap = { version = "4.5.30", features = [ @@ -167,7 +174,7 @@ clap_complete_nushell = { version = "4.5.10" } async-recursion = "1.0.5" -network-interface = "2.0" +network-interface = "2.0.5" # for wireguard boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true } @@ -287,6 +294,13 @@ ctor = "0.8.0" stun_codec = "0.3.4" bytecodec = "0.4.15" x25519-dalek = { version = "2.0", features = ["static_secrets"] } +smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp.git", rev = "0a926767a68bc88d5512afefa7529c5ecdade4ea", default-features = false, features = [ + "std", + "medium-ethernet", + "proto-ipv4", + "proto-ipv6", + "socket-raw", +] } [target.'cfg(target_os = "linux")'.dev-dependencies] defguard_wireguard_rs = "0.4.2" @@ -375,7 +389,12 @@ magic-dns = [ "easytier-core/proxy-packet", "easytier-proto/magic-dns", ] -faketcp = ["dep:flume", "easytier-proto/faketcp"] +faketcp = [ + "dep:flume", + "dep:pnet_datalink", + "dep:smoltcp", + "easytier-proto/faketcp", +] zstd = ["easytier-core/zstd", "easytier-proto/zstd"] upnp = ["dep:igd-next", "dep:natpmp"] endpoint-discovery = [ diff --git a/easytier/src/common/ifcfg/netlink.rs b/easytier/src/common/ifcfg/netlink.rs index 5cd8f728..a401e744 100644 --- a/easytier/src/common/ifcfg/netlink.rs +++ b/easytier/src/common/ifcfg/netlink.rs @@ -11,6 +11,17 @@ use std::{ os::fd::AsRawFd, }; +use super::{ + Error, IfConfiguerTrait, + netlink_wire::{ + AddressMessage, MessageBuilder, MessageIter, NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, + NLM_F_DUMP_INTR, NLM_F_EXCL, NLM_F_REQUEST, NLMSG_DONE, NLMSG_ERROR, NeighborMessage, + NetlinkDecode, NetlinkEncode, RTM_DELADDR, RTM_DELNEIGH, RTM_DELROUTE, RTM_GETNEIGH, + RTM_GETROUTE, RTM_NEWADDR, RTM_NEWNEIGH, RTM_NEWROUTE, RouteMessage, RouteMessageBuilder, + RouteType, netlink_error_code, + }, +}; +use crate::common::network::ip_mask_to_prefix; use anyhow::Context; use async_trait::async_trait; use cidr::{IpInet, Ipv4Inet, Ipv6Inet}; @@ -23,18 +34,6 @@ use nix::{ net::if_::InterfaceFlags, sys::socket::SockaddrLike as _, }; -use pnet::ipnetwork::ip_mask_to_prefix; - -use super::{ - Error, IfConfiguerTrait, - netlink_wire::{ - AddressMessage, MessageBuilder, MessageIter, NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, - NLM_F_DUMP_INTR, NLM_F_EXCL, NLM_F_REQUEST, NLMSG_DONE, NLMSG_ERROR, NeighborMessage, - NetlinkDecode, NetlinkEncode, RTM_DELADDR, RTM_DELNEIGH, RTM_DELROUTE, RTM_GETNEIGH, - RTM_GETROUTE, RTM_NEWADDR, RTM_NEWNEIGH, RTM_NEWROUTE, RouteMessage, RouteMessageBuilder, - RouteType, netlink_error_code, - }, -}; pub(crate) fn dummy_socket() -> Result { Ok(std::net::UdpSocket::bind("0:0")?) diff --git a/easytier/src/common/network.rs b/easytier/src/common/network.rs index f48b8227..408fb104 100644 --- a/easytier/src/common/network.rs +++ b/easytier/src/common/network.rs @@ -1,13 +1,6 @@ -#[cfg(target_os = "windows")] -use std::net::IpAddr; +use std::{collections::HashMap, net::IpAddr}; -#[cfg(target_os = "windows")] -use network_interface::{ - Addr as SystemAddr, NetworkInterface as SystemNetworkInterface, NetworkInterfaceConfig, -}; -use pnet::datalink::NetworkInterface; -#[cfg(target_os = "windows")] -use pnet::{ipnetwork::IpNetwork, util::MacAddr}; +use network_interface::{NetworkInterface, NetworkInterfaceConfig}; #[cfg(all(target_os = "macos", not(feature = "macos-ne")))] use tokio::sync::Mutex; @@ -15,8 +8,113 @@ use crate::proto::peer_rpc::GetIpListResponse; use super::netns::NetNS; +#[derive(Clone, Copy, Debug, Default)] +struct InterfaceState { + is_point_to_point: bool, + is_loopback: bool, + is_up: bool, + #[cfg(target_os = "linux")] + is_lower_up: bool, +} + +#[cfg(any( + all(target_os = "linux", not(target_env = "ohos")), + all(target_os = "macos", not(feature = "macos-ne")), + target_os = "freebsd" +))] +fn collect_interface_states() -> HashMap { + let mut states = HashMap::new(); + if let Ok(interfaces) = nix::ifaddrs::getifaddrs() { + use nix::net::if_::InterfaceFlags; + + for interface in interfaces { + let flags = interface.flags; + #[cfg(target_os = "linux")] + let is_lower_up = flags.contains(InterfaceFlags::IFF_LOWER_UP); + states.insert( + interface.interface_name, + InterfaceState { + is_point_to_point: flags.contains(InterfaceFlags::IFF_POINTOPOINT), + is_loopback: flags.contains(InterfaceFlags::IFF_LOOPBACK), + is_up: flags.contains(InterfaceFlags::IFF_UP), + #[cfg(target_os = "linux")] + is_lower_up, + }, + ); + } + } + states +} + +#[cfg(not(any( + all(target_os = "linux", not(target_env = "ohos")), + all(target_os = "macos", not(feature = "macos-ne")), + target_os = "freebsd" +)))] +fn collect_interface_states() -> HashMap { + HashMap::new() +} + +#[cfg(any(target_os = "freebsd", target_os = "windows"))] +fn has_nonzero_mac(iface: &NetworkInterface) -> bool { + iface.mac_addr.as_deref().is_some_and(|mac| { + let mut octets = mac.split([':', '-']); + let mut nonzero = false; + for _ in 0..6 { + let Some(value) = octets + .next() + .and_then(|octet| u8::from_str_radix(octet, 16).ok()) + else { + return false; + }; + nonzero |= value != 0; + } + octets.next().is_none() && nonzero + }) +} + +#[allow(dead_code)] +pub(crate) fn ip_mask_to_prefix(mask: IpAddr) -> Result { + match mask { + IpAddr::V4(mask) => { + let raw = u32::from(mask); + let prefix = raw.leading_ones() as u8; + let expected = if prefix == 0 { + 0 + } else { + u32::MAX << (32 - prefix) + }; + (raw == expected).then_some(prefix).ok_or(()) + } + IpAddr::V6(mask) => { + let raw = u128::from(mask); + let prefix = raw.leading_ones() as u8; + let expected = if prefix == 0 { + 0 + } else { + u128::MAX << (128 - prefix) + }; + (raw == expected).then_some(prefix).ok_or(()) + } + } +} + struct InterfaceFilter { iface: NetworkInterface, + state: InterfaceState, +} + +fn interface_state( + iface: &NetworkInterface, + states: &HashMap, +) -> InterfaceState { + states.get(&iface.name).copied().unwrap_or(InterfaceState { + is_loopback: iface.internal, + is_up: true, + #[cfg(target_os = "linux")] + is_lower_up: true, + ..Default::default() + }) } #[cfg(any( @@ -40,7 +138,7 @@ impl InterfaceFilter { async fn has_valid_ip(&self) -> bool { self.iface - .ips + .addr .iter() .map(|ip| ip.ip()) .any(|ip| !ip.is_loopback() && !ip.is_unspecified() && !ip.is_multicast()) @@ -50,18 +148,18 @@ impl InterfaceFilter { tracing::trace!( "filter linux iface: {:?}, is_point_to_point: {}, is_loopback: {}, is_up: {}, is_lower_up: {}, is_tun: {}, has_valid_ip: {}", self.iface, - self.iface.is_point_to_point(), - self.iface.is_loopback(), - self.iface.is_up(), - self.iface.is_lower_up(), + self.state.is_point_to_point, + self.state.is_loopback, + self.state.is_up, + self.state.is_lower_up, self.is_tun_tap_device().await, self.has_valid_ip().await ); - !self.iface.is_point_to_point() - && !self.iface.is_loopback() - && self.iface.is_up() - && self.iface.is_lower_up() + !self.state.is_point_to_point + && !self.state.is_loopback + && self.state.is_up + && self.state.is_lower_up && !self.is_tun_tap_device().await && self.has_valid_ip().await } @@ -134,13 +232,13 @@ impl InterfaceFilter { #[cfg(target_os = "freebsd")] async fn is_interface_physical(&self) -> bool { // if mac addr is not zero, then it's physical interface - self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false) + has_nonzero_mac(&self.iface) } async fn filter_iface(&self) -> bool { - !self.iface.is_point_to_point() - && !self.iface.is_loopback() - && self.iface.is_up() + !self.state.is_point_to_point + && !self.state.is_loopback + && self.state.is_up && self.is_interface_physical().await } } @@ -151,19 +249,19 @@ impl InterfaceFilter { tracing::debug!( "iface_name: {:?}, p2p: {:?}, is_up: {:?}, iface: {:?}", self.iface.name, - self.iface.is_point_to_point(), - self.iface.is_up(), + self.state.is_point_to_point, + self.state.is_up, self.iface ); - !self.iface.is_point_to_point() - && !self.iface.is_loopback() + !self.state.is_point_to_point + && !self.state.is_loopback && self .iface - .ips + .addr .iter() .map(|ip| ip.ip()) .any(|ip| !ip.is_loopback() && !ip.is_unspecified() && !ip.is_multicast()) - && self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false) + && has_nonzero_mac(&self.iface) } } @@ -211,15 +309,66 @@ pub(crate) async fn collect_interfaces(net_ns: NetNS, filter: bool) -> Vec NetworkInterface { + let internal = iface.is_loopback(); + let addr = iface + .ips + .into_iter() + .filter_map(|network| match (network.ip(), network.mask()) { + (IpAddr::V4(ip), IpAddr::V4(netmask)) => { + Some(network_interface::Addr::V4(network_interface::V4IfAddr { + ip, + broadcast: None, + netmask: Some(netmask), + })) + } + (IpAddr::V6(ip), IpAddr::V6(netmask)) => { + Some(network_interface::Addr::V6(network_interface::V6IfAddr { + ip, + broadcast: None, + netmask: Some(netmask), + })) + } + _ => None, + }) + .collect(); + NetworkInterface { + name: iface.name, + addr, + mac_addr: iface.mac.map(|mac| mac.to_string()), + index: iface.index, + internal, + } +} + async fn collect_interfaces_in_current_namespace(filter: bool) -> Vec { - #[cfg(target_os = "windows")] - let ifaces = collect_interfaces_windows(); - #[cfg(not(target_os = "windows"))] - let ifaces = pnet::datalink::interfaces(); + let ifaces = match NetworkInterface::show() { + Ok(ifaces) => ifaces, + Err(error) => { + tracing::warn!(?error, "failed to enumerate network interfaces"); + #[cfg(feature = "faketcp")] + { + match std::panic::catch_unwind(pnet_datalink::interfaces) { + Ok(ifaces) => ifaces.into_iter().map(convert_pnet_interface).collect(), + Err(_) => { + tracing::error!( + "failed to enumerate network interfaces via network-interface and pnet" + ); + return Vec::new(); + } + } + } + #[cfg(not(feature = "faketcp"))] + return Vec::new(); + } + }; + let states = collect_interface_states(); let mut ret = vec![]; for iface in ifaces { let f = InterfaceFilter { iface: iface.clone(), + state: interface_state(&iface, &states), }; if filter && !f.filter_iface().await { @@ -250,83 +399,6 @@ where .expect("namespace-local network operation panicked") } -#[cfg(target_os = "windows")] -fn collect_interfaces_windows() -> Vec { - match SystemNetworkInterface::show() { - Ok(ifaces) => ifaces.into_iter().map(convert_windows_interface).collect(), - Err(e) => { - tracing::warn!( - ?e, - "failed to enumerate interfaces via network-interface, falling back to pnet" - ); - match std::panic::catch_unwind(pnet::datalink::interfaces) { - Ok(ifaces) => ifaces, - Err(_) => { - tracing::error!( - "failed to enumerate interfaces via both network-interface and pnet" - ); - Vec::new() - } - } - } - } -} - -#[cfg(target_os = "windows")] -fn convert_windows_interface(iface: SystemNetworkInterface) -> NetworkInterface { - let mac = iface.mac_addr.as_deref().and_then(|mac| { - mac.parse::() - .map_err( - |e| tracing::debug!(iface = %iface.name, mac, ?e, "failed to parse interface mac"), - ) - .ok() - }); - - let ips = iface - .addr - .into_iter() - .filter_map(convert_windows_interface_addr) - .collect(); - - NetworkInterface { - name: iface.name, - description: String::new(), - index: iface.index, - mac, - ips, - // pnet does not populate Windows flags either, so keep the existing semantics. - flags: 0, - } -} - -#[cfg(target_os = "windows")] -fn convert_windows_interface_addr(addr: SystemAddr) -> Option { - match addr { - SystemAddr::V4(addr) => { - let netmask = addr - .netmask - .map(IpAddr::V4) - .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::new(255, 255, 255, 255))); - IpNetwork::with_netmask(IpAddr::V4(addr.ip), netmask) - .map_err( - |e| tracing::debug!(ip = %addr.ip, ?addr.netmask, ?e, "failed to convert ipv4"), - ) - .ok() - } - SystemAddr::V6(addr) => { - let netmask = addr - .netmask - .map(IpAddr::V6) - .unwrap_or(IpAddr::V6(std::net::Ipv6Addr::from(u128::MAX))); - IpNetwork::with_netmask(IpAddr::V6(addr.ip), netmask) - .map_err( - |e| tracing::debug!(ip = %addr.ip, ?addr.netmask, ?e, "failed to convert ipv6"), - ) - .ok() - } - } -} - #[tracing::instrument(skip(net_ns))] pub(crate) async fn collect_local_ip_addrs(net_ns: NetNS) -> GetIpListResponse { #[cfg(target_os = "linux")] @@ -349,7 +421,7 @@ async fn collect_local_ip_addrs_in_current_namespace() -> GetIpListResponse { let ifaces = collect_interfaces_in_current_namespace(true).await; for iface in ifaces { - for ip in iface.ips { + for ip in iface.addr { let ip: std::net::IpAddr = ip.ip(); if let std::net::IpAddr::V4(v4) = ip { if ip.is_loopback() || ip.is_multicast() { @@ -362,7 +434,7 @@ async fn collect_local_ip_addrs_in_current_namespace() -> GetIpListResponse { let ifaces = collect_interfaces_in_current_namespace(false).await; for iface in ifaces { - for ip in iface.ips { + for ip in iface.addr { let ip: std::net::IpAddr = ip.ip(); if let std::net::IpAddr::V6(v6) = ip { if v6.is_multicast() || v6.is_loopback() || v6.is_unicast_link_local() { @@ -394,6 +466,22 @@ async fn collect_local_ip_addrs_in_current_namespace() -> GetIpListResponse { mod tests { use super::*; + #[test] + fn converts_contiguous_ip_masks_to_prefixes() { + assert_eq!( + ip_mask_to_prefix(IpAddr::V4("255.255.254.0".parse().unwrap())), + Ok(23) + ); + assert_eq!( + ip_mask_to_prefix(IpAddr::V6("ffff:ffff:ffff:ffff::".parse().unwrap())), + Ok(64) + ); + assert_eq!( + ip_mask_to_prefix(IpAddr::V4("255.0.255.0".parse().unwrap())), + Err(()) + ); + } + #[cfg(target_os = "linux")] #[tokio::test] async fn namespace_operation_does_not_migrate_between_os_threads() { diff --git a/easytier/src/host_runtime.rs b/easytier/src/host_runtime.rs index f92ba2f8..25a44958 100644 --- a/easytier/src/host_runtime.rs +++ b/easytier/src/host_runtime.rs @@ -142,7 +142,7 @@ impl ConnectorRuntime for NativeHostRuntime { .into_iter() .find(|interface| { interface - .ips + .addr .iter() .any(|local| matches!(local.ip(), IpAddr::V6(local_ip) if local_ip == ip)) }) diff --git a/easytier/src/instance/composition.rs b/easytier/src/instance/composition.rs index 60437bc5..d307c617 100644 --- a/easytier/src/instance/composition.rs +++ b/easytier/src/instance/composition.rs @@ -260,11 +260,7 @@ mod tests { WrappedTransportConnect, WrappedTransportEngine, }; use easytier_core::listener::plan::ListenerRuntimeConfig; - use pnet::packet::{ - ip::IpNextHeaderProtocols, - ipv4::{self, MutableIpv4Packet}, - udp::{self, MutableUdpPacket}, - }; + use smoltcp::wire::{IpAddress, IpProtocol, Ipv4Packet, UdpPacket}; #[cfg(feature = "kcp")] use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -500,30 +496,26 @@ mod tests { let destination_ip = "10.250.0.2".parse().unwrap(); let mut ip_packet = vec![0u8; 28]; { - let mut ipv4 = MutableIpv4Packet::new(&mut ip_packet).unwrap(); + let mut ipv4 = Ipv4Packet::new_unchecked(&mut ip_packet); ipv4.set_version(4); - ipv4.set_header_length(5); - ipv4.set_total_length(28); - ipv4.set_ttl(64); - ipv4.set_next_level_protocol(IpNextHeaderProtocols::Udp); - ipv4.set_source(source_ip); - ipv4.set_destination(destination_ip); + ipv4.set_header_len(20); + ipv4.set_total_len(28); + ipv4.set_hop_limit(64); + ipv4.set_next_header(IpProtocol::Udp); + ipv4.set_src_addr(source_ip); + ipv4.set_dst_addr(destination_ip); } { - let mut udp = MutableUdpPacket::new(&mut ip_packet[20..]).unwrap(); - udp.set_source(10000); - udp.set_destination(10001); - udp.set_length(8); - udp.set_checksum(udp::ipv4_checksum( - &udp.to_immutable(), - &source_ip, - &destination_ip, - )); - } - { - let mut ipv4 = MutableIpv4Packet::new(&mut ip_packet).unwrap(); - ipv4.set_checksum(ipv4::checksum(&ipv4.to_immutable())); + let mut udp = UdpPacket::new_unchecked(&mut ip_packet[20..]); + udp.set_src_port(10000); + udp.set_dst_port(10001); + udp.set_len(8); + udp.fill_checksum( + &IpAddress::Ipv4(source_ip), + &IpAddress::Ipv4(destination_ip), + ); } + Ipv4Packet::new_unchecked(&mut ip_packet).fill_checksum(); let received = tokio::time::timeout(std::time::Duration::from_secs(10), async { loop { instance_a diff --git a/easytier/src/instance/public_ipv6_provider/linux.rs b/easytier/src/instance/public_ipv6_provider/linux.rs index 9d38cceb..f6052eef 100644 --- a/easytier/src/instance/public_ipv6_provider/linux.rs +++ b/easytier/src/instance/public_ipv6_provider/linux.rs @@ -237,9 +237,9 @@ fn detect_default_route_ipv6_interfaces( routes: &[DetectedIpv6Route], max_prefix_len: u8, ) -> Vec { + use crate::common::network::ip_mask_to_prefix; use nix::ifaddrs::getifaddrs; use nix::sys::socket::SockaddrLike; - use pnet::ipnetwork::ip_mask_to_prefix; let wan_ifindices = default_route_ifindices(routes); if wan_ifindices.is_empty() { diff --git a/easytier/src/socket/fake_tcp/mod.rs b/easytier/src/socket/fake_tcp/mod.rs index 0d2c9dd3..65f2b954 100644 --- a/easytier/src/socket/fake_tcp/mod.rs +++ b/easytier/src/socket/fake_tcp/mod.rs @@ -4,7 +4,6 @@ mod stack; use bytes::BytesMut; use network_interface::NetworkInterfaceConfig; -use pnet::util::MacAddr; use std::{ io, net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}, @@ -25,6 +24,7 @@ use easytier_core::{ use crate::{common::netns::NetNS, tunnel::FromUrl}; use self::netfilter::create_tun; +use self::packet::MacAddr; use futures::Future; use tokio_util::task::AbortOnDropHandle; @@ -35,6 +35,15 @@ struct IpToIfNameCache { ip_to_ifname: DashMap)>, } +fn parse_mac_addr(value: &str) -> Option { + let mut bytes = [0; 6]; + let mut octets = value.split([':', '-']); + for byte in &mut bytes { + *byte = u8::from_str_radix(octets.next()?, 16).ok()?; + } + octets.next().is_none().then(|| MacAddr::from_bytes(&bytes)) +} + impl IpToIfNameCache { fn new() -> Self { Self { @@ -50,9 +59,10 @@ impl IpToIfNameCache { }; for iface in interfaces { let mac = iface.mac_addr.as_deref().and_then(|mac| { - mac.parse::().map_err(|e| { - tracing::debug!(iface = %iface.name, mac, ?e, "failed to parse interface mac") - }).ok() + parse_mac_addr(mac).or_else(|| { + tracing::debug!(iface = %iface.name, mac, "failed to parse interface mac"); + None + }) }); for ip in iface.addr.iter() { self.ip_to_ifname.insert(ip.ip(), (iface.name.clone(), mac)); diff --git a/easytier/src/socket/fake_tcp/netfilter/linux_bpf.rs b/easytier/src/socket/fake_tcp/netfilter/linux_bpf.rs index f9922df3..620121cb 100644 --- a/easytier/src/socket/fake_tcp/netfilter/linux_bpf.rs +++ b/easytier/src/socket/fake_tcp/netfilter/linux_bpf.rs @@ -630,11 +630,9 @@ impl stack::Tun for LinuxBpfTun { mod tests { use super::*; - use crate::socket::fake_tcp::packet::build_tcp_packet; + use crate::socket::fake_tcp::packet::{MacAddr, TCP_FLAG_SYN, build_tcp_packet}; use crate::socket::fake_tcp::stack::Tun; - use pnet::datalink; - use pnet::packet::tcp::TcpFlags; - use pnet::util::MacAddr; + use pnet_datalink as datalink; use rand::Rng; use std::net::{IpAddr, Ipv4Addr}; use tokio::time::{Duration, timeout}; @@ -656,7 +654,7 @@ mod tests { IpAddr::V4(ip) => Some(ip), IpAddr::V6(_) => None, })?; - return Some((iface.name, ipv4, mac)); + return Some((iface.name, ipv4, MacAddr::from_bytes(&mac.octets()))); } None } @@ -741,7 +739,7 @@ mod tests { dst_addr, 1, 0, - TcpFlags::SYN, + TCP_FLAG_SYN, Some(b"ping"), ); @@ -794,7 +792,7 @@ mod tests { non_matching_dst, 1, 0, - TcpFlags::SYN, + TCP_FLAG_SYN, Some(b"nope"), ); send_raw_frame(&ifname, &non_matching).unwrap(); @@ -819,7 +817,7 @@ mod tests { dst_addr, 2, 0, - TcpFlags::SYN, + TCP_FLAG_SYN, Some(b"ok"), ); send_raw_frame(&ifname, &matching).unwrap(); diff --git a/easytier/src/socket/fake_tcp/netfilter/pnet.rs b/easytier/src/socket/fake_tcp/netfilter/pnet.rs index ceea5751..c561813e 100644 --- a/easytier/src/socket/fake_tcp/netfilter/pnet.rs +++ b/easytier/src/socket/fake_tcp/netfilter/pnet.rs @@ -10,9 +10,9 @@ use std::{ use bytes::{Bytes, BytesMut}; use dashmap::DashMap; use once_cell::sync::Lazy; -use pnet::{ - datalink::{self, DataLinkSender, NetworkInterface}, - packet::{ethernet::EtherTypes, ip::IpNextHeaderProtocols, ipv6::Ipv6Packet}, +use pnet_datalink::{self as datalink, DataLinkSender, NetworkInterface}; +use smoltcp::wire::{ + EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, Ipv6Packet, TcpPacket, }; #[cfg(target_os = "linux")] use std::os::unix::fs::MetadataExt; @@ -27,49 +27,38 @@ fn filter_tcp_packet( src_addr: Option<&SocketAddr>, dst_addr: Option<&SocketAddr>, ) -> bool { - use pnet::packet::Packet; - use pnet::packet::ethernet::EthernetPacket; - use pnet::packet::ipv4::Ipv4Packet; - use pnet::packet::tcp::TcpPacket; - - let ethernet = if let Some(ethernet) = EthernetPacket::new(packet) { - ethernet - } else { + let Ok(ethernet) = EthernetFrame::new_checked(packet) else { return false; }; - match ethernet.get_ethertype() { - EtherTypes::Ipv4 => { - let ipv4 = if let Some(ipv4) = Ipv4Packet::new(ethernet.payload()) { - ipv4 - } else { + match ethernet.ethertype() { + EthernetProtocol::Ipv4 => { + let Ok(ipv4) = Ipv4Packet::new_checked(ethernet.payload()) else { return false; }; - if ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp { + if ipv4.next_header() != IpProtocol::Tcp { return false; } - let tcp = if let Some(tcp) = TcpPacket::new(ipv4.payload()) { - tcp - } else { + let Ok(tcp) = TcpPacket::new_checked(ipv4.payload()) else { return false; }; if let Some(src_addr) = src_addr { - if IpAddr::V4(ipv4.get_source()) != src_addr.ip() { + if IpAddr::V4(ipv4.src_addr()) != src_addr.ip() { return false; } - if tcp.get_source() != src_addr.port() { + if tcp.src_port() != src_addr.port() { return false; } } if let Some(dst_addr) = dst_addr { - if IpAddr::V4(ipv4.get_destination()) != dst_addr.ip() { + if IpAddr::V4(ipv4.dst_addr()) != dst_addr.ip() { return false; } - if tcp.get_destination() != dst_addr.port() { + if tcp.dst_port() != dst_addr.port() { return false; } } @@ -79,43 +68,39 @@ fn filter_tcp_packet( "FakeTcpSocketListener packet matched filter, dispatching, src_addr: {:?}, dst_addr: {:?}, packet_src_ip: {:?}, packet_dst_ip: {:?}, packet_src_port: {:?}, packet_dst_port: {:?}", src_addr, dst_addr, - ipv4.get_source(), - ipv4.get_destination(), - tcp.get_source(), - tcp.get_destination(), + ipv4.src_addr(), + ipv4.dst_addr(), + tcp.src_port(), + tcp.dst_port(), ); } - EtherTypes::Ipv6 => { - let ipv6 = if let Some(ipv6) = Ipv6Packet::new(ethernet.payload()) { - ipv6 - } else { + EthernetProtocol::Ipv6 => { + let Ok(ipv6) = Ipv6Packet::new_checked(ethernet.payload()) else { return false; }; - if ipv6.get_next_header() != IpNextHeaderProtocols::Tcp { + if ipv6.next_header() != IpProtocol::Tcp { return false; } - let tcp = if let Some(tcp) = TcpPacket::new(ipv6.payload()) { - tcp - } else { + let Ok(tcp) = TcpPacket::new_checked(ipv6.payload()) else { return false; }; if let Some(src_addr) = src_addr { - if IpAddr::V6(ipv6.get_source()) != src_addr.ip() { + if IpAddr::V6(ipv6.src_addr()) != src_addr.ip() { return false; } - if tcp.get_source() != src_addr.port() { + if tcp.src_port() != src_addr.port() { return false; } } if let Some(dst_addr) = dst_addr { - if IpAddr::V6(ipv6.get_destination()) != dst_addr.ip() { + if IpAddr::V6(ipv6.dst_addr()) != dst_addr.ip() { return false; } - if tcp.get_destination() != dst_addr.port() { + if tcp.dst_port() != dst_addr.port() { return false; } } @@ -150,7 +135,7 @@ struct InterfaceWorker { impl InterfaceWorker { fn new(interface: NetworkInterface) -> io::Result> { let (tx, mut rx) = match datalink::channel(&interface, Default::default()) { - Ok(pnet::datalink::Channel::Ethernet(tx, rx)) => (tx, rx), + Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx), Ok(_) => return Err(io::Error::other("Unhandled channel type")), Err(e) => return Err(io::Error::other(e)), }; diff --git a/easytier/src/socket/fake_tcp/packet.rs b/easytier/src/socket/fake_tcp/packet.rs index 45cfbc5c..e36884d7 100644 --- a/easytier/src/socket/fake_tcp/packet.rs +++ b/easytier/src/socket/fake_tcp/packet.rs @@ -1,36 +1,67 @@ use bytes::{Bytes, BytesMut}; -use pnet::packet::ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket}; -use pnet::packet::{ip, ipv4, ipv6, tcp}; -use pnet::util::MacAddr; -use std::convert::TryInto; +use smoltcp::wire::{ + ETHERNET_HEADER_LEN, EthernetAddress, EthernetFrame, EthernetProtocol, IpAddress, IpProtocol, + Ipv4Packet, Ipv6Packet, TCP_HEADER_LEN, TcpPacket, TcpSeqNumber, +}; use std::net::{IpAddr, SocketAddr}; -const IPV4_HEADER_LEN: usize = 20; -const IPV6_HEADER_LEN: usize = 40; -const TCP_HEADER_LEN: usize = 20; +use smoltcp::wire::{IPV4_HEADER_LEN, IPV6_HEADER_LEN}; + +pub type MacAddr = EthernetAddress; + +pub const TCP_FLAG_FIN: u8 = 0x01; +pub const TCP_FLAG_SYN: u8 = 0x02; +pub const TCP_FLAG_RST: u8 = 0x04; +pub const TCP_FLAG_PSH: u8 = 0x08; +pub const TCP_FLAG_ACK: u8 = 0x10; +pub const TCP_FLAG_URG: u8 = 0x20; +pub const TCP_FLAG_ECE: u8 = 0x40; +pub const TCP_FLAG_CWR: u8 = 0x80; + #[derive(Debug)] pub enum IPPacket<'p> { - V4(ipv4::Ipv4Packet<'p>), - V6(ipv6::Ipv6Packet<'p>), + V4(Ipv4Packet<&'p [u8]>), + V6(Ipv6Packet<&'p [u8]>), } impl IPPacket<'_> { pub fn get_source(&self) -> IpAddr { match self { - IPPacket::V4(p) => IpAddr::V4(p.get_source()), - IPPacket::V6(p) => IpAddr::V6(p.get_source()), + IPPacket::V4(p) => IpAddr::V4(p.src_addr()), + IPPacket::V6(p) => IpAddr::V6(p.src_addr()), } } pub fn get_destination(&self) -> IpAddr { match self { - IPPacket::V4(p) => IpAddr::V4(p.get_destination()), - IPPacket::V6(p) => IpAddr::V6(p.get_destination()), + IPPacket::V4(p) => IpAddr::V4(p.dst_addr()), + IPPacket::V6(p) => IpAddr::V6(p.dst_addr()), } } } -const ETH_HDR_LEN: usize = 14; +fn set_tcp_flags + AsMut<[u8]>>(tcp: &mut TcpPacket, flags: u8) { + tcp.set_fin(flags & TCP_FLAG_FIN != 0); + tcp.set_syn(flags & TCP_FLAG_SYN != 0); + tcp.set_rst(flags & TCP_FLAG_RST != 0); + tcp.set_psh(flags & TCP_FLAG_PSH != 0); + tcp.set_ack(flags & TCP_FLAG_ACK != 0); + tcp.set_urg(flags & TCP_FLAG_URG != 0); + tcp.set_ece(flags & TCP_FLAG_ECE != 0); + tcp.set_cwr(flags & TCP_FLAG_CWR != 0); +} + +#[cfg(test)] +pub fn tcp_flags>(tcp: &TcpPacket) -> u8 { + u8::from(tcp.fin()) + | (u8::from(tcp.syn()) << 1) + | (u8::from(tcp.rst()) << 2) + | (u8::from(tcp.psh()) << 3) + | (u8::from(tcp.ack()) << 4) + | (u8::from(tcp.urg()) << 5) + | (u8::from(tcp.ece()) << 6) + | (u8::from(tcp.cwr()) << 7) +} #[allow(clippy::too_many_arguments)] pub fn build_tcp_packet( @@ -47,76 +78,72 @@ pub fn build_tcp_packet( SocketAddr::V4(_) => IPV4_HEADER_LEN, SocketAddr::V6(_) => IPV6_HEADER_LEN, }; - let wscale = (flags & tcp::TcpFlags::SYN) != 0; + let wscale = flags & TCP_FLAG_SYN != 0; let tcp_header_len = TCP_HEADER_LEN + if wscale { 4 } else { 0 }; // nop + wscale let tcp_total_len = tcp_header_len + payload.map_or(0, |payload| payload.len()); let total_len = ip_header_len + tcp_total_len; - let mut buf = BytesMut::zeroed(ETH_HDR_LEN + total_len); + let mut buf = BytesMut::zeroed(ETHERNET_HEADER_LEN + total_len); - let mut eth_buf = buf.split_to(ETH_HDR_LEN); + let mut eth_buf = buf.split_to(ETHERNET_HEADER_LEN); let mut ip_buf = buf.split_to(ip_header_len); let mut tcp_buf = buf.split_to(tcp_total_len); assert_eq!(0, buf.len()); - let mut tcp = tcp::MutableTcpPacket::new(&mut tcp_buf).unwrap(); - tcp.set_window(0xffff); - tcp.set_source(local_addr.port()); - tcp.set_destination(remote_addr.port()); - tcp.set_sequence(seq); - tcp.set_acknowledgement(ack); - tcp.set_flags(flags); - tcp.set_data_offset(TCP_HEADER_LEN as u8 / 4 + if wscale { 1 } else { 0 }); + let mut tcp = TcpPacket::new_unchecked(&mut tcp_buf); + tcp.set_window_len(0xffff); + tcp.set_src_port(local_addr.port()); + tcp.set_dst_port(remote_addr.port()); + tcp.set_seq_number(TcpSeqNumber(seq as i32)); + tcp.set_ack_number(TcpSeqNumber(ack as i32)); + set_tcp_flags(&mut tcp, flags); + tcp.set_header_len(tcp_header_len as u8); if wscale { - let wscale = tcp::TcpOption::wscale(14); - tcp.set_options(&[tcp::TcpOption::nop(), wscale]); + tcp.options_mut().copy_from_slice(&[1, 3, 3, 14]); } if let Some(payload) = payload { - tcp.set_payload(payload); + tcp.payload_mut().copy_from_slice(payload); } - let mut ethernet = MutableEthernetPacket::new(&mut eth_buf).unwrap(); - ethernet.set_destination(dst_mac); - ethernet.set_source(src_mac); + let mut ethernet = EthernetFrame::new_unchecked(&mut eth_buf); + ethernet.set_dst_addr(dst_mac); + ethernet.set_src_addr(src_mac); ethernet.set_ethertype(match local_addr { - SocketAddr::V4(_) => EtherTypes::Ipv4, - SocketAddr::V6(_) => EtherTypes::Ipv6, + SocketAddr::V4(_) => EthernetProtocol::Ipv4, + SocketAddr::V6(_) => EthernetProtocol::Ipv6, }); match (local_addr, remote_addr) { (SocketAddr::V4(local), SocketAddr::V4(remote)) => { - let mut v4 = ipv4::MutableIpv4Packet::new(&mut ip_buf).unwrap(); + let mut v4 = Ipv4Packet::new_unchecked(&mut ip_buf); v4.set_version(4); - v4.set_header_length(IPV4_HEADER_LEN as u8 / 4); - v4.set_next_level_protocol(ip::IpNextHeaderProtocols::Tcp); - v4.set_ttl(64); - v4.set_source(*local.ip()); - v4.set_destination(*remote.ip()); - v4.set_total_length(total_len.try_into().unwrap()); - v4.set_flags(ipv4::Ipv4Flags::DontFragment); + v4.set_header_len(IPV4_HEADER_LEN as u8); + v4.set_next_header(IpProtocol::Tcp); + v4.set_hop_limit(64); + v4.set_src_addr(*local.ip()); + v4.set_dst_addr(*remote.ip()); + v4.set_total_len(total_len.try_into().unwrap()); + v4.set_dont_frag(true); - tcp.set_checksum(tcp::ipv4_checksum( - &tcp.to_immutable(), - &v4.get_source(), - &v4.get_destination(), - )); - - v4.set_checksum(ipv4::checksum(&v4.to_immutable())); + tcp.fill_checksum( + &IpAddress::Ipv4(*local.ip()), + &IpAddress::Ipv4(*remote.ip()), + ); + v4.fill_checksum(); } (SocketAddr::V6(local), SocketAddr::V6(remote)) => { - let mut v6 = ipv6::MutableIpv6Packet::new(&mut ip_buf).unwrap(); + let mut v6 = Ipv6Packet::new_unchecked(&mut ip_buf); v6.set_version(6); - v6.set_payload_length(tcp_total_len.try_into().unwrap()); - v6.set_next_header(ip::IpNextHeaderProtocols::Tcp); + v6.set_payload_len(tcp_total_len.try_into().unwrap()); + v6.set_next_header(IpProtocol::Tcp); v6.set_hop_limit(64); - v6.set_source(*local.ip()); - v6.set_destination(*remote.ip()); + v6.set_src_addr(*local.ip()); + v6.set_dst_addr(*remote.ip()); - tcp.set_checksum(tcp::ipv6_checksum( - &tcp.to_immutable(), - &v6.get_source(), - &v6.get_destination(), - )); + tcp.fill_checksum( + &IpAddress::Ipv6(*local.ip()), + &IpAddress::Ipv6(*remote.ip()), + ); } _ => unreachable!(), }; @@ -126,40 +153,34 @@ pub fn build_tcp_packet( eth_buf.freeze() } -pub fn parse_ip_packet( - buf: &Bytes, -) -> Option<(MacAddr, MacAddr, IPPacket<'_>, tcp::TcpPacket<'_>)> { - let eth = EthernetPacket::new(buf.as_ref())?; - let src_mac = eth.get_source(); - let dst_mac = eth.get_destination(); - let ethertype = eth.get_ethertype(); +pub fn parse_ip_packet(buf: &Bytes) -> Option<(MacAddr, MacAddr, IPPacket<'_>, TcpPacket<&[u8]>)> { + let eth = EthernetFrame::new_checked(buf.as_ref()).ok()?; + let src_mac = eth.src_addr(); + let dst_mac = eth.dst_addr(); + let ethertype = eth.ethertype(); tracing::trace!("Parsing IP packet: {:?}", eth); - let ip_payload = &buf[ETH_HDR_LEN..]; + let ip_payload = eth.payload(); match ethertype { - EtherTypes::Ipv4 => { - let v4 = ipv4::Ipv4Packet::new(ip_payload)?; - if v4.get_next_level_protocol() != ip::IpNextHeaderProtocols::Tcp { + EthernetProtocol::Ipv4 => { + let v4 = Ipv4Packet::new_checked(ip_payload).ok()?; + if usize::from(v4.header_len()) < IPV4_HEADER_LEN { return None; } - - let tcp_offset = usize::from(v4.get_header_length()) * 4; - if tcp_offset < IPV4_HEADER_LEN || tcp_offset > ip_payload.len() { + if v4.next_header() != IpProtocol::Tcp { return None; } - - let tcp = tcp::TcpPacket::new(&ip_payload[tcp_offset..])?; + let tcp = TcpPacket::new_checked(v4.payload()).ok()?; Some((src_mac, dst_mac, IPPacket::V4(v4), tcp)) } - EtherTypes::Ipv6 => { - let v6 = ipv6::Ipv6Packet::new(ip_payload)?; - if v6.get_next_header() != ip::IpNextHeaderProtocols::Tcp { + EthernetProtocol::Ipv6 => { + let v6 = Ipv6Packet::new_checked(ip_payload).ok()?; + if v6.next_header() != IpProtocol::Tcp { return None; } - - let tcp = tcp::TcpPacket::new(&ip_payload[IPV6_HEADER_LEN..])?; + let tcp = TcpPacket::new_checked(v6.payload()).ok()?; Some((src_mac, dst_mac, IPPacket::V6(v6), tcp)) } _ => None, @@ -169,12 +190,11 @@ pub fn parse_ip_packet( #[cfg(test)] mod tests { use super::*; - use pnet::packet::Packet as _; #[test] fn parse_ipv4_packet_round_trip() { - let src_mac = MacAddr::new(0x02, 0, 0, 0, 0, 1); - let dst_mac = MacAddr::new(0x02, 0, 0, 0, 0, 2); + let src_mac = MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 1]); + let dst_mac = MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 2]); let local_addr: SocketAddr = "192.0.2.1:12345".parse().unwrap(); let remote_addr: SocketAddr = "198.51.100.2:23456".parse().unwrap(); let payload = b"hello fake tcp"; @@ -186,7 +206,7 @@ mod tests { remote_addr, 10, 20, - tcp::TcpFlags::ACK, + TCP_FLAG_ACK, Some(payload), ); @@ -197,15 +217,15 @@ mod tests { assert_eq!(parsed_dst_mac, dst_mac); assert_eq!(ip_packet.get_source(), local_addr.ip()); assert_eq!(ip_packet.get_destination(), remote_addr.ip()); - assert_eq!(tcp_packet.get_source(), local_addr.port()); - assert_eq!(tcp_packet.get_destination(), remote_addr.port()); + assert_eq!(tcp_packet.src_port(), local_addr.port()); + assert_eq!(tcp_packet.dst_port(), remote_addr.port()); assert_eq!(tcp_packet.payload(), payload); } #[test] fn build_and_parse_ipv6_packet_round_trip() { - let src_mac = MacAddr::new(0x02, 0, 0, 0, 0, 3); - let dst_mac = MacAddr::new(0x02, 0, 0, 0, 0, 4); + let src_mac = MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 3]); + let dst_mac = MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 4]); let local_addr: SocketAddr = "[2001:db8::1]:12345".parse().unwrap(); let remote_addr: SocketAddr = "[2001:db8::2]:23456".parse().unwrap(); let payload = b"ipv6 payload"; @@ -217,12 +237,12 @@ mod tests { remote_addr, 30, 40, - tcp::TcpFlags::ACK, + TCP_FLAG_ACK, Some(payload), ); - let ethernet = EthernetPacket::new(packet.as_ref()).unwrap(); - assert_eq!(ethernet.get_ethertype(), EtherTypes::Ipv6); + let ethernet = EthernetFrame::new_checked(packet.as_ref()).unwrap(); + assert_eq!(ethernet.ethertype(), EthernetProtocol::Ipv6); let (parsed_src_mac, parsed_dst_mac, ip_packet, tcp_packet) = parse_ip_packet(&packet).unwrap(); @@ -231,48 +251,159 @@ mod tests { assert_eq!(parsed_dst_mac, dst_mac); assert_eq!(ip_packet.get_source(), local_addr.ip()); assert_eq!(ip_packet.get_destination(), remote_addr.ip()); - assert_eq!(tcp_packet.get_source(), local_addr.port()); - assert_eq!(tcp_packet.get_destination(), remote_addr.port()); + assert_eq!(tcp_packet.src_port(), local_addr.port()); + assert_eq!(tcp_packet.dst_port(), remote_addr.port()); assert_eq!(tcp_packet.payload(), payload); } #[test] fn parse_rejects_short_ethernet_frame() { - let packet = Bytes::from_static(&[0u8; ETH_HDR_LEN - 1]); + let packet = Bytes::from_static(&[0u8; ETHERNET_HEADER_LEN - 1]); assert!(parse_ip_packet(&packet).is_none()); } #[test] fn parse_rejects_truncated_ipv4_tcp_packet() { let packet = build_tcp_packet( - MacAddr::new(0x02, 0, 0, 0, 0, 5), - MacAddr::new(0x02, 0, 0, 0, 0, 6), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 5]), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 6]), "192.0.2.10:1111".parse().unwrap(), "198.51.100.20:2222".parse().unwrap(), 1, 2, - tcp::TcpFlags::ACK, + TCP_FLAG_ACK, None, ); - let truncated = Bytes::copy_from_slice(&packet[..ETH_HDR_LEN + IPV4_HEADER_LEN + 10]); + let truncated = + Bytes::copy_from_slice(&packet[..ETHERNET_HEADER_LEN + IPV4_HEADER_LEN + 10]); assert!(parse_ip_packet(&truncated).is_none()); } + #[test] + fn parse_rejects_ipv4_header_shorter_than_minimum() { + let packet = build_tcp_packet( + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 5]), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 6]), + "192.0.2.10:1111".parse().unwrap(), + "198.51.100.20:2222".parse().unwrap(), + 1, + 0x5000_0000, + TCP_FLAG_ACK, + None, + ); + let mut malformed = BytesMut::from(packet.as_ref()); + Ipv4Packet::new_unchecked(&mut malformed[ETHERNET_HEADER_LEN..]) + .set_header_len((IPV4_HEADER_LEN - 4) as u8); + let malformed = malformed.freeze(); + + assert!(parse_ip_packet(&malformed).is_none()); + } + #[test] fn parse_rejects_truncated_ipv6_header() { let packet = build_tcp_packet( - MacAddr::new(0x02, 0, 0, 0, 0, 7), - MacAddr::new(0x02, 0, 0, 0, 0, 8), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 7]), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 8]), "[2001:db8::10]:1111".parse().unwrap(), "[2001:db8::20]:2222".parse().unwrap(), 1, 2, - tcp::TcpFlags::ACK, + TCP_FLAG_ACK, None, ); - let truncated = Bytes::copy_from_slice(&packet[..ETH_HDR_LEN + IPV6_HEADER_LEN - 1]); + let truncated = + Bytes::copy_from_slice(&packet[..ETHERNET_HEADER_LEN + IPV6_HEADER_LEN - 1]); assert!(parse_ip_packet(&truncated).is_none()); } + + #[test] + fn syn_packet_preserves_wire_format_and_unsigned_sequence() { + let packet = build_tcp_packet( + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 1]), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 2]), + "192.0.2.1:12345".parse().unwrap(), + "198.51.100.2:23456".parse().unwrap(), + 0x8000_0001, + 0xffff_fffe, + TCP_FLAG_SYN | TCP_FLAG_ACK, + None, + ); + + assert_eq!( + &packet[..ETHERNET_HEADER_LEN], + &[0x02, 0, 0, 0, 0, 2, 0x02, 0, 0, 0, 0, 1, 0x08, 0x00] + ); + let (_, _, IPPacket::V4(ipv4), tcp) = parse_ip_packet(&packet).unwrap() else { + panic!("expected IPv4 packet"); + }; + assert_eq!(ipv4.header_len(), IPV4_HEADER_LEN as u8); + assert!(ipv4.dont_frag()); + assert_eq!(ipv4.hop_limit(), 64); + assert!(ipv4.verify_checksum()); + assert_eq!(tcp.header_len(), (TCP_HEADER_LEN + 4) as u8); + assert_eq!(tcp.options(), &[1, 3, 3, 14]); + assert_eq!(tcp_flags(&tcp), TCP_FLAG_SYN | TCP_FLAG_ACK); + assert_eq!(tcp.seq_number().0 as u32, 0x8000_0001); + assert_eq!(tcp.ack_number().0 as u32, 0xffff_fffe); + assert!(tcp.verify_checksum( + &IpAddress::Ipv4(ipv4.src_addr()), + &IpAddress::Ipv4(ipv4.dst_addr()), + )); + } + + #[test] + fn ethernet_padding_is_not_tcp_payload() { + let packet = build_tcp_packet( + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 1]), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 2]), + "192.0.2.1:12345".parse().unwrap(), + "198.51.100.2:23456".parse().unwrap(), + 1, + 2, + TCP_FLAG_ACK, + Some(b"payload"), + ); + let mut padded = BytesMut::from(packet.as_ref()); + padded.extend_from_slice(&[0; 16]); + let padded = padded.freeze(); + + let (_, _, _, tcp) = parse_ip_packet(&padded).unwrap(); + assert_eq!(tcp.payload(), b"payload"); + } + + #[test] + fn ipv4_options_do_not_shift_tcp_payload() { + let packet = build_tcp_packet( + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 1]), + MacAddr::from_bytes(&[0x02, 0, 0, 0, 0, 2]), + "192.0.2.1:12345".parse().unwrap(), + "198.51.100.2:23456".parse().unwrap(), + 1, + 2, + TCP_FLAG_ACK, + Some(b"payload"), + ); + let ip_start = ETHERNET_HEADER_LEN; + let tcp_start = ip_start + IPV4_HEADER_LEN; + let mut with_options = Vec::with_capacity(packet.len() + 4); + with_options.extend_from_slice(&packet[..tcp_start]); + with_options.extend_from_slice(&[1, 1, 1, 0]); + with_options.extend_from_slice(&packet[tcp_start..]); + { + let total_len = with_options.len() - ip_start; + let mut ipv4 = Ipv4Packet::new_unchecked(&mut with_options[ip_start..]); + ipv4.set_header_len((IPV4_HEADER_LEN + 4) as u8); + ipv4.set_total_len(total_len as u16); + ipv4.fill_checksum(); + } + let with_options = Bytes::from(with_options); + + let (_, _, IPPacket::V4(ipv4), tcp) = parse_ip_packet(&with_options).unwrap() else { + panic!("expected IPv4 packet"); + }; + assert_eq!(ipv4.header_len(), (IPV4_HEADER_LEN + 4) as u8); + assert_eq!(tcp.payload(), b"payload"); + } } diff --git a/easytier/src/socket/fake_tcp/stack.rs b/easytier/src/socket/fake_tcp/stack.rs index d3934456..b474f94c 100644 --- a/easytier/src/socket/fake_tcp/stack.rs +++ b/easytier/src/socket/fake_tcp/stack.rs @@ -41,9 +41,6 @@ use super::packet::*; use bytes::{Bytes, BytesMut}; use crossbeam::atomic::AtomicCell; -use pnet::packet::tcp::TcpOptionNumbers; -use pnet::packet::{Packet, tcp}; -use pnet::util::MacAddr; use std::collections::HashMap; use std::fmt; #[cfg(test)] @@ -60,6 +57,47 @@ use tracing::{error, info, trace, warn}; const TIMEOUT: time::Duration = time::Duration::from_secs(1); const MPMC_BUFFER_LEN: usize = 512; +const TCP_OPTION_END: u8 = 0; +const TCP_OPTION_NOP: u8 = 1; +const TCP_OPTION_SACK: u8 = 5; + +struct TcpOptionIter<'a> { + remaining: &'a [u8], +} + +impl<'a> TcpOptionIter<'a> { + fn new(options: &'a [u8]) -> Self { + Self { remaining: options } + } +} + +impl<'a> Iterator for TcpOptionIter<'a> { + type Item = (u8, &'a [u8]); + + fn next(&mut self) -> Option { + let kind = *self.remaining.first()?; + if kind == TCP_OPTION_END { + self.remaining = &[]; + return None; + } + if kind == TCP_OPTION_NOP { + self.remaining = &self.remaining[1..]; + return Some((kind, &[])); + } + let Some(&length) = self.remaining.get(1) else { + self.remaining = &[]; + return None; + }; + let length = usize::from(length); + if length < 2 || length > self.remaining.len() { + self.remaining = &[]; + return None; + } + let payload = &self.remaining[2..length]; + self.remaining = &self.remaining[length..]; + Some((kind, payload)) + } +} #[async_trait::async_trait] pub trait Tun: Send + Sync + 'static { @@ -181,7 +219,7 @@ impl Socket { build_tcp_packet( self.local_mac, - self.remote_mac.load().unwrap_or(MacAddr::zero()), + self.remote_mac.load().unwrap_or_default(), self.local_addr, self.remote_addr, self.seq.load(Ordering::Relaxed), @@ -201,7 +239,7 @@ impl Socket { pub fn try_send(&self, payload: &[u8]) -> Option<()> { match self.state.load() { State::Established => { - let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, Some(payload)); + let buf = self.build_tcp_packet(TCP_FLAG_ACK, Some(payload)); self.seq.fetch_add(payload.len() as u32, Ordering::Relaxed); self.tun.try_send(&buf).ok().and(Some(())) } @@ -211,7 +249,7 @@ impl Socket { pub fn close(&self) { if self.state.load() != State::Idle { - let buf = self.build_tcp_packet(tcp::TcpFlags::RST, None); + let buf = self.build_tcp_packet(TCP_FLAG_RST, None); let _ = self.tun.try_send(&buf); self.state.store(State::Idle); } @@ -256,32 +294,30 @@ impl Socket { self.remote_mac.store(Some(src_mac)); - if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 { + if tcp_packet.rst() { info!("Connection {} reset by peer", self); return None; } - if (tcp_packet.get_flags() & tcp::TcpFlags::ACK) != 0 - && tcp_packet.payload().is_empty() - { + if tcp_packet.ack() && tcp_packet.payload().is_empty() { self.seq - .store(tcp_packet.get_acknowledgement(), Ordering::Relaxed); + .store(tcp_packet.ack_number().0 as u32, Ordering::Relaxed); } let payload = tcp_packet.payload(); - let new_ack = tcp_packet.get_sequence().wrapping_add(payload.len() as u32); + let new_ack = + (tcp_packet.seq_number().0 as u32).wrapping_add(payload.len() as u32); self.ack.store(new_ack, Ordering::Relaxed); - for opt in tcp_packet.get_options_iter() { - if opt.get_number() == TcpOptionNumbers::SACK { + for (kind, option_payload) in TcpOptionIter::new(tcp_packet.options()) { + if kind == TCP_OPTION_SACK { // SACK 选项类型为 5 - let payload = opt.payload(); - for chunk in payload.chunks(8) { + for chunk in option_payload.chunks(8) { if chunk.len() != 8 { continue; } - let left = tcp_packet.get_acknowledgement(); + let left = tcp_packet.ack_number().0 as u32; let right = u32::from_be_bytes(chunk[0..4].try_into().unwrap()); let len = right.wrapping_sub(left); @@ -295,12 +331,12 @@ impl Socket { let buf = build_tcp_packet( self.local_mac, - self.remote_mac.load().unwrap_or(MacAddr::zero()), + self.remote_mac.load().unwrap_or_default(), self.local_addr, self.remote_addr, left, self.ack.load(Ordering::Relaxed), - tcp::TcpFlags::ACK, + TCP_FLAG_ACK, Some(&data), ); @@ -332,18 +368,19 @@ impl Socket { continue; }; - if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 { + if tcp_packet.rst() { tracing::trace!("Connection {} reset by peer", self); return None; } - let expected_flag = tcp::TcpFlags::SYN | tcp::TcpFlags::ACK; - if (tcp_packet.get_flags() & expected_flag) == expected_flag { + if tcp_packet.syn() && tcp_packet.ack() { // found our SYN + ACK self.seq - .store(tcp_packet.get_acknowledgement(), Ordering::Relaxed); - self.ack - .store(tcp_packet.get_sequence() + 1, Ordering::Relaxed); + .store(tcp_packet.ack_number().0 as u32, Ordering::Relaxed); + self.ack.store( + (tcp_packet.seq_number().0 as u32).wrapping_add(1), + Ordering::Relaxed, + ); self.remote_mac.store(Some(src_mac)); self.state.store(State::Established); return Some(0); @@ -385,12 +422,12 @@ impl Drop for Socket { let buf = build_tcp_packet( self.local_mac, - self.remote_mac.load().unwrap_or(MacAddr::zero()), + self.remote_mac.load().unwrap_or_default(), self.local_addr, self.remote_addr, self.seq.load(Ordering::Relaxed), 0, - tcp::TcpFlags::RST, + TCP_FLAG_RST, None, ); if let Err(e) = self.tun.try_send(&buf) { @@ -434,7 +471,7 @@ impl Stack { Stack { shared, - local_mac: local_mac.unwrap_or(MacAddr::zero()), + local_mac: local_mac.unwrap_or_default(), reader_task: AbortOnDropHandle::new(t), } } @@ -514,11 +551,11 @@ impl Stack { Some((_src_mac, _dst_mac, ip_packet, tcp_packet)) => { let local_addr = SocketAddr::new( ip_packet.get_destination(), - tcp_packet.get_destination(), + tcp_packet.dst_port(), ); let remote_addr = SocketAddr::new( ip_packet.get_source(), - tcp_packet.get_source(), + tcp_packet.src_port(), ); let tuple = AddrTuple::new(local_addr, remote_addr); @@ -548,7 +585,7 @@ impl Stack { } } - if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 { + if tcp_packet.rst() { info!("Unknown RST TCP packet from {}, ignoring", remote_addr); continue; } else { @@ -604,6 +641,43 @@ mod tests { time::{Duration, timeout}, }; + #[test] + fn tcp_option_iterator_preserves_all_sack_blocks() { + for block_count in 1..=4_u32 { + let mut options = vec![TCP_OPTION_NOP, TCP_OPTION_SACK, (2 + block_count * 8) as u8]; + for block in 0..block_count { + options.extend_from_slice(&(100 + block * 10).to_be_bytes()); + options.extend_from_slice(&(110 + block * 10).to_be_bytes()); + } + options.push(TCP_OPTION_END); + + let parsed = TcpOptionIter::new(&options).collect::>(); + assert_eq!(parsed[0], (TCP_OPTION_NOP, &[][..])); + assert_eq!(parsed[1].0, TCP_OPTION_SACK); + assert_eq!(parsed[1].1.len(), block_count as usize * 8); + let last = parsed[1].1.len() - 8; + assert_eq!( + u32::from_be_bytes(parsed[1].1[last..last + 4].try_into().unwrap()), + 100 + (block_count - 1) * 10 + ); + assert_eq!( + u32::from_be_bytes(parsed[1].1[last + 4..last + 8].try_into().unwrap()), + 110 + (block_count - 1) * 10 + ); + } + } + + #[test] + fn tcp_option_iterator_stops_at_end_or_malformed_length() { + assert_eq!( + TcpOptionIter::new(&[TCP_OPTION_END, TCP_OPTION_SACK, 2]).count(), + 0 + ); + assert_eq!(TcpOptionIter::new(&[TCP_OPTION_SACK]).count(), 0); + assert_eq!(TcpOptionIter::new(&[TCP_OPTION_SACK, 1]).count(), 0); + assert_eq!(TcpOptionIter::new(&[TCP_OPTION_SACK, 10, 0, 0]).count(), 0); + } + #[derive(Default)] struct FailingTun { fail: Notify,