mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 03:22:05 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7aae67c43 | ||
|
|
f24735a86f | ||
|
|
7756a15cbe | ||
|
|
741460e1e4 | ||
|
|
13412895c5 |
@@ -51,11 +51,6 @@ jobs:
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install LTTng UST headers
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y liblttng-ust-dev
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# rust-analyzer config
|
||||
# Skip the `easytier-gui` (Tauri) crate because its build scripts require
|
||||
# system libraries webkit2gtk-4.1 / javascriptcoregtk-4.1 that are not
|
||||
# installed on this host. Excluding it keeps rust-analyzer healthy for the
|
||||
# rest of the workspace.
|
||||
|
||||
# Override the command used to run build scripts / collect build data.
|
||||
[cargo.buildScripts]
|
||||
overrideCommand = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--quiet",
|
||||
"--workspace",
|
||||
"--exclude",
|
||||
"easytier-gui",
|
||||
"--message-format=json",
|
||||
"--all-targets",
|
||||
"--keep-going",
|
||||
]
|
||||
|
||||
# Override the command used for diagnostics-on-save.
|
||||
[check]
|
||||
overrideCommand = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--workspace",
|
||||
"--exclude",
|
||||
"easytier-gui",
|
||||
"--message-format=json",
|
||||
"--all-targets",
|
||||
"--keep-going",
|
||||
]
|
||||
Generated
+138
-63
@@ -129,6 +129,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
@@ -772,29 +778,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.65.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"peeking_take_while",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
"shlex",
|
||||
"syn 2.0.117",
|
||||
"which 4.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
@@ -808,7 +791,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
@@ -1162,6 +1145,12 @@ dependencies = [
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.10"
|
||||
@@ -1271,6 +1260,33 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cidr"
|
||||
version = "0.3.1"
|
||||
@@ -1616,6 +1632,42 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
@@ -2289,6 +2341,7 @@ dependencies = [
|
||||
"clap_complete",
|
||||
"clap_complete_nushell",
|
||||
"console-subscriber",
|
||||
"criterion",
|
||||
"crossbeam",
|
||||
"ctor 0.8.0",
|
||||
"dashmap",
|
||||
@@ -2321,8 +2374,6 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"lttng-ust",
|
||||
"lttng-ust-generate",
|
||||
"machine-uid",
|
||||
"maplit",
|
||||
"mimalloc",
|
||||
@@ -2384,6 +2435,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"terminal_size",
|
||||
"thiserror 1.0.63",
|
||||
"thread_local",
|
||||
"thunk-rs",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemalloc-sys",
|
||||
@@ -4521,6 +4573,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
@@ -4689,7 +4752,7 @@ source = "git+https://github.com/EasyTier/kcp-sys?rev=d7427c22d764deb1860a7d37ac
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_impl",
|
||||
"bindgen 0.72.1",
|
||||
"bindgen",
|
||||
"bitflags 2.8.0",
|
||||
"bytes",
|
||||
"cc",
|
||||
@@ -4736,12 +4799,6 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -4805,7 +4862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4935,20 +4992,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lttng-ust"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/EasyTier/lttng-ust-rs.git#fb3a7a86e9c3d8edad34cc70fd48f908b745f8d9"
|
||||
|
||||
[[package]]
|
||||
name = "lttng-ust-generate"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/EasyTier/lttng-ust-rs.git#fb3a7a86e9c3d8edad34cc70fd48f908b745f8d9"
|
||||
dependencies = [
|
||||
"bindgen 0.65.1",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -5901,6 +5944,12 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
@@ -6268,12 +6317,6 @@ dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.4"
|
||||
@@ -6537,6 +6580,34 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet"
|
||||
version = "0.35.0"
|
||||
@@ -7118,7 +7189,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.11",
|
||||
@@ -7139,7 +7210,7 @@ dependencies = [
|
||||
"lru-slab",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
@@ -7826,12 +7897,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.0"
|
||||
@@ -9921,6 +9986,16 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.0"
|
||||
@@ -11325,7 +11400,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+24
-3
@@ -28,6 +28,14 @@ path = "src/easytier-cli.rs"
|
||||
name = "easytier"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bench]]
|
||||
name = "tx_throughput"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "packet_bytes_extraction"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
git-version = "0.3.9"
|
||||
|
||||
@@ -37,7 +45,6 @@ tracing-subscriber = { version = "0.3", features = [
|
||||
"local-time",
|
||||
"time",
|
||||
] }
|
||||
lttng-ust = { git = "https://github.com/EasyTier/lttng-ust-rs.git", optional = true }
|
||||
derivative = "2.2.0"
|
||||
derive_more = { version = "2.1.1", features = ["full"] }
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
@@ -217,6 +224,7 @@ smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp.git", rev = "0a926767a6
|
||||
"async",
|
||||
] }
|
||||
parking_lot = { version = "0.12.0" }
|
||||
thread_local = "1.1"
|
||||
|
||||
wildmatch = "2.3.4"
|
||||
|
||||
@@ -330,7 +338,6 @@ quote = "1"
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
lttng-ust-generate = { git = "https://github.com/EasyTier/lttng-ust-rs.git", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
reqwest = { version = "0.12.12", features = ["blocking"] }
|
||||
@@ -338,6 +345,7 @@ zip = "4.0.0"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
serial_test = "3.0.0"
|
||||
rstest = "0.25.0"
|
||||
futures-util = "0.3.31"
|
||||
@@ -345,6 +353,14 @@ maplit = "1.0.2"
|
||||
tempfile = "3.22.0"
|
||||
ctor = "0.8.0"
|
||||
|
||||
[[bench]]
|
||||
name = "acl_hotpath"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "zc_packet"
|
||||
harness = false
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
defguard_wireguard_rs = "0.4.2"
|
||||
tokio-socks = "0.5.2"
|
||||
@@ -404,9 +420,14 @@ jemalloc-prof = [
|
||||
"jemalloc-sys/stats",
|
||||
]
|
||||
tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||
lttng = ["dep:lttng-ust", "dep:lttng-ust-generate"]
|
||||
magic-dns = ["dep:hickory-client", "dep:hickory-server"]
|
||||
faketcp = ["dep:flume"]
|
||||
zstd = ["dep:zstd"]
|
||||
# Deprecated: hotpath profiling has been removed. These feature aliases are
|
||||
# retained as no-ops so existing build scripts using `--features hotpath*`
|
||||
# continue to work without pulling in any dependencies.
|
||||
hotpath = []
|
||||
hotpath-cpu = ["hotpath"]
|
||||
hotpath-alloc = ["hotpath"]
|
||||
# For Network Extension on macOS
|
||||
macos-ne = []
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Benchmarks
|
||||
|
||||
Criterion benchmarks for EasyTier hot paths.
|
||||
|
||||
| Bench | What it measures |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `tx_throughput` | End-to-end TX injection path through `peer_manager::send_msg_by_ip` |
|
||||
| `packet_bytes_extraction` | `ZCPacket::payload_bytes` / `tunnel_payload_bytes` extraction (advance hot path) |
|
||||
|
||||
## Packet Bytes Extraction
|
||||
|
||||
Criterion benchmark for `ZCPacket` bytes extraction — the methods touched by the
|
||||
`advance`-based slicing refactor. Measures `payload_bytes` and
|
||||
`tunnel_payload_bytes` at two payload sizes (1280, 4096). Setup
|
||||
(`ZCPacket::new_with_payload`) runs in the benchmark harness's preparation
|
||||
phase and is excluded from the timed region, so the numbers reflect only the
|
||||
extraction call.
|
||||
|
||||
### Quick start
|
||||
|
||||
```bash
|
||||
cargo bench --bench packet_bytes_extraction
|
||||
```
|
||||
|
||||
Smoke run:
|
||||
|
||||
```bash
|
||||
PACKET_BYTES_MEASUREMENT_SECS=2 \
|
||||
PACKET_BYTES_WARMUP_SECS=1 \
|
||||
PACKET_BYTES_SAMPLE_SIZE=10 \
|
||||
cargo bench --bench packet_bytes_extraction -- --quiet
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default | Notes |
|
||||
| ------------------------------- | ------- | ---------------------------- |
|
||||
| `PACKET_BYTES_MEASUREMENT_SECS` | `10` | Criterion `measurement_time` |
|
||||
| `PACKET_BYTES_WARMUP_SECS` | `3` | Criterion `warm_up_time` |
|
||||
| `PACKET_BYTES_SAMPLE_SIZE` | `10` | Criterion `sample_size` (min 10) |
|
||||
|
||||
---
|
||||
|
||||
## TX Throughput Benchmark
|
||||
|
||||
Criterion benchmark for EasyTier's TX injection path (`peer_manager::send_msg_by_ip`).
|
||||
|
||||
## What it measures
|
||||
|
||||
The benchmark sets up two EasyTier instances (`hot-a` / `hot-b`) and drives
|
||||
packets from `hot-a` to `hot-b` via `peer_manager.send_msg_by_ip`. This is the
|
||||
same entry point `easytier-core` uses for daily forwarded traffic, so the
|
||||
numbers reflect the real TX hot path: NIC pipeline → route lookup →
|
||||
compress/encrypt → peer connection → tunnel send.
|
||||
|
||||
Two variants are reported per tunnel kind:
|
||||
|
||||
| Bench | What it measures |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `tx_throughput/<tunnel>` | Serial baseline. One send in flight at a time. Reports per-packet CPU cost (TX injection latency). |
|
||||
| `tx_throughput/<tunnel>-saturate` | Spawns `TX_THROUGHPUT_INFLIGHT` tokio tasks that independently pump `send_msg_by_ip`. Reports the aggregate throughput ceiling the peer manager + tunnel can sustain across worker threads. |
|
||||
|
||||
> **Out of scope (by design):** TUN read/write (`no_tun = true`), compression
|
||||
> (default `None`), reverse/RX-side measurement, multi-peer fanout. Add
|
||||
> separate benchmarks if you need those.
|
||||
|
||||
## Quick start
|
||||
|
||||
### ring tunnel (no root, fastest)
|
||||
|
||||
```bash
|
||||
cargo bench --bench tx_throughput
|
||||
```
|
||||
|
||||
Smoke run (faster iteration):
|
||||
|
||||
```bash
|
||||
TX_THROUGHPUT_MEASUREMENT_SECS=2 \
|
||||
TX_THROUGHPUT_WARMUP_SECS=1 \
|
||||
TX_THROUGHPUT_SAMPLE_SIZE=10 \
|
||||
cargo bench --bench tx_throughput -- --quiet
|
||||
```
|
||||
|
||||
### tcp / udp tunnels (requires Docker + root)
|
||||
|
||||
The benchmark creates a Docker network and registers each container's netns
|
||||
under `/var/run/netns`, which requires root. Run the whole command under
|
||||
`sudo`:
|
||||
|
||||
```bash
|
||||
sudo TX_THROUGHPUT_TUNNEL=tcp \
|
||||
TX_THROUGHPUT_MEASUREMENT_SECS=5 \
|
||||
TX_THROUGHPUT_WARMUP_SECS=2 \
|
||||
TX_THROUGHPUT_INFLIGHT=64 \
|
||||
cargo bench --bench tx_throughput -- --quiet
|
||||
|
||||
sudo TX_THROUGHPUT_TUNNEL=udp cargo bench --bench tx_throughput -- --quiet
|
||||
```
|
||||
|
||||
> If `sudo` cannot find `cargo`, use `sudo -E` or the absolute path
|
||||
> (`$(which cargo)`).
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Default | Notes |
|
||||
| -------------------------------- | --------------------- | -------------------------------------- |
|
||||
| `TX_THROUGHPUT_TUNNEL` | `ring` | `ring` / `tcp` / `udp` |
|
||||
| `TX_THROUGHPUT_PKT_SIZE` | `1400` | IP total length in bytes |
|
||||
| `TX_THROUGHPUT_WORKER_THREADS` | `4` | tokio worker threads |
|
||||
| `TX_THROUGHPUT_INFLIGHT` | `64` | saturate-mode concurrency (task count) |
|
||||
| `TX_THROUGHPUT_TUNNEL_PORT` | `35521` | tcp/udp listen port |
|
||||
| `TX_THROUGHPUT_MEASUREMENT_SECS` | `10` | Criterion `measurement_time` |
|
||||
| `TX_THROUGHPUT_WARMUP_SECS` | `3` | Criterion `warm_up_time` |
|
||||
| `TX_THROUGHPUT_SAMPLE_SIZE` | `10` | Criterion `sample_size` (min 10) |
|
||||
| `TX_THROUGHPUT_DOCKER_IMAGE` | `busybox:latest` | tcp/udp only |
|
||||
| `TX_THROUGHPUT_DOCKER_NET` | `easytier-bench-<id>` | auto-generated unique name |
|
||||
| `TX_THROUGHPUT_DOCKER_SUBNET` | `172.31.250.0/24` | |
|
||||
| `TX_THROUGHPUT_DOCKER_IP_A` | `172.31.250.2` | |
|
||||
| `TX_THROUGHPUT_DOCKER_IP_B` | `172.31.250.3` | |
|
||||
|
||||
## Parameter sweeps
|
||||
|
||||
```bash
|
||||
# Packet size
|
||||
for sz in 64 256 1400 9000; do
|
||||
TX_THROUGHPUT_PKT_SIZE=$sz cargo bench --bench tx_throughput -- --quick
|
||||
done
|
||||
|
||||
# Inflight depth (self-check: depth=1 should match serial baseline)
|
||||
for d in 1 4 16 64 256; do
|
||||
TX_THROUGHPUT_INFLIGHT=$d cargo bench --bench tx_throughput -- --quick
|
||||
done
|
||||
|
||||
# Worker threads
|
||||
for w in 1 2 4 8; do
|
||||
TX_THROUGHPUT_WORKER_THREADS=$w cargo bench --bench tx_throughput -- --quick
|
||||
done
|
||||
```
|
||||
|
||||
## Interpreting results
|
||||
|
||||
- **`<tunnel>`** reports per-packet latency. Lower is better. Throughput
|
||||
column here is "what one in-flight sender sustains".
|
||||
- **`<tunnel>-saturate`** reports aggregate throughput across
|
||||
`TX_THROUGHPUT_INFLIGHT` concurrent senders. If this matches the serial
|
||||
baseline, the TX path is bottlenecked on an internal serialization point
|
||||
(lock, single-threaded queue, etc.) rather than CPU or link bandwidth.
|
||||
|
||||
### Known finding (ring, single peer)
|
||||
|
||||
On the ring tunnel with a single destination peer, saturate does **not** beat
|
||||
serial (observed ~277 MiB/s saturate vs ~288 MiB/s serial on a 4-worker
|
||||
runtime). This points to a serialization point inside the peer-connection TX
|
||||
path. Tunnels with real I/O await points (tcp/udp via Docker) are expected to
|
||||
show a saturate > serial gap; verify with the sudo commands above.
|
||||
|
||||
## Output artifacts
|
||||
|
||||
Criterion writes HTML reports + SVG plots under
|
||||
`easytier/target/criterion/`. Open `tx_throughput/<tunnel>/report/index.html`
|
||||
or `.../<tunnel>-saturate/report/index.html` in a browser to inspect
|
||||
distributions and regressions across runs.
|
||||
@@ -0,0 +1,137 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||
use easytier::common::acl_processor::{AclProcessor, PacketInfo};
|
||||
use easytier::proto::acl::*;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
fn make_acl_config() -> Acl {
|
||||
let mut acl_config = Acl::default();
|
||||
let mut acl_v1 = AclV1::default();
|
||||
|
||||
let mut chain = Chain {
|
||||
name: "bench_inbound".to_string(),
|
||||
chain_type: ChainType::Inbound as i32,
|
||||
enabled: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
chain.rules.push(Rule {
|
||||
name: "allow_all".to_string(),
|
||||
priority: 100,
|
||||
enabled: true,
|
||||
action: Action::Allow as i32,
|
||||
protocol: Protocol::Any as i32,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
acl_v1.chains.push(chain);
|
||||
acl_config.acl_v1 = Some(acl_v1);
|
||||
acl_config
|
||||
}
|
||||
|
||||
fn make_packet_info() -> PacketInfo {
|
||||
PacketInfo {
|
||||
src_ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
|
||||
dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
|
||||
src_port: Some(12345),
|
||||
dst_port: Some(80),
|
||||
protocol: Protocol::Tcp,
|
||||
packet_size: 1024,
|
||||
src_groups: Arc::new(vec![]),
|
||||
dst_groups: Arc::new(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_cache_hit_single(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let processor = rt.block_on(async { AclProcessor::new(make_acl_config()) });
|
||||
let packet_info = make_packet_info();
|
||||
|
||||
// Prime the cache
|
||||
let _ = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||
|
||||
c.bench_function("acl_cache_hit_1t", |b| {
|
||||
b.iter(|| {
|
||||
std::hint::black_box(processor.process_packet(&packet_info, ChainType::Inbound));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_cache_hit_multi(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("acl_cache_hit_multi");
|
||||
for threads in [2, 4, 8] {
|
||||
let processor = Arc::new(rt.block_on(async { AclProcessor::new(make_acl_config()) }));
|
||||
let packet_info = Arc::new(make_packet_info());
|
||||
|
||||
// Prime the cache
|
||||
let _ = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(threads),
|
||||
&threads,
|
||||
|b, &threads| {
|
||||
b.iter_custom(|iters| {
|
||||
use std::sync::Barrier;
|
||||
use std::thread;
|
||||
|
||||
let barrier = Arc::new(Barrier::new(threads + 1));
|
||||
let per_thread = (iters / threads as u64) as usize;
|
||||
let mut handles = Vec::with_capacity(threads);
|
||||
|
||||
for _ in 0..threads {
|
||||
let processor = Arc::clone(&processor);
|
||||
let packet_info = Arc::clone(&packet_info);
|
||||
let barrier = Arc::clone(&barrier);
|
||||
handles.push(thread::spawn(move || {
|
||||
barrier.wait();
|
||||
for _ in 0..per_thread {
|
||||
std::hint::black_box(
|
||||
processor.process_packet(&packet_info, ChainType::Inbound),
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
barrier.wait();
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
start.elapsed()
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_unique_rule_match(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let processor = rt.block_on(async { AclProcessor::new(make_acl_config()) });
|
||||
|
||||
c.bench_function("acl_unique_rule_match_1t", |b| {
|
||||
let mut i = 0usize;
|
||||
b.iter(|| {
|
||||
let mut packet_info = make_packet_info();
|
||||
packet_info.src_port = Some((1024 + (i % 60_000)) as u16);
|
||||
packet_info.src_ip = IpAddr::V4(Ipv4Addr::new(
|
||||
10,
|
||||
((i >> 16) & 0xff) as u8,
|
||||
((i >> 8) & 0xff) as u8,
|
||||
(i & 0xff) as u8,
|
||||
));
|
||||
std::hint::black_box(processor.process_packet(&packet_info, ChainType::Inbound));
|
||||
i = i.wrapping_add(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_cache_hit_single,
|
||||
bench_cache_hit_multi,
|
||||
bench_unique_rule_match
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::hint::black_box;
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main};
|
||||
|
||||
use easytier::tunnel::packet_def::ZCPacket;
|
||||
|
||||
const PAYLOAD_SIZES: &[usize] = &[1280, 4096];
|
||||
|
||||
fn env_parse<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn bench_payload_bytes(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("payload_bytes");
|
||||
for &size in PAYLOAD_SIZES {
|
||||
let data = vec![0u8; size];
|
||||
group.throughput(Throughput::Bytes(size as u64));
|
||||
group.bench_with_input(format!("{size}"), &data, |b, data| {
|
||||
b.iter_batched(
|
||||
|| ZCPacket::new_with_payload(black_box(data)),
|
||||
|p| black_box(p).payload_bytes(),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_tunnel_payload_bytes(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tunnel_payload_bytes");
|
||||
for &size in PAYLOAD_SIZES {
|
||||
let data = vec![0u8; size];
|
||||
group.throughput(Throughput::Bytes(size as u64));
|
||||
group.bench_with_input(format!("{size}"), &data, |b, data| {
|
||||
b.iter_batched(
|
||||
|| ZCPacket::new_with_payload(black_box(data)),
|
||||
|p| black_box(p).tunnel_payload_bytes(),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion_config() -> Criterion {
|
||||
let measurement_secs = env_parse("PACKET_BYTES_MEASUREMENT_SECS", 10u64);
|
||||
let warmup_secs = env_parse("PACKET_BYTES_WARMUP_SECS", 3u64);
|
||||
let sample_size = env_parse("PACKET_BYTES_SAMPLE_SIZE", 10usize).max(10);
|
||||
|
||||
Criterion::default()
|
||||
.measurement_time(Duration::from_secs(measurement_secs))
|
||||
.warm_up_time(Duration::from_secs(warmup_secs))
|
||||
.sample_size(sample_size)
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion_config();
|
||||
targets = bench_payload_bytes, bench_tunnel_payload_bytes
|
||||
}
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,472 @@
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use criterion::{Criterion, Throughput, criterion_group, criterion_main};
|
||||
|
||||
use easytier::{
|
||||
common::config::{ConfigLoader, TomlConfigLoader},
|
||||
instance::instance::Instance,
|
||||
tunnel::{
|
||||
packet_def::ZCPacket, ring::RingTunnelConnector, tcp::TcpTunnelConnector,
|
||||
udp::UdpTunnelConnector,
|
||||
},
|
||||
};
|
||||
|
||||
const VIRTUAL_IP_A: &str = "10.144.144.1";
|
||||
const VIRTUAL_IP_B: &str = "10.144.144.2";
|
||||
const DEFAULT_DOCKER_SUBNET: &str = "172.31.250.0/24";
|
||||
const DEFAULT_DOCKER_IP_A: &str = "172.31.250.2";
|
||||
const DEFAULT_DOCKER_IP_B: &str = "172.31.250.3";
|
||||
const DEFAULT_TUNNEL_PORT: u16 = 35521;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum TunnelKind {
|
||||
Ring,
|
||||
Tcp,
|
||||
Udp,
|
||||
}
|
||||
|
||||
impl TunnelKind {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
TunnelKind::Ring => "ring",
|
||||
TunnelKind::Tcp => "tcp",
|
||||
TunnelKind::Udp => "udp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TunnelKind {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"ring" => Ok(TunnelKind::Ring),
|
||||
"tcp" => Ok(TunnelKind::Tcp),
|
||||
"udp" => Ok(TunnelKind::Udp),
|
||||
other => Err(format!(
|
||||
"unsupported TX_THROUGHPUT_TUNNEL={other:?}; expected ring, tcp, or udp"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BenchTopology {
|
||||
_docker: Option<DockerNetns>,
|
||||
inst_a: Instance,
|
||||
_inst_b: Instance,
|
||||
dst: IpAddr,
|
||||
packet: ZCPacket,
|
||||
}
|
||||
|
||||
struct DockerNetns {
|
||||
network: String,
|
||||
container_a: String,
|
||||
container_b: String,
|
||||
netns_a: String,
|
||||
netns_b: String,
|
||||
ip_a: String,
|
||||
netns_a_path: PathBuf,
|
||||
netns_b_path: PathBuf,
|
||||
}
|
||||
|
||||
impl DockerNetns {
|
||||
fn create() -> Self {
|
||||
let id = unique_id();
|
||||
let image = env_string("TX_THROUGHPUT_DOCKER_IMAGE", "busybox:latest");
|
||||
let network = env_string("TX_THROUGHPUT_DOCKER_NET", &format!("easytier-bench-{id}"));
|
||||
let subnet = env_string("TX_THROUGHPUT_DOCKER_SUBNET", DEFAULT_DOCKER_SUBNET);
|
||||
let ip_a = env_string("TX_THROUGHPUT_DOCKER_IP_A", DEFAULT_DOCKER_IP_A);
|
||||
let ip_b = env_string("TX_THROUGHPUT_DOCKER_IP_B", DEFAULT_DOCKER_IP_B);
|
||||
let container_a = format!("easytier-bench-a-{id}");
|
||||
let container_b = format!("easytier-bench-b-{id}");
|
||||
let netns_a = format!("easytier-bench-a-{id}");
|
||||
let netns_b = format!("easytier-bench-b-{id}");
|
||||
|
||||
docker(&[
|
||||
"network", "create", "--driver", "bridge", "--subnet", &subnet, &network,
|
||||
]);
|
||||
|
||||
let mut docker_netns = Self {
|
||||
network,
|
||||
container_a,
|
||||
container_b,
|
||||
netns_a,
|
||||
netns_b,
|
||||
ip_a: ip_a.clone(),
|
||||
netns_a_path: PathBuf::new(),
|
||||
netns_b_path: PathBuf::new(),
|
||||
};
|
||||
|
||||
docker_netns.start_container(&docker_netns.container_a, &ip_a, &image);
|
||||
docker_netns.start_container(&docker_netns.container_b, &ip_b, &image);
|
||||
|
||||
let pid_a = docker(&["inspect", "-f", "{{.State.Pid}}", &docker_netns.container_a]);
|
||||
let pid_b = docker(&["inspect", "-f", "{{.State.Pid}}", &docker_netns.container_b]);
|
||||
|
||||
docker_netns.netns_a_path = register_netns(&docker_netns.netns_a, &pid_a);
|
||||
docker_netns.netns_b_path = register_netns(&docker_netns.netns_b, &pid_b);
|
||||
docker_netns
|
||||
}
|
||||
|
||||
fn start_container(&self, name: &str, ip: &str, image: &str) {
|
||||
docker(&[
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
name,
|
||||
"--network",
|
||||
&self.network,
|
||||
"--ip",
|
||||
ip,
|
||||
image,
|
||||
"sleep",
|
||||
"3600",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DockerNetns {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.netns_a_path);
|
||||
let _ = std::fs::remove_file(&self.netns_b_path);
|
||||
docker_ignore(&["rm", "-f", &self.container_a, &self.container_b]);
|
||||
docker_ignore(&["network", "rm", &self.network]);
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_tx_throughput(c: &mut Criterion) {
|
||||
let tunnel = env_string("TX_THROUGHPUT_TUNNEL", "ring")
|
||||
.parse::<TunnelKind>()
|
||||
.unwrap_or_else(|err| panic!("{err}"));
|
||||
let packet_size = env_parse("TX_THROUGHPUT_PKT_SIZE", 1400usize);
|
||||
const MIN_PKT_SIZE: usize = 28; // IPv4 (20) + UDP (8) header
|
||||
assert!(
|
||||
packet_size >= MIN_PKT_SIZE,
|
||||
"TX_THROUGHPUT_PKT_SIZE={packet_size} is smaller than the minimum {MIN_PKT_SIZE} (IPv4+UDP headers)"
|
||||
);
|
||||
let worker_threads = env_parse("TX_THROUGHPUT_WORKER_THREADS", 4usize);
|
||||
let inflight_depth = env_parse("TX_THROUGHPUT_INFLIGHT", 64usize).max(1);
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(worker_threads)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("create tokio runtime");
|
||||
|
||||
let topology = runtime.block_on(setup_topology(tunnel, packet_size));
|
||||
let peer_manager = topology.inst_a.get_peer_manager();
|
||||
let packet = topology.packet.clone();
|
||||
let dst = topology.dst;
|
||||
|
||||
eprintln!(
|
||||
"tx_throughput: tunnel={} inflight={} workers={} pkt_size={}",
|
||||
tunnel.as_str(),
|
||||
inflight_depth.max(1),
|
||||
worker_threads,
|
||||
packet_size
|
||||
);
|
||||
|
||||
let mut group = c.benchmark_group("tx_throughput");
|
||||
group.throughput(Throughput::Bytes(packet_size as u64));
|
||||
|
||||
// Serial baseline: one packet in flight at a time.
|
||||
// Measures per-packet CPU cost (TX injection latency).
|
||||
group.bench_function(tunnel.as_str(), |b| {
|
||||
b.iter_custom(|iterations| {
|
||||
let pm = peer_manager.clone();
|
||||
let pkt = packet.clone();
|
||||
runtime.block_on(async move {
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
pm.send_msg_by_ip(pkt.clone(), dst, false)
|
||||
.await
|
||||
.expect("send packet by EasyTier IP");
|
||||
}
|
||||
start.elapsed()
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Saturate: spawn TX_THROUGHPUT_INFLIGHT worker tasks, each independently
|
||||
// pumping send_msg_by_ip. Work is distributed across tokio worker threads,
|
||||
// exposing the peer manager + tunnel's true aggregate throughput ceiling.
|
||||
// With TX_THROUGHPUT_INFLIGHT=1 it degrades to the serial baseline.
|
||||
group.bench_function(format!("{}-saturate", tunnel.as_str()), |b| {
|
||||
b.iter_custom(|iterations| {
|
||||
let pm = peer_manager.clone();
|
||||
let pkt = packet.clone();
|
||||
let concurrency = inflight_depth.min(iterations as usize).max(1);
|
||||
runtime.block_on(async move {
|
||||
let counter = Arc::new(AtomicU64::new(iterations));
|
||||
let start = Instant::now();
|
||||
let mut handles = Vec::with_capacity(concurrency);
|
||||
for _ in 0..concurrency {
|
||||
let pm = pm.clone();
|
||||
let pkt = pkt.clone();
|
||||
let counter = counter.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
loop {
|
||||
if counter
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |cur| {
|
||||
if cur > 0 { Some(cur - 1) } else { None }
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
pm.send_msg_by_ip(pkt.clone(), dst, false)
|
||||
.await
|
||||
.expect("send packet by EasyTier IP");
|
||||
}
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.await.expect("saturate worker task panicked");
|
||||
}
|
||||
start.elapsed()
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
|
||||
runtime.block_on(async move {
|
||||
drop(topology);
|
||||
});
|
||||
}
|
||||
|
||||
async fn setup_topology(tunnel: TunnelKind, packet_size: usize) -> BenchTopology {
|
||||
let tunnel_port = env_parse("TX_THROUGHPUT_TUNNEL_PORT", DEFAULT_TUNNEL_PORT);
|
||||
let docker = match tunnel {
|
||||
TunnelKind::Ring => None,
|
||||
TunnelKind::Tcp | TunnelKind::Udp => Some(DockerNetns::create()),
|
||||
};
|
||||
|
||||
let (netns_a, netns_b) = match &docker {
|
||||
Some(docker) => (Some(docker.netns_a.clone()), Some(docker.netns_b.clone())),
|
||||
None => (None, None),
|
||||
};
|
||||
let listeners_a = match tunnel {
|
||||
TunnelKind::Ring => Vec::new(),
|
||||
TunnelKind::Tcp | TunnelKind::Udp => vec![
|
||||
format!("{}://0.0.0.0:{}", tunnel.as_str(), tunnel_port)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
],
|
||||
};
|
||||
|
||||
let mut inst_a = Instance::new(no_tun_config("hot-a", VIRTUAL_IP_A, netns_a, listeners_a));
|
||||
let mut inst_b = Instance::new(no_tun_config("hot-b", VIRTUAL_IP_B, netns_b, Vec::new()));
|
||||
|
||||
inst_a.run().await.expect("inst_a run");
|
||||
inst_b.run().await.expect("inst_b run");
|
||||
|
||||
match tunnel {
|
||||
TunnelKind::Ring => inst_b
|
||||
.get_conn_manager()
|
||||
.add_connector(RingTunnelConnector::new(
|
||||
format!("ring://{}", inst_a.id()).parse().unwrap(),
|
||||
)),
|
||||
TunnelKind::Tcp => inst_b
|
||||
.get_conn_manager()
|
||||
.add_connector(TcpTunnelConnector::new(
|
||||
format!(
|
||||
"tcp://{}:{}",
|
||||
docker.as_ref().expect("tcp benchmark needs Docker").ip_a,
|
||||
tunnel_port
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)),
|
||||
TunnelKind::Udp => inst_b
|
||||
.get_conn_manager()
|
||||
.add_connector(UdpTunnelConnector::new(
|
||||
format!(
|
||||
"udp://{}:{}",
|
||||
docker.as_ref().expect("udp benchmark needs Docker").ip_a,
|
||||
tunnel_port
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)),
|
||||
}
|
||||
|
||||
wait_for_routes(&inst_a, &inst_b).await;
|
||||
|
||||
BenchTopology {
|
||||
_docker: docker,
|
||||
inst_a,
|
||||
_inst_b: inst_b,
|
||||
dst: VIRTUAL_IP_B.parse().unwrap(),
|
||||
packet: make_data_packet(VIRTUAL_IP_A, VIRTUAL_IP_B, packet_size),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_routes(inst_a: &Instance, inst_b: &Instance) {
|
||||
tokio::time::timeout(Duration::from_secs(15), async {
|
||||
loop {
|
||||
let routes_a = inst_a.get_peer_manager().list_routes().await;
|
||||
let routes_b = inst_b.get_peer_manager().list_routes().await;
|
||||
if !routes_a.is_empty() && !routes_b.is_empty() {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("EasyTier routes did not converge within 15s");
|
||||
}
|
||||
|
||||
fn make_data_packet(src: &str, dst: &str, total_size: usize) -> ZCPacket {
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
let hdr_len = 28;
|
||||
let payload_len = total_size.saturating_sub(hdr_len);
|
||||
let ip_total_len = (hdr_len + payload_len) as u16;
|
||||
let mut buf = BytesMut::with_capacity(total_size);
|
||||
|
||||
buf.extend_from_slice(&[
|
||||
0x45,
|
||||
0x00,
|
||||
(ip_total_len >> 8) as u8,
|
||||
(ip_total_len & 0xff) as u8,
|
||||
0x00,
|
||||
0x00,
|
||||
0x40,
|
||||
0x00,
|
||||
0x40,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
]);
|
||||
let src: Ipv4Addr = src.parse().unwrap();
|
||||
buf.extend_from_slice(&src.octets());
|
||||
let dst: Ipv4Addr = dst.parse().unwrap();
|
||||
buf.extend_from_slice(&dst.octets());
|
||||
|
||||
let udp_len = (8 + payload_len) as u16;
|
||||
buf.extend_from_slice(&[
|
||||
0x30,
|
||||
0x39,
|
||||
0xd4,
|
||||
0x31,
|
||||
(udp_len >> 8) as u8,
|
||||
(udp_len & 0xff) as u8,
|
||||
0x00,
|
||||
0x00,
|
||||
]);
|
||||
|
||||
buf.resize(total_size, 0xaa);
|
||||
ZCPacket::new_with_payload(&buf)
|
||||
}
|
||||
|
||||
fn no_tun_config(
|
||||
name: &str,
|
||||
ipv4: &str,
|
||||
netns: Option<String>,
|
||||
listeners: Vec<url::Url>,
|
||||
) -> TomlConfigLoader {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_inst_name(name.to_owned());
|
||||
config.set_netns(netns);
|
||||
config.set_ipv4(Some(ipv4.parse().unwrap()));
|
||||
config.set_listeners(listeners);
|
||||
let mut flags = config.get_flags();
|
||||
flags.no_tun = true;
|
||||
config.set_flags(flags);
|
||||
config
|
||||
}
|
||||
|
||||
fn register_netns(name: &str, pid: &str) -> PathBuf {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let dir = PathBuf::from("/var/run/netns");
|
||||
std::fs::create_dir_all(&dir).expect("create /var/run/netns");
|
||||
let path = dir.join(name);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
std::os::unix::fs::symlink(format!("/proc/{pid}/ns/net"), &path)
|
||||
.expect("link Docker netns into /var/run/netns");
|
||||
path
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (name, pid);
|
||||
panic!("Docker netns benchmark requires Linux");
|
||||
}
|
||||
}
|
||||
|
||||
fn docker(args: &[&str]) -> String {
|
||||
let output = Command::new("docker")
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("failed to run docker {args:?}: {err}"));
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"docker {:?} failed with status {:?}: {}",
|
||||
args,
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_owned()
|
||||
}
|
||||
|
||||
fn docker_ignore(args: &[&str]) {
|
||||
let _ = Command::new("docker")
|
||||
.args(args)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
|
||||
fn env_string(name: &str, default: &str) -> String {
|
||||
std::env::var(name).unwrap_or_else(|_| default.to_owned())
|
||||
}
|
||||
|
||||
fn env_parse<T>(name: &str, default: T) -> T
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
match std::env::var(name) {
|
||||
Ok(value) => value
|
||||
.parse()
|
||||
.unwrap_or_else(|err| panic!("invalid {name}={value:?}: {err}")),
|
||||
Err(_) => default,
|
||||
}
|
||||
}
|
||||
|
||||
fn criterion_config() -> Criterion {
|
||||
let measurement_secs = env_parse("TX_THROUGHPUT_MEASUREMENT_SECS", 10u64);
|
||||
let warmup_secs = env_parse("TX_THROUGHPUT_WARMUP_SECS", 3u64);
|
||||
let sample_size = env_parse("TX_THROUGHPUT_SAMPLE_SIZE", 10usize).max(10);
|
||||
|
||||
Criterion::default()
|
||||
.measurement_time(Duration::from_secs(measurement_secs))
|
||||
.warm_up_time(Duration::from_secs(warmup_secs))
|
||||
.sample_size(sample_size)
|
||||
}
|
||||
|
||||
fn unique_id() -> String {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock before UNIX epoch")
|
||||
.as_nanos();
|
||||
format!("{}-{nanos}", std::process::id())
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion_config();
|
||||
targets = bench_tx_throughput
|
||||
}
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,51 @@
|
||||
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||
use easytier::tunnel::packet_def::{ZCPacket, ZCPacketType};
|
||||
|
||||
fn bench_new_with_payload(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("zc_new_with_payload");
|
||||
for size in [64usize, 1500] {
|
||||
let payload = vec![0xabu8; size];
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &payload, |b, payload| {
|
||||
b.iter(|| {
|
||||
std::hint::black_box(ZCPacket::new_with_payload(std::hint::black_box(payload)));
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_new_for_foreign_network(c: &mut Criterion) {
|
||||
let payload = vec![0xabu8; 64];
|
||||
let foreign_packet = ZCPacket::new_with_payload(&payload);
|
||||
let network_name = "bench-network".to_string();
|
||||
|
||||
c.bench_function("zc_new_for_foreign_network_64b", |b| {
|
||||
b.iter(|| {
|
||||
std::hint::black_box(ZCPacket::new_for_foreign_network(
|
||||
std::hint::black_box(&network_name),
|
||||
42,
|
||||
std::hint::black_box(&foreign_packet),
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_convert_type(c: &mut Criterion) {
|
||||
let payload = vec![0xabu8; 64];
|
||||
let packet = ZCPacket::new_with_payload(&payload);
|
||||
|
||||
c.bench_function("zc_convert_type_tcp_64b", |b| {
|
||||
b.iter(|| {
|
||||
let p = std::hint::black_box(packet.clone());
|
||||
std::hint::black_box(p.convert_type(ZCPacketType::TCP));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_new_with_payload,
|
||||
bench_new_for_foreign_network,
|
||||
bench_convert_type
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -133,35 +133,6 @@ fn check_locale() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lttng")]
|
||||
fn generate_lttng_tracepoints(out: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use lttng_ust_generate::{CIntegerType, CTFType, Generator, LogLevel, Provider};
|
||||
|
||||
let mut provider = Provider::new("easytier");
|
||||
provider
|
||||
.create_class("marker")
|
||||
.add_field("name", CTFType::SequenceText)
|
||||
.add_field("value", CTFType::Integer(CIntegerType::U64))
|
||||
.instantiate_with_level("mark", LogLevel::Info);
|
||||
|
||||
provider
|
||||
.create_class("duration")
|
||||
.add_field("name", CTFType::SequenceText)
|
||||
.add_field("duration_micros", CTFType::Integer(CIntegerType::U64))
|
||||
.instantiate_with_level("duration_micros", LogLevel::Info);
|
||||
|
||||
Generator::default()
|
||||
.generated_lib_name("easytier_lttng_tracepoints")
|
||||
.register_provider(provider)
|
||||
.output_file_name(out.join("lttng_tracepoints.rs"))
|
||||
.generate()
|
||||
.map_err(|error| {
|
||||
std::io::Error::other(format!("failed to generate LTTng tracepoints: {error:?}"))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
cfg_aliases! {
|
||||
mobile: {
|
||||
@@ -203,9 +174,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
let out = PathBuf::from(env::var("OUT_DIR")?);
|
||||
#[cfg(feature = "lttng")]
|
||||
generate_lttng_tracepoints(&out)?;
|
||||
|
||||
let descriptor = out.join("descriptors.bin");
|
||||
|
||||
let mut config = prost_build::Config::new();
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
sync::atomic::{AtomicU64, Ordering::Relaxed},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use quanta::Instant;
|
||||
@@ -76,7 +77,41 @@ pub struct FastLookupRule {
|
||||
pub stateful: bool,
|
||||
pub rate_limit: u32,
|
||||
pub burst_limit: u32,
|
||||
pub rule_stats: Arc<RuleStats>,
|
||||
pub rule_stats: Arc<RuleStatsTracker>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RuleStatsTracker {
|
||||
rule: Option<Rule>,
|
||||
packets: AtomicU64,
|
||||
bytes: AtomicU64,
|
||||
}
|
||||
|
||||
impl RuleStatsTracker {
|
||||
fn new(rule: Option<Rule>) -> Self {
|
||||
Self {
|
||||
rule,
|
||||
packets: AtomicU64::new(0),
|
||||
bytes: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure(impl_type = "RuleStatsTracker"))]
|
||||
fn increment(&self, packet_size: usize) {
|
||||
self.packets.fetch_add(1, Relaxed);
|
||||
self.bytes.fetch_add(packet_size as u64, Relaxed);
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> RuleStats {
|
||||
RuleStats {
|
||||
rule: self.rule.clone(),
|
||||
stat: Some(StatItem {
|
||||
packet_count: self.packets.load(Relaxed),
|
||||
byte_count: self.bytes.load(Relaxed),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache key combining packet info and chain type
|
||||
@@ -108,17 +143,17 @@ impl AclCacheKey {
|
||||
}
|
||||
|
||||
// Cache entry with timestamp for LRU cleanup
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AclCacheEntry {
|
||||
pub action: Action,
|
||||
pub matched_rule: RuleId,
|
||||
pub last_access: Instant,
|
||||
pub last_access: AtomicU64,
|
||||
// New fields to track rule characteristics for proper cache behavior
|
||||
pub conn_track_key: Option<String>,
|
||||
pub rate_limit_keys: Vec<RateLimitKey>,
|
||||
pub chain_type: ChainType,
|
||||
pub acl_result: Option<AclResult>,
|
||||
pub rule_stats_vec: Vec<Arc<RuleStats>>,
|
||||
pub rule_stats_vec: Vec<Arc<RuleStatsTracker>>,
|
||||
}
|
||||
|
||||
// Packet info extracted for ACL processing
|
||||
@@ -211,7 +246,7 @@ pub struct AclProcessor {
|
||||
default_outbound_action: Action,
|
||||
default_forward_action: Action,
|
||||
|
||||
default_rule_stats: Arc<RuleStats>,
|
||||
default_rule_stats: Arc<RuleStatsTracker>,
|
||||
|
||||
// Connection tracking table - shared across different processor instances if needed
|
||||
conn_track: Arc<DashMap<String, ConnTrackEntry>>,
|
||||
@@ -224,6 +259,13 @@ pub struct AclProcessor {
|
||||
cache_max_size: usize,
|
||||
cache_cleanup_interval: Duration,
|
||||
|
||||
// Coarse monotonic timestamp updated by the cleanup task, used to avoid
|
||||
// calling Instant::now() on every cache hit.
|
||||
coarse_millis: Arc<AtomicU64>,
|
||||
|
||||
// Hot-path counters that bypass the DashMap stats table
|
||||
cache_hits: AtomicU64,
|
||||
|
||||
// Statistics
|
||||
stats: Arc<DashMap<AclStatKey, u64>>,
|
||||
|
||||
@@ -259,18 +301,14 @@ impl AclProcessor {
|
||||
default_outbound_action,
|
||||
default_forward_action,
|
||||
|
||||
default_rule_stats: Arc::new(RuleStats {
|
||||
rule: None,
|
||||
stat: Some(StatItem {
|
||||
packet_count: 0,
|
||||
byte_count: 0,
|
||||
}),
|
||||
}),
|
||||
default_rule_stats: Arc::new(RuleStatsTracker::new(None)),
|
||||
conn_track: conn_track.unwrap_or_else(|| Arc::new(DashMap::new())),
|
||||
rate_limiters: rate_limiters.unwrap_or_else(|| Arc::new(DashMap::new())),
|
||||
rule_cache: Arc::new(DashMap::new()), // Always start with fresh cache
|
||||
cache_max_size: 1024, // Limit cache to 1k entries
|
||||
cache_cleanup_interval: Duration::from_secs(20), // Cleanup every 5 minutes
|
||||
coarse_millis: Arc::new(AtomicU64::new(0)),
|
||||
cache_hits: AtomicU64::new(0),
|
||||
stats: stats.unwrap_or_else(|| Arc::new(DashMap::new())),
|
||||
tasks,
|
||||
};
|
||||
@@ -374,11 +412,14 @@ impl AclProcessor {
|
||||
let rule_cache = self.rule_cache.clone();
|
||||
let cache_max_size = self.cache_max_size;
|
||||
let cleanup_interval = self.cache_cleanup_interval;
|
||||
let coarse_millis = self.coarse_millis.clone();
|
||||
|
||||
self.tasks.spawn(async move {
|
||||
let mut interval = tokio::time::interval(cleanup_interval);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let now = crate::common::stats_manager::now_monotonic_millis();
|
||||
coarse_millis.store(now, Relaxed);
|
||||
Self::cleanup_cache(&rule_cache, cache_max_size);
|
||||
rule_cache.shrink_to_fit();
|
||||
|
||||
@@ -401,10 +442,9 @@ impl AclProcessor {
|
||||
/// Clean up cache using LRU strategy
|
||||
fn cleanup_cache(cache: &DashMap<AclCacheKey, AclCacheEntry>, max_size: usize) {
|
||||
// remove cache not be used in last 15 second
|
||||
let expired_timepoint = Instant::now()
|
||||
.checked_sub(Duration::from_secs(15))
|
||||
.unwrap_or(Instant::now());
|
||||
cache.retain(|_, entry| entry.last_access > expired_timepoint);
|
||||
let now = crate::common::stats_manager::now_monotonic_millis();
|
||||
let cutoff = now.saturating_sub(15_000);
|
||||
cache.retain(|_, entry| entry.last_access.load(Relaxed) > cutoff);
|
||||
|
||||
let current_size = cache.len();
|
||||
if current_size <= max_size {
|
||||
@@ -412,9 +452,9 @@ impl AclProcessor {
|
||||
}
|
||||
|
||||
// Remove oldest entries (LRU cleanup)
|
||||
let mut entries: Vec<(AclCacheKey, Instant)> = cache
|
||||
let mut entries: Vec<(AclCacheKey, u64)> = cache
|
||||
.iter()
|
||||
.map(|entry| (entry.key().clone(), entry.value().last_access))
|
||||
.map(|entry| (entry.key().clone(), entry.value().last_access.load(Relaxed)))
|
||||
.collect();
|
||||
|
||||
// Sort by last_access (oldest first)
|
||||
@@ -433,6 +473,7 @@ impl AclProcessor {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure(impl_type = "AclProcessor"))]
|
||||
pub(crate) fn process_packet_with_cache_entry(
|
||||
&self,
|
||||
packet_info: &PacketInfo,
|
||||
@@ -459,42 +500,41 @@ impl AclProcessor {
|
||||
cache_entry.acl_result.clone().unwrap()
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure(impl_type = "AclProcessor"))]
|
||||
fn inc_cache_entry_stats(&self, cache_entry: &AclCacheEntry, packet_info: &PacketInfo) {
|
||||
for rule_stats in cache_entry.rule_stats_vec.iter() {
|
||||
// Use unsafe code to mutate the contents behind the Arc
|
||||
let stat_ptr = rule_stats.stat.as_ref().unwrap() as *const StatItem as *mut StatItem;
|
||||
unsafe {
|
||||
(*stat_ptr).packet_count += 1;
|
||||
(*stat_ptr).byte_count += packet_info.packet_size as u64;
|
||||
}
|
||||
rule_stats.increment(packet_info.packet_size);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rules_stats(&self) -> Vec<RuleStats> {
|
||||
let mut stats: Vec<RuleStats> = Vec::new();
|
||||
for rule in self.inbound_rules.iter() {
|
||||
stats.push((*rule.rule_stats).clone());
|
||||
stats.push(rule.rule_stats.snapshot());
|
||||
}
|
||||
for rule in self.outbound_rules.iter() {
|
||||
stats.push((*rule.rule_stats).clone());
|
||||
stats.push(rule.rule_stats.snapshot());
|
||||
}
|
||||
for rule in self.forward_rules.iter() {
|
||||
stats.push((*rule.rule_stats).clone());
|
||||
stats.push(rule.rule_stats.snapshot());
|
||||
}
|
||||
stats
|
||||
}
|
||||
|
||||
/// Process a packet through ACL rules - Now lock-free!
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure(impl_type = "AclProcessor"))]
|
||||
pub fn process_packet(&self, packet_info: &PacketInfo, chain_type: ChainType) -> AclResult {
|
||||
// Check cache first for performance
|
||||
let cache_key = AclCacheKey::from_packet_info(packet_info, chain_type);
|
||||
|
||||
// If cache hit and can skip checks, return cached result
|
||||
if let Some(mut cached) = self.rule_cache.get_mut(&cache_key) {
|
||||
// Update last access time for LRU
|
||||
cached.last_access = Instant::now();
|
||||
|
||||
self.increment_stat(AclStatKey::CacheHits);
|
||||
// If cache hit and can skip checks, return cached result.
|
||||
// Use get() (read lock) instead of get_mut() (write lock) and update
|
||||
// last_access via AtomicU64, avoiding expensive Instant::now().
|
||||
if let Some(cached) = self.rule_cache.get(&cache_key) {
|
||||
cached
|
||||
.last_access
|
||||
.store(self.coarse_millis.load(Relaxed), Relaxed);
|
||||
self.cache_hits.fetch_add(1, Relaxed);
|
||||
return self.process_packet_with_cache_entry(packet_info, &cached);
|
||||
}
|
||||
|
||||
@@ -516,7 +556,7 @@ impl AclProcessor {
|
||||
let mut cache_entry = AclCacheEntry {
|
||||
action: Action::Allow,
|
||||
matched_rule: RuleId::Default,
|
||||
last_access: Instant::now(),
|
||||
last_access: AtomicU64::new(self.coarse_millis.load(Relaxed)),
|
||||
conn_track_key: None,
|
||||
rate_limit_keys: vec![],
|
||||
chain_type,
|
||||
@@ -581,8 +621,9 @@ impl AclProcessor {
|
||||
// Cache the result with rule info
|
||||
self.increment_stat(AclStatKey::RuleMatches);
|
||||
self.inc_cache_entry_stats(&cache_entry, packet_info);
|
||||
self.cache_result(&cache_key, cache_entry.clone());
|
||||
return cache_entry.acl_result.clone().unwrap();
|
||||
let result = cache_entry.acl_result.clone().unwrap();
|
||||
self.cache_result(&cache_key, cache_entry);
|
||||
return result;
|
||||
}
|
||||
|
||||
let default_action = match chain_type {
|
||||
@@ -618,8 +659,9 @@ impl AclProcessor {
|
||||
|
||||
// Cache the default result (no rule info)
|
||||
self.inc_cache_entry_stats(&cache_entry, packet_info);
|
||||
self.cache_result(&cache_key, cache_entry.clone());
|
||||
cache_entry.acl_result.clone().unwrap()
|
||||
let result = cache_entry.acl_result.clone().unwrap();
|
||||
self.cache_result(&cache_key, cache_entry);
|
||||
result
|
||||
}
|
||||
|
||||
/// Get shared state for preserving across hot reloads
|
||||
@@ -743,13 +785,11 @@ impl AclProcessor {
|
||||
|
||||
/// Check connection state for stateful rules
|
||||
fn check_connection_state(&self, conn_track_key: &str, packet_info: &PacketInfo) {
|
||||
let now = current_unix_secs();
|
||||
self.conn_track
|
||||
.entry(conn_track_key.to_string())
|
||||
.and_modify(|x| {
|
||||
x.last_seen = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
x.last_seen = now;
|
||||
x.packet_count += 1;
|
||||
x.byte_count += packet_info.packet_size as u64;
|
||||
x.state = ConnState::Established as i32;
|
||||
@@ -763,14 +803,8 @@ impl AclProcessor {
|
||||
),
|
||||
protocol: packet_info.protocol as i32,
|
||||
state: ConnState::New as i32,
|
||||
created_at: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
last_seen: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
created_at: now,
|
||||
last_seen: now,
|
||||
packet_count: 1,
|
||||
byte_count: packet_info.packet_size as u64,
|
||||
});
|
||||
@@ -862,13 +896,7 @@ impl AclProcessor {
|
||||
stateful: rule.stateful,
|
||||
rate_limit: rule.rate_limit,
|
||||
burst_limit: rule.burst_limit,
|
||||
rule_stats: Arc::new(RuleStats {
|
||||
rule: Some(rule.clone()),
|
||||
stat: Some(StatItem {
|
||||
packet_count: 0,
|
||||
byte_count: 0,
|
||||
}),
|
||||
}),
|
||||
rule_stats: Arc::new(RuleStatsTracker::new(Some(rule.clone()))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,6 +922,10 @@ impl AclProcessor {
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// Add cache statistics using enum keys
|
||||
stats.insert(
|
||||
AclStatKey::CacheHits.as_str(),
|
||||
self.cache_hits.load(Relaxed),
|
||||
);
|
||||
stats.insert(AclStatKey::CacheSize.as_str(), self.rule_cache.len() as u64);
|
||||
stats.insert(
|
||||
AclStatKey::CacheMaxSize.as_str(),
|
||||
@@ -908,14 +940,11 @@ impl AclProcessor {
|
||||
conn_track: Arc<DashMap<String, ConnTrackEntry>>,
|
||||
timeout_secs: u64,
|
||||
) {
|
||||
let current_time = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let current_time = current_unix_secs();
|
||||
let keys_to_remove: Vec<String> = conn_track
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if current_time - entry.last_seen > timeout_secs {
|
||||
if current_time.saturating_sub(entry.last_seen) > timeout_secs {
|
||||
Some(entry.key().clone())
|
||||
} else {
|
||||
None
|
||||
@@ -930,11 +959,7 @@ impl AclProcessor {
|
||||
|
||||
/// Get cache hit rate
|
||||
pub fn get_cache_hit_rate(&self) -> f64 {
|
||||
let cache_hits = self
|
||||
.stats
|
||||
.get(&AclStatKey::CacheHits)
|
||||
.map(|v| *v.value())
|
||||
.unwrap_or(0);
|
||||
let cache_hits = self.cache_hits.load(Relaxed);
|
||||
let total_requests = cache_hits
|
||||
+ self
|
||||
.stats
|
||||
@@ -950,6 +975,13 @@ impl AclProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_unix_secs() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
// 新增辅助函数
|
||||
fn parse_port_start(port_strs: &[String]) -> Option<u16> {
|
||||
port_strs
|
||||
|
||||
@@ -219,6 +219,7 @@ pub struct GlobalCtx {
|
||||
|
||||
running_listeners: Mutex<Vec<url::Url>>,
|
||||
advertised_ipv6_public_addr_prefix: Mutex<Option<cidr::Ipv6Cidr>>,
|
||||
tun_device_name: Mutex<Option<String>>,
|
||||
|
||||
flags: ArcSwap<Flags>,
|
||||
|
||||
@@ -336,6 +337,7 @@ impl GlobalCtx {
|
||||
|
||||
running_listeners: Mutex::new(Vec::new()),
|
||||
advertised_ipv6_public_addr_prefix: Mutex::new(None),
|
||||
tun_device_name: Mutex::new(None),
|
||||
|
||||
flags: ArcSwap::new(Arc::new(flags)),
|
||||
|
||||
@@ -370,6 +372,24 @@ impl GlobalCtx {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_tun_device_name(&self, name: Option<String>) {
|
||||
*self.tun_device_name.lock().unwrap() = name;
|
||||
}
|
||||
|
||||
pub(crate) fn set_tun_device_ready(&self, name: String) {
|
||||
self.set_tun_device_name(Some(name.clone()));
|
||||
self.issue_event(GlobalCtxEvent::TunDeviceReady(name));
|
||||
}
|
||||
|
||||
pub(crate) fn set_tun_device_error(&self, error: String) {
|
||||
self.set_tun_device_name(None);
|
||||
self.issue_event(GlobalCtxEvent::TunDeviceError(error));
|
||||
}
|
||||
|
||||
pub fn get_tun_device_name(&self) -> Option<String> {
|
||||
self.tun_device_name.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn check_network_in_whitelist(&self, network_name: &str) -> Result<(), anyhow::Error> {
|
||||
if self
|
||||
.get_flags()
|
||||
@@ -825,6 +845,36 @@ pub mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tun_device_name_tracks_explicit_runtime_state() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config);
|
||||
|
||||
assert_eq!(global_ctx.get_tun_device_name(), None);
|
||||
|
||||
global_ctx.issue_event(GlobalCtxEvent::TunDeviceReady("ignored".to_string()));
|
||||
assert_eq!(global_ctx.get_tun_device_name(), None);
|
||||
|
||||
let mut subscriber = global_ctx.subscribe();
|
||||
|
||||
global_ctx.set_tun_device_ready("easytier0".to_string());
|
||||
assert_eq!(
|
||||
global_ctx.get_tun_device_name(),
|
||||
Some("easytier0".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
subscriber.recv().await.unwrap(),
|
||||
GlobalCtxEvent::TunDeviceReady("easytier0".to_string())
|
||||
);
|
||||
|
||||
global_ctx.set_tun_device_error("closed".to_string());
|
||||
assert_eq!(global_ctx.get_tun_device_name(), None);
|
||||
assert_eq!(
|
||||
subscriber.recv().await.unwrap(),
|
||||
GlobalCtxEvent::TunDeviceError("closed".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn trusted_key_source_lookup_is_precise() {
|
||||
let config = TomlConfigLoader::default();
|
||||
|
||||
@@ -177,3 +177,20 @@ pub(crate) fn list_ipv6_route_messages()
|
||||
pub(crate) fn get_interface_index(name: &str) -> Result<u32, Error> {
|
||||
netlink::NetlinkIfConfiger::get_interface_index(name)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn add_ipv6_ndp_proxy(name: &str, address: Ipv6Addr) -> Result<(), Error> {
|
||||
netlink::NetlinkIfConfiger::add_ipv6_ndp_proxy(name, address)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn remove_ipv6_ndp_proxy(name: &str, address: Ipv6Addr) -> Result<(), Error> {
|
||||
netlink::NetlinkIfConfiger::remove_ipv6_ndp_proxy(name, address)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn list_ipv6_ndp_proxy(
|
||||
name: &str,
|
||||
) -> Result<std::collections::BTreeSet<Ipv6Addr>, Error> {
|
||||
netlink::NetlinkIfConfiger::list_ipv6_ndp_proxy(name)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
ffi::CString,
|
||||
fmt::Debug,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
@@ -16,6 +17,10 @@ use netlink_packet_core::{
|
||||
use netlink_packet_route::{
|
||||
AddressFamily, RouteNetlinkMessage,
|
||||
address::{AddressAttribute, AddressMessage},
|
||||
neighbour::{
|
||||
NeighbourAddress, NeighbourAttribute, NeighbourFlags, NeighbourHeader, NeighbourMessage,
|
||||
NeighbourState,
|
||||
},
|
||||
route::{
|
||||
RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope,
|
||||
RouteType,
|
||||
@@ -375,6 +380,105 @@ impl NetlinkIfConfiger {
|
||||
pub(crate) fn list_ipv6_route_messages() -> Result<Vec<RouteMessage>, Error> {
|
||||
Self::list_route_messages(AddressFamily::Inet6)
|
||||
}
|
||||
|
||||
fn ipv6_ndp_proxy_message(name: &str, address: Ipv6Addr) -> Result<NeighbourMessage, Error> {
|
||||
let mut message = NeighbourMessage::default();
|
||||
message.header = NeighbourHeader {
|
||||
family: AddressFamily::Inet6,
|
||||
ifindex: Self::get_interface_index(name)?,
|
||||
state: NeighbourState::Permanent,
|
||||
flags: NeighbourFlags::Proxy,
|
||||
kind: RouteType::Unicast,
|
||||
};
|
||||
message
|
||||
.attributes
|
||||
.push(NeighbourAttribute::Destination(NeighbourAddress::Inet6(
|
||||
address,
|
||||
)));
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub(crate) fn add_ipv6_ndp_proxy(name: &str, address: Ipv6Addr) -> Result<(), Error> {
|
||||
send_netlink_req_and_wait_one_resp(
|
||||
RouteNetlinkMessage::NewNeighbour(Self::ipv6_ndp_proxy_message(name, address)?),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_ipv6_ndp_proxy(name: &str, address: Ipv6Addr) -> Result<(), Error> {
|
||||
send_netlink_req_and_wait_one_resp(
|
||||
RouteNetlinkMessage::DelNeighbour(Self::ipv6_ndp_proxy_message(name, address)?),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
fn list_neighbour_messages(
|
||||
address_family: AddressFamily,
|
||||
) -> Result<Vec<NeighbourMessage>, Error> {
|
||||
let mut message = NeighbourMessage::default();
|
||||
message.header.family = address_family;
|
||||
message.header.flags = NeighbourFlags::Proxy;
|
||||
|
||||
let s = send_netlink_req(
|
||||
RouteNetlinkMessage::GetNeighbour(message),
|
||||
NLM_F_REQUEST | NLM_F_DUMP,
|
||||
)?;
|
||||
|
||||
let mut ret_vec = vec![];
|
||||
let mut resp = Vec::<u8>::new();
|
||||
loop {
|
||||
if resp.is_empty() {
|
||||
let (new_resp, _) = s.recv_from_full()?;
|
||||
resp = new_resp;
|
||||
}
|
||||
|
||||
let ret = NetlinkMessage::<RouteNetlinkMessage>::deserialize(&resp)
|
||||
.with_context(|| "Failed to deserialize netlink neighbour message")?;
|
||||
resp = resp.split_off(ret.buffer_len());
|
||||
|
||||
tracing::debug!("net link response <<< {:?}", ret);
|
||||
|
||||
match ret.payload {
|
||||
NetlinkPayload::Error(e) => {
|
||||
if e.code == NonZero::new(0) {
|
||||
continue;
|
||||
} else {
|
||||
return Err(e.to_io().into());
|
||||
}
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewNeighbour(m)) => {
|
||||
ret_vec.push(m);
|
||||
}
|
||||
NetlinkPayload::Done(_) => {
|
||||
break;
|
||||
}
|
||||
p => {
|
||||
tracing::error!("Unexpected netlink response: {:?}", p);
|
||||
return Err(anyhow::anyhow!("Unexpected netlink response").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret_vec)
|
||||
}
|
||||
|
||||
pub(crate) fn list_ipv6_ndp_proxy(name: &str) -> Result<BTreeSet<Ipv6Addr>, Error> {
|
||||
let ifindex = Self::get_interface_index(name)?;
|
||||
|
||||
Ok(Self::list_neighbour_messages(AddressFamily::Inet6)?
|
||||
.into_iter()
|
||||
.filter(|message| {
|
||||
message.header.ifindex == ifindex
|
||||
&& message.header.flags.contains(NeighbourFlags::Proxy)
|
||||
})
|
||||
.filter_map(|message| {
|
||||
message.attributes.into_iter().find_map(|attr| match attr {
|
||||
NeighbourAttribute::Destination(NeighbourAddress::Inet6(addr)) => Some(addr),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#[cfg(feature = "lttng")]
|
||||
lttng_ust::import_tracepoints!(
|
||||
concat!(env!("OUT_DIR"), "/lttng_tracepoints.rs"),
|
||||
tracepoints
|
||||
);
|
||||
|
||||
#[cfg(feature = "lttng")]
|
||||
pub fn mark(name: &str, value: u64) {
|
||||
tracepoints::easytier::mark(name, value);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "lttng"))]
|
||||
#[inline(always)]
|
||||
pub fn mark(_name: &str, _value: u64) {}
|
||||
|
||||
#[cfg(feature = "lttng")]
|
||||
pub fn duration_micros(name: &str, duration_micros: u64) {
|
||||
tracepoints::easytier::duration_micros(name, duration_micros);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "lttng"))]
|
||||
#[inline(always)]
|
||||
pub fn duration_micros(_name: &str, _duration_micros: u64) {}
|
||||
@@ -18,11 +18,11 @@ pub mod global_ctx;
|
||||
pub mod idn;
|
||||
pub mod ifcfg;
|
||||
pub mod log;
|
||||
pub mod lttng;
|
||||
pub mod machine_id;
|
||||
pub mod netns;
|
||||
pub mod network;
|
||||
pub mod os_info;
|
||||
pub mod sharded_counter;
|
||||
pub mod stats_manager;
|
||||
pub mod stun;
|
||||
pub mod stun_codec_ext;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
|
||||
|
||||
const PUBLISH_INTERVAL: u64 = 256;
|
||||
|
||||
/// Counter optimized for long-lived worker threads.
|
||||
///
|
||||
/// Pending values below `PUBLISH_INTERVAL` live in `ThreadLocal` shards. The
|
||||
/// shards are retained until this counter is dropped, so this is intended for
|
||||
/// tokio workers or similarly long-lived threads rather than high-churn threads.
|
||||
pub struct ShardedCounter {
|
||||
published: AtomicU64,
|
||||
locals: thread_local::ThreadLocal<AtomicU64>,
|
||||
}
|
||||
|
||||
impl ShardedCounter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
published: AtomicU64::new(0),
|
||||
locals: thread_local::ThreadLocal::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure(impl_type = "ShardedCounter"))]
|
||||
pub fn add(&self, delta: u64) {
|
||||
let local = self.locals.get_or(|| AtomicU64::new(0));
|
||||
let v = local.load(Relaxed).saturating_add(delta);
|
||||
local.store(v, Relaxed);
|
||||
if v >= PUBLISH_INTERVAL {
|
||||
let pending = local.swap(0, Relaxed);
|
||||
if pending > 0 {
|
||||
self.published.fetch_add(pending, Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn inc(&self) {
|
||||
self.add(1);
|
||||
}
|
||||
|
||||
pub fn get(&self) -> u64 {
|
||||
self.locals
|
||||
.iter()
|
||||
.fold(self.published.load(Relaxed), |total, local| {
|
||||
total.saturating_add(local.load(Relaxed))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.published.store(0, Relaxed);
|
||||
for local in self.locals.iter() {
|
||||
local.store(0, Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShardedCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ShardedCounter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ShardedCounter")
|
||||
.field("value", &self.get())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{sync::Arc, thread};
|
||||
|
||||
#[test]
|
||||
fn sharded_counter_get_includes_other_thread_locals() {
|
||||
let counter = Arc::new(ShardedCounter::new());
|
||||
let thread_counter = Arc::clone(&counter);
|
||||
|
||||
thread::spawn(move || {
|
||||
for _ in 0..10 {
|
||||
thread_counter.inc();
|
||||
}
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(counter.get(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sharded_counter_reset_clears_other_thread_locals() {
|
||||
let counter = Arc::new(ShardedCounter::new());
|
||||
let thread_counter = Arc::clone(&counter);
|
||||
|
||||
thread::spawn(move || {
|
||||
for _ in 0..10 {
|
||||
thread_counter.inc();
|
||||
}
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
|
||||
counter.reset();
|
||||
|
||||
assert_eq!(counter.get(), 0);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
use crate::common::sharded_counter::ShardedCounter;
|
||||
use dashmap::DashMap;
|
||||
use quanta::Instant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::UnsafeCell;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
static START_INSTANT: LazyLock<Instant> = LazyLock::new(Instant::now);
|
||||
|
||||
/// Predefined metric names for type safety
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum MetricName {
|
||||
@@ -375,10 +378,10 @@ impl Default for LabelSet {
|
||||
}
|
||||
}
|
||||
|
||||
/// UnsafeCounter provides a high-performance counter using UnsafeCell
|
||||
/// High-performance counter backed by sharded thread-local accumulation.
|
||||
#[derive(Debug)]
|
||||
pub struct UnsafeCounter {
|
||||
value: UnsafeCell<u64>,
|
||||
inner: ShardedCounter,
|
||||
}
|
||||
|
||||
impl Default for UnsafeCounter {
|
||||
@@ -390,121 +393,56 @@ impl Default for UnsafeCounter {
|
||||
impl UnsafeCounter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
value: UnsafeCell::new(0),
|
||||
inner: ShardedCounter::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_value(initial: u64) -> Self {
|
||||
Self {
|
||||
value: UnsafeCell::new(initial),
|
||||
}
|
||||
pub fn add(&self, delta: u64) {
|
||||
self.inner.add(delta);
|
||||
}
|
||||
|
||||
/// Increment the counter by the given amount
|
||||
/// # Safety
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn add(&self, delta: u64) {
|
||||
let ptr = self.value.get();
|
||||
unsafe {
|
||||
*ptr = (*ptr).saturating_add(delta);
|
||||
}
|
||||
pub fn inc(&self) {
|
||||
self.inner.inc();
|
||||
}
|
||||
|
||||
/// Increment the counter by 1
|
||||
/// # Safety
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn inc(&self) {
|
||||
unsafe {
|
||||
self.add(1);
|
||||
}
|
||||
pub fn get(&self) -> u64 {
|
||||
self.inner.get()
|
||||
}
|
||||
|
||||
/// Get the current value of the counter
|
||||
/// # Safety
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is modifying this counter simultaneously.
|
||||
pub unsafe fn get(&self) -> u64 {
|
||||
let ptr = self.value.get();
|
||||
unsafe { *ptr }
|
||||
}
|
||||
|
||||
/// Reset the counter to zero
|
||||
/// # Safety
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn reset(&self) {
|
||||
let ptr = self.value.get();
|
||||
unsafe {
|
||||
*ptr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the counter to a specific value
|
||||
/// # Safety
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn set(&self, value: u64) {
|
||||
let ptr = self.value.get();
|
||||
unsafe {
|
||||
*ptr = value;
|
||||
}
|
||||
pub fn reset(&self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// UnsafeCounter is Send + Sync because the safety is guaranteed by the caller
|
||||
unsafe impl Send for UnsafeCounter {}
|
||||
unsafe impl Sync for UnsafeCounter {}
|
||||
|
||||
/// MetricData contains both the counter and last update timestamp
|
||||
/// Uses UnsafeCell for lock-free access
|
||||
#[derive(Debug)]
|
||||
struct MetricData {
|
||||
counter: UnsafeCounter,
|
||||
last_updated: UnsafeCell<Instant>,
|
||||
last_updated: AtomicU64,
|
||||
}
|
||||
|
||||
pub(crate) fn now_monotonic_millis() -> u64 {
|
||||
Instant::now().duration_since(*START_INSTANT).as_millis() as u64
|
||||
}
|
||||
|
||||
impl MetricData {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
counter: UnsafeCounter::new(),
|
||||
last_updated: UnsafeCell::new(Instant::now()),
|
||||
last_updated: AtomicU64::new(now_monotonic_millis()),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_with_value(initial: u64) -> Self {
|
||||
Self {
|
||||
counter: UnsafeCounter::new_with_value(initial),
|
||||
last_updated: UnsafeCell::new(Instant::now()),
|
||||
}
|
||||
fn touch(&self) {
|
||||
self.last_updated
|
||||
.store(now_monotonic_millis(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Update the last_updated timestamp
|
||||
/// # Safety
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is accessing this timestamp simultaneously.
|
||||
unsafe fn touch(&self) {
|
||||
let ptr = self.last_updated.get();
|
||||
unsafe {
|
||||
*ptr = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last updated timestamp
|
||||
/// # Safety
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is modifying this timestamp simultaneously.
|
||||
unsafe fn get_last_updated(&self) -> Instant {
|
||||
let ptr = self.last_updated.get();
|
||||
unsafe { *ptr }
|
||||
fn get_last_updated(&self) -> u64 {
|
||||
self.last_updated.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
// MetricData is Send + Sync because the safety is guaranteed by the caller
|
||||
unsafe impl Send for MetricData {}
|
||||
unsafe impl Sync for MetricData {}
|
||||
|
||||
/// MetricKey uniquely identifies a metric with its name and labels
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct MetricKey {
|
||||
@@ -545,41 +483,23 @@ impl CounterHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the counter by the given amount
|
||||
pub fn add(&self, delta: u64) {
|
||||
unsafe {
|
||||
self.metric_data.counter.add(delta);
|
||||
self.metric_data.touch();
|
||||
}
|
||||
self.metric_data.counter.add(delta);
|
||||
self.metric_data.touch();
|
||||
}
|
||||
|
||||
/// Increment the counter by 1
|
||||
pub fn inc(&self) {
|
||||
unsafe {
|
||||
self.metric_data.counter.inc();
|
||||
self.metric_data.touch();
|
||||
}
|
||||
self.metric_data.counter.inc();
|
||||
self.metric_data.touch();
|
||||
}
|
||||
|
||||
/// Get the current value of the counter
|
||||
pub fn get(&self) -> u64 {
|
||||
unsafe { self.metric_data.counter.get() }
|
||||
self.metric_data.counter.get()
|
||||
}
|
||||
|
||||
/// Reset the counter to zero
|
||||
pub fn reset(&self) {
|
||||
unsafe {
|
||||
self.metric_data.counter.reset();
|
||||
self.metric_data.touch();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the counter to a specific value
|
||||
pub fn set(&self, value: u64) {
|
||||
unsafe {
|
||||
self.metric_data.counter.set(value);
|
||||
self.metric_data.touch();
|
||||
}
|
||||
self.metric_data.counter.reset();
|
||||
self.metric_data.touch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,9 +535,7 @@ impl StatsManager {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let Some(cutoff_time) = Instant::now().checked_sub(Duration::from_secs(180)) else {
|
||||
continue;
|
||||
};
|
||||
let cutoff_millis = now_monotonic_millis().saturating_sub(180_000);
|
||||
|
||||
let Some(counters) = counters_clone.upgrade() else {
|
||||
break;
|
||||
@@ -625,7 +543,7 @@ impl StatsManager {
|
||||
|
||||
counters.retain(|_, metric_data: &mut Arc<MetricData>| {
|
||||
Arc::strong_count(metric_data) > 1
|
||||
|| unsafe { metric_data.get_last_updated() > cutoff_time }
|
||||
|| metric_data.get_last_updated() >= cutoff_millis
|
||||
});
|
||||
counters.shrink_to_fit();
|
||||
}
|
||||
@@ -663,7 +581,7 @@ impl StatsManager {
|
||||
let key = entry.key();
|
||||
let metric_data = entry.value();
|
||||
|
||||
let value = unsafe { metric_data.counter.get() };
|
||||
let value = metric_data.counter.get();
|
||||
|
||||
metrics.push(MetricSnapshot {
|
||||
name: key.name,
|
||||
@@ -696,7 +614,7 @@ impl StatsManager {
|
||||
let key = MetricKey::new(name, labels.clone());
|
||||
|
||||
if let Some(metric_data) = self.counters.get(&key) {
|
||||
let value = unsafe { metric_data.counter.get() };
|
||||
let value = metric_data.counter.get();
|
||||
Some(MetricSnapshot {
|
||||
name,
|
||||
labels: labels.clone(),
|
||||
@@ -797,17 +715,9 @@ mod tests {
|
||||
async fn test_unsafe_counter() {
|
||||
let counter = UnsafeCounter::new();
|
||||
|
||||
unsafe {
|
||||
assert_eq!(counter.get(), 0);
|
||||
counter.inc();
|
||||
assert_eq!(counter.get(), 1);
|
||||
counter.add(5);
|
||||
assert_eq!(counter.get(), 6);
|
||||
counter.set(10);
|
||||
assert_eq!(counter.get(), 10);
|
||||
counter.reset();
|
||||
assert_eq!(counter.get(), 0);
|
||||
}
|
||||
assert_eq!(counter.get(), 0);
|
||||
counter.add(256);
|
||||
assert_eq!(counter.get(), 256);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -853,11 +763,11 @@ mod tests {
|
||||
let stats = StatsManager::new();
|
||||
|
||||
let counter1 = stats.get_simple_counter(MetricName::TrafficBytesTx);
|
||||
counter1.set(100);
|
||||
counter1.add(100);
|
||||
|
||||
let labels = LabelSet::new().with_label("status", "success");
|
||||
let counter2 = stats.get_counter(MetricName::PeerRpcClientTx, labels);
|
||||
counter2.set(50);
|
||||
counter2.add(50);
|
||||
|
||||
let traffic_labels = LabelSet::new()
|
||||
.with_label_type(LabelType::NetworkName("default".to_string()))
|
||||
@@ -865,7 +775,7 @@ mod tests {
|
||||
"87ede5a2-9c3d-492d-9bbe-989b9d07e742".to_string(),
|
||||
));
|
||||
let counter3 = stats.get_counter(MetricName::TrafficBytesTxByInstance, traffic_labels);
|
||||
counter3.set(25);
|
||||
counter3.add(25);
|
||||
|
||||
let prometheus_output = stats.export_prometheus();
|
||||
|
||||
@@ -885,7 +795,7 @@ mod tests {
|
||||
|
||||
let labels = LabelSet::new().with_label("peer", "test");
|
||||
let counter = stats.get_counter(MetricName::PeerRpcClientTx, labels.clone());
|
||||
counter.set(42);
|
||||
counter.add(42);
|
||||
|
||||
let metric = stats
|
||||
.get_metric(MetricName::PeerRpcClientTx, &labels)
|
||||
@@ -902,11 +812,11 @@ mod tests {
|
||||
|
||||
stats
|
||||
.get_simple_counter(MetricName::PeerRpcClientTx)
|
||||
.set(10);
|
||||
stats.get_simple_counter(MetricName::PeerRpcErrors).set(2);
|
||||
.add(10);
|
||||
stats.get_simple_counter(MetricName::PeerRpcErrors).add(2);
|
||||
stats
|
||||
.get_simple_counter(MetricName::TrafficBytesTx)
|
||||
.set(100);
|
||||
.add(100);
|
||||
|
||||
let rpc_metrics = stats.get_metrics_by_prefix("peer_rpc");
|
||||
assert_eq!(rpc_metrics.len(), 2);
|
||||
@@ -921,19 +831,15 @@ mod tests {
|
||||
|
||||
// 创建一些计数器
|
||||
let counter1 = stats.get_simple_counter(MetricName::PeerRpcClientTx);
|
||||
counter1.set(10);
|
||||
counter1.add(10);
|
||||
|
||||
let labels = LabelSet::new().with_label("test", "value");
|
||||
let counter2 = stats.get_counter(MetricName::TrafficBytesTx, labels);
|
||||
counter2.set(20);
|
||||
counter2.add(20);
|
||||
|
||||
// 验证计数器存在
|
||||
assert_eq!(stats.metric_count(), 2);
|
||||
|
||||
// 注意:实际的清理测试需要等待3分钟,这在单元测试中不现实
|
||||
// 这里我们只验证清理机制的基本结构是否正确
|
||||
// 清理逻辑在后台线程中运行,会自动删除超过3分钟未更新的条目
|
||||
|
||||
// 验证计数器仍然可以正常工作
|
||||
counter1.inc();
|
||||
assert_eq!(counter1.get(), 11);
|
||||
@@ -946,14 +852,14 @@ mod tests {
|
||||
async fn test_cleanup_keeps_metrics_with_live_handles() {
|
||||
let stats = StatsManager::new();
|
||||
let counter = stats.get_simple_counter(MetricName::TrafficBytesForwarded);
|
||||
counter.set(1);
|
||||
counter.add(1);
|
||||
|
||||
let cutoff_time = Instant::now().checked_add(Duration::from_secs(1)).unwrap();
|
||||
// Use a future cutoff so last_updated check always fails
|
||||
let future_cutoff = now_monotonic_millis() + 1000;
|
||||
stats
|
||||
.counters
|
||||
.retain(|_, metric_data: &mut Arc<MetricData>| {
|
||||
Arc::strong_count(metric_data) > 1
|
||||
|| unsafe { metric_data.get_last_updated() > cutoff_time }
|
||||
Arc::strong_count(metric_data) > 1 || metric_data.get_last_updated() > future_cutoff
|
||||
});
|
||||
|
||||
assert_eq!(stats.metric_count(), 1);
|
||||
@@ -963,8 +869,7 @@ mod tests {
|
||||
stats
|
||||
.counters
|
||||
.retain(|_, metric_data: &mut Arc<MetricData>| {
|
||||
Arc::strong_count(metric_data) > 1
|
||||
|| unsafe { metric_data.get_last_updated() > cutoff_time }
|
||||
Arc::strong_count(metric_data) > 1 || metric_data.get_last_updated() > future_cutoff
|
||||
});
|
||||
assert_eq!(stats.metric_count(), 0);
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@ use crate::vpn_portal::{self, VpnPortal};
|
||||
use super::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
|
||||
use super::listeners::ListenerManager;
|
||||
use super::public_ipv6_provider::{
|
||||
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
|
||||
should_run_public_ipv6_provider_reconcile, validate_public_ipv6_config,
|
||||
validate_public_ipv6_config_values,
|
||||
PublicIpv6ProviderReconcileTask, reconcile_public_ipv6_provider_runtime,
|
||||
run_public_ipv6_provider_reconcile_task, should_run_public_ipv6_provider_reconcile,
|
||||
validate_public_ipv6_config, validate_public_ipv6_config_values,
|
||||
};
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
@@ -194,6 +194,44 @@ impl NicCtxContainer {
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
type ArcNicCtx = Arc<Mutex<Option<NicCtxContainer>>>;
|
||||
type ArcPublicIpv6ProviderTaskSlot = Arc<PublicIpv6ProviderTaskSlot>;
|
||||
|
||||
struct PublicIpv6ProviderTaskSlot {
|
||||
task: Mutex<Option<PublicIpv6ProviderReconcileTask>>,
|
||||
closing: AtomicBool,
|
||||
}
|
||||
|
||||
impl PublicIpv6ProviderTaskSlot {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
task: Mutex::new(None),
|
||||
closing: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_started(&self, global_ctx: &ArcGlobalCtx) {
|
||||
let mut task = self.task.lock().await;
|
||||
if self.closing.load(Ordering::Acquire) || task.is_some() {
|
||||
return;
|
||||
}
|
||||
*task = run_public_ipv6_provider_reconcile_task(global_ctx);
|
||||
}
|
||||
|
||||
async fn shutdown(&self) {
|
||||
self.closing.store(true, Ordering::Release);
|
||||
let task = self.task.lock().await.take();
|
||||
if let Some(task) = task {
|
||||
task.shutdown().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_public_ipv6_provider_reconcile_task(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
task_slot: &ArcPublicIpv6ProviderTaskSlot,
|
||||
) {
|
||||
task_slot.ensure_started(global_ctx).await;
|
||||
}
|
||||
|
||||
pub struct InstanceRpcServerHook {
|
||||
rpc_portal_whitelist: Vec<IpCidr>,
|
||||
@@ -254,6 +292,7 @@ pub struct InstanceConfigPatcher {
|
||||
socks5_server: Weak<Socks5Server>,
|
||||
peer_manager: Weak<PeerManager>,
|
||||
conn_manager: Weak<ManualConnectorManager>,
|
||||
public_ipv6_provider_task: ArcPublicIpv6ProviderTaskSlot,
|
||||
}
|
||||
|
||||
impl InstanceConfigPatcher {
|
||||
@@ -324,7 +363,6 @@ impl InstanceConfigPatcher {
|
||||
self.patch_mapped_listeners(patch.mapped_listeners).await?;
|
||||
self.patch_connector(patch.connectors).await?;
|
||||
|
||||
let provider_reconcile_was_running = should_run_public_ipv6_provider_reconcile(&global_ctx);
|
||||
let mut provider_config_changed = false;
|
||||
if let Some(hostname) = patch.hostname {
|
||||
global_ctx.set_hostname(hostname.clone());
|
||||
@@ -362,10 +400,12 @@ impl InstanceConfigPatcher {
|
||||
if provider_config_changed {
|
||||
reconcile_public_ipv6_provider_runtime(&global_ctx).await;
|
||||
|
||||
let provider_reconcile_should_run =
|
||||
should_run_public_ipv6_provider_reconcile(&global_ctx);
|
||||
if !provider_reconcile_was_running && provider_reconcile_should_run {
|
||||
run_public_ipv6_provider_reconcile_task(&global_ctx);
|
||||
if should_run_public_ipv6_provider_reconcile(&global_ctx) {
|
||||
ensure_public_ipv6_provider_reconcile_task(
|
||||
&global_ctx,
|
||||
&self.public_ipv6_provider_task,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,6 +687,7 @@ pub struct Instance {
|
||||
socks5_server: Arc<Socks5Server>,
|
||||
|
||||
proxy_cidrs_monitor: Option<AbortOnDropHandle<()>>,
|
||||
public_ipv6_provider_task: ArcPublicIpv6ProviderTaskSlot,
|
||||
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
@@ -734,6 +775,7 @@ impl Instance {
|
||||
socks5_server,
|
||||
|
||||
proxy_cidrs_monitor: None,
|
||||
public_ipv6_provider_task: Arc::new(PublicIpv6ProviderTaskSlot::new()),
|
||||
|
||||
global_ctx,
|
||||
}
|
||||
@@ -1034,7 +1076,11 @@ impl Instance {
|
||||
.await?;
|
||||
self.listener_manager.lock().await.run().await?;
|
||||
self.peer_manager.run().await?;
|
||||
run_public_ipv6_provider_reconcile_task(&self.global_ctx);
|
||||
ensure_public_ipv6_provider_reconcile_task(
|
||||
&self.global_ctx,
|
||||
&self.public_ipv6_provider_task,
|
||||
)
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
{
|
||||
@@ -1347,6 +1393,7 @@ impl Instance {
|
||||
socks5_server: Arc::downgrade(&self.socks5_server),
|
||||
peer_manager: Arc::downgrade(&self.peer_manager),
|
||||
conn_manager: Arc::downgrade(&self.conn_manager),
|
||||
public_ipv6_provider_task: self.public_ipv6_provider_task.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1602,6 +1649,7 @@ impl Instance {
|
||||
}
|
||||
|
||||
pub async fn clear_resources(&mut self) {
|
||||
self.public_ipv6_provider_task.shutdown().await;
|
||||
self.peer_manager.clear_resources().await;
|
||||
#[cfg(feature = "tun")]
|
||||
let _ = self.nic_ctx.lock().await.take();
|
||||
@@ -1787,6 +1835,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_ipv6_provider_task_slot_does_not_restart_after_shutdown() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let slot = std::sync::Arc::new(super::PublicIpv6ProviderTaskSlot::new());
|
||||
global_ctx.config.set_ipv6_public_addr_provider(true);
|
||||
global_ctx
|
||||
.config
|
||||
.set_ipv6_public_addr_prefix(Some("2001:db8::/48".parse().unwrap()));
|
||||
|
||||
slot.shutdown().await;
|
||||
super::ensure_public_ipv6_provider_reconcile_task(&global_ctx, &slot).await;
|
||||
|
||||
assert!(slot.task.lock().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_public_ipv6_patch_allows_enabling_auto_with_manual_ipv6() {
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use byteorder::WriteBytesExt as _;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use cidr::{Ipv4Inet, Ipv6Inet};
|
||||
use futures::{SinkExt, Stream, StreamExt, lock::BiLock, ready};
|
||||
use pin_project_lite::pin_project;
|
||||
@@ -180,12 +180,13 @@ impl ZCPacketToBytes for TunZCPacketToBytes {
|
||||
assert!(payload_offset >= 4);
|
||||
|
||||
let ret = if self.has_packet_info {
|
||||
let mut inner = inner.split_off(payload_offset - 4);
|
||||
inner.advance(payload_offset - 4);
|
||||
let proto = infer_proto(&inner[4..]);
|
||||
self.fill_packet_info(&mut inner[0..4], proto)?;
|
||||
inner
|
||||
} else {
|
||||
inner.split_off(payload_offset)
|
||||
inner.advance(payload_offset);
|
||||
inner
|
||||
};
|
||||
|
||||
tracing::debug!(?ret, ?payload_offset, "convert zc packet to tun packet");
|
||||
@@ -1361,12 +1362,11 @@ impl NicCtx {
|
||||
}
|
||||
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string()));
|
||||
.set_tun_device_ready(nic.ifname().to_string());
|
||||
ret
|
||||
}
|
||||
Err(err) => {
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::TunDeviceError(err.to_string()));
|
||||
self.global_ctx.set_tun_device_error(err.to_string());
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
@@ -1405,12 +1405,11 @@ impl NicCtx {
|
||||
match nic.create_dev_for_mobile(tun_fd).await {
|
||||
Ok(ret) => {
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string()));
|
||||
.set_tun_device_ready(nic.ifname().to_string());
|
||||
ret
|
||||
}
|
||||
Err(err) => {
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::TunDeviceError(err.to_string()));
|
||||
self.global_ctx.set_tun_device_error(err.to_string());
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,12 @@ impl EasyTierLauncher {
|
||||
#[cfg(mobile)]
|
||||
Self::run_routine_for_mobile(&instance, &data, &mut tasks).await;
|
||||
|
||||
instance.run().await?;
|
||||
if let Err(err) = instance.run().await {
|
||||
tasks.abort_all();
|
||||
drop(tasks);
|
||||
instance.clear_resources().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
data.data_plane
|
||||
|
||||
@@ -990,13 +990,9 @@ mod tests {
|
||||
|
||||
s.sync_root_key(root_key, 2, 2, true);
|
||||
assert!(s.check_replay_for_test(2, 0, SecureDatagramDirection::AToB, now + 2));
|
||||
let expires_at = s.sync_rx_grace_expires_at_ms.load(Ordering::Relaxed);
|
||||
|
||||
assert!(!s.check_replay_for_test(
|
||||
0,
|
||||
1,
|
||||
SecureDatagramDirection::AToB,
|
||||
now + SecureDatagramSession::SYNC_RX_GRACE_AFTER_MS + 3
|
||||
));
|
||||
assert!(!s.check_replay_for_test(0, 1, SecureDatagramDirection::AToB, expires_at + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -477,6 +477,12 @@ struct PublicIpv6Lab {
|
||||
extra_bridges: [&'static str; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PublicIpv6LabTopology {
|
||||
DelegatedPrefix,
|
||||
OnLinkPrefix,
|
||||
}
|
||||
|
||||
impl PublicIpv6Lab {
|
||||
const PROVIDER_NS: &'static str = "net_a";
|
||||
const CLIENT_NS: &'static str = "net_b";
|
||||
@@ -490,11 +496,13 @@ impl PublicIpv6Lab {
|
||||
const PROVIDER_DEFAULT_FROM: &'static str = "2001:db8:100::/64";
|
||||
const PROVIDER_WAN_ADDR: &'static str = "2001:db8:ffff:1::2/64";
|
||||
const UPSTREAM_WAN_ADDR: &'static str = "2001:db8:ffff:1::1/64";
|
||||
const ON_LINK_PROVIDER_WAN_ADDR: &'static str = "2001:db8:100::2/64";
|
||||
const ON_LINK_UPSTREAM_WAN_ADDR: &'static str = "2001:db8:100::1/64";
|
||||
const UPSTREAM_SERVER_ADDR: &'static str = "2001:db8:ffff:2::1/64";
|
||||
const SERVER_ADDR: &'static str = "2001:db8:ffff:2::100/64";
|
||||
const SERVER_IP: &'static str = "2001:db8:ffff:2::100";
|
||||
|
||||
fn setup() -> Self {
|
||||
fn setup_with_topology(topology: PublicIpv6LabTopology) -> Self {
|
||||
prepare_linux_namespaces();
|
||||
|
||||
del_netns(Self::UPSTREAM_NS);
|
||||
@@ -544,13 +552,23 @@ impl PublicIpv6Lab {
|
||||
Self::SERVER_BRIDGE,
|
||||
);
|
||||
|
||||
let (provider_wan_addr, upstream_wan_addr) = match topology {
|
||||
PublicIpv6LabTopology::DelegatedPrefix => {
|
||||
(Self::PROVIDER_WAN_ADDR, Self::UPSTREAM_WAN_ADDR)
|
||||
}
|
||||
PublicIpv6LabTopology::OnLinkPrefix => (
|
||||
Self::ON_LINK_PROVIDER_WAN_ADDR,
|
||||
Self::ON_LINK_UPSTREAM_WAN_ADDR,
|
||||
),
|
||||
};
|
||||
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&["addr", "add", Self::PROVIDER_WAN_ADDR, "dev", "pubwan0"],
|
||||
&["addr", "add", provider_wan_addr, "dev", "pubwan0"],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::UPSTREAM_NS,
|
||||
&["addr", "add", Self::UPSTREAM_WAN_ADDR, "dev", "upwan0"],
|
||||
&["addr", "add", upstream_wan_addr, "dev", "upwan0"],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::UPSTREAM_NS,
|
||||
@@ -561,37 +579,56 @@ impl PublicIpv6Lab {
|
||||
&["addr", "add", Self::SERVER_ADDR, "dev", "srv0"],
|
||||
);
|
||||
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&["link", "add", "pubprefix0", "type", "dummy"],
|
||||
);
|
||||
run_ip_in_ns(Self::PROVIDER_NS, &["link", "set", "pubprefix0", "up"]);
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
Self::PROVIDER_PREFIX,
|
||||
"dev",
|
||||
"pubprefix0",
|
||||
],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
"default",
|
||||
"from",
|
||||
Self::PROVIDER_DEFAULT_FROM,
|
||||
"via",
|
||||
"2001:db8:ffff:1::1",
|
||||
"dev",
|
||||
"pubwan0",
|
||||
],
|
||||
);
|
||||
match topology {
|
||||
PublicIpv6LabTopology::DelegatedPrefix => {
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&["link", "add", "pubprefix0", "type", "dummy"],
|
||||
);
|
||||
run_ip_in_ns(Self::PROVIDER_NS, &["link", "set", "pubprefix0", "up"]);
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
Self::PROVIDER_PREFIX,
|
||||
"dev",
|
||||
"pubprefix0",
|
||||
],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
"default",
|
||||
"from",
|
||||
Self::PROVIDER_DEFAULT_FROM,
|
||||
"via",
|
||||
"2001:db8:ffff:1::1",
|
||||
"dev",
|
||||
"pubwan0",
|
||||
],
|
||||
);
|
||||
}
|
||||
PublicIpv6LabTopology::OnLinkPrefix => {
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
"default",
|
||||
"via",
|
||||
"2001:db8:100::1",
|
||||
"dev",
|
||||
"pubwan0",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
run_ip_in_ns(
|
||||
Self::SERVER_NS,
|
||||
@@ -606,19 +643,21 @@ impl PublicIpv6Lab {
|
||||
"srv0",
|
||||
],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::UPSTREAM_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
Self::PROVIDER_PREFIX,
|
||||
"via",
|
||||
"2001:db8:ffff:1::2",
|
||||
"dev",
|
||||
"upwan0",
|
||||
],
|
||||
);
|
||||
if matches!(topology, PublicIpv6LabTopology::DelegatedPrefix) {
|
||||
run_ip_in_ns(
|
||||
Self::UPSTREAM_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
Self::PROVIDER_PREFIX,
|
||||
"via",
|
||||
"2001:db8:ffff:1::2",
|
||||
"dev",
|
||||
"upwan0",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
run_sysctl_in_ns(Self::PROVIDER_NS, "net.ipv6.conf.all.forwarding=1");
|
||||
run_sysctl_in_ns(Self::UPSTREAM_NS, "net.ipv6.conf.all.forwarding=1");
|
||||
@@ -672,7 +711,15 @@ fn get_public_ipv6_config(
|
||||
async fn init_public_ipv6_two_node(
|
||||
client_inst_id: uuid::Uuid,
|
||||
) -> (PublicIpv6Lab, Instance, Instance) {
|
||||
let lab = PublicIpv6Lab::setup();
|
||||
init_public_ipv6_two_node_with_topology(client_inst_id, PublicIpv6LabTopology::DelegatedPrefix)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn init_public_ipv6_two_node_with_topology(
|
||||
client_inst_id: uuid::Uuid,
|
||||
topology: PublicIpv6LabTopology,
|
||||
) -> (PublicIpv6Lab, Instance, Instance) {
|
||||
let lab = PublicIpv6Lab::setup_with_topology(topology);
|
||||
|
||||
let provider_cfg = get_public_ipv6_config(
|
||||
"provider_public_ipv6",
|
||||
@@ -756,6 +803,13 @@ fn addr_exists_in_ns(ns: &str, dev: &str, needle: &str) -> bool {
|
||||
run_ip_in_ns_output(ns, &["-6", "addr", "show", "dev", dev]).contains(needle)
|
||||
}
|
||||
|
||||
fn ndp_proxy_exists_in_ns(ns: &str, dev: &str, addr: std::net::Ipv6Addr) -> bool {
|
||||
let addr = addr.to_string();
|
||||
run_ip_in_ns_output(ns, &["-6", "neigh", "show", "proxy", "dev", dev])
|
||||
.lines()
|
||||
.any(|line| line.split_whitespace().next() == Some(addr.as_str()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn public_ipv6_auto_addr_end_to_end() {
|
||||
@@ -878,6 +932,67 @@ pub async fn public_ipv6_auto_addr_end_to_end() {
|
||||
drop_insts(vec![provider, client]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn public_ipv6_auto_addr_on_link_ndp_proxy_end_to_end() {
|
||||
let client_id = uuid::Uuid::parse_str("44444444-4444-4444-4444-444444444444").unwrap();
|
||||
let (_lab, provider, client) =
|
||||
init_public_ipv6_two_node_with_topology(client_id, PublicIpv6LabTopology::OnLinkPrefix)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
provider
|
||||
.get_global_ctx()
|
||||
.get_advertised_ipv6_public_addr_prefix()
|
||||
== Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
let leased = wait_for_public_ipv6_addr(&client).await;
|
||||
wait_for_public_ipv6_route(&provider, leased).await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
addr_exists_in_ns(
|
||||
PublicIpv6Lab::CLIENT_NS,
|
||||
PublicIpv6Lab::CLIENT_TUN,
|
||||
&leased.to_string(),
|
||||
) && route_exists_in_ns(
|
||||
PublicIpv6Lab::PROVIDER_NS,
|
||||
&format!("{} dev {}", leased.address(), PublicIpv6Lab::PROVIDER_TUN),
|
||||
)
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
ndp_proxy_exists_in_ns(PublicIpv6Lab::PROVIDER_NS, "pubwan0", leased.address())
|
||||
},
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
ping6_test(
|
||||
PublicIpv6Lab::SERVER_NS,
|
||||
leased.address().to_string().as_str(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await;
|
||||
|
||||
drop_insts(vec![provider, client]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn public_ipv6_auto_addr_reconnect_reuses_same_address() {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use bytes::Buf;
|
||||
use bytes::Bytes;
|
||||
use bytes::BytesMut;
|
||||
use zerocopy::byteorder::*;
|
||||
use zerocopy::AsBytes;
|
||||
use zerocopy::FromBytes;
|
||||
use zerocopy::FromZeroes;
|
||||
use zerocopy::byteorder::*;
|
||||
|
||||
type DefaultEndian = LittleEndian;
|
||||
|
||||
@@ -485,8 +486,16 @@ impl ZCPacket {
|
||||
let payload_off = ret.packet_type.get_packet_offsets().payload_offset;
|
||||
let total_len = payload_off + payload.len();
|
||||
ret.inner.reserve(total_len);
|
||||
unsafe { ret.inner.set_len(total_len) };
|
||||
ret.mut_payload()[..payload.len()].copy_from_slice(payload);
|
||||
|
||||
// SAFETY: `reserve` guarantees capacity >= total_len.
|
||||
// We zero the header region and copy payload before advancing length,
|
||||
// so every byte in [0..total_len) is initialized before any read.
|
||||
unsafe {
|
||||
let ptr = ret.inner.as_mut_ptr();
|
||||
std::ptr::write_bytes(ptr, 0, payload_off);
|
||||
std::ptr::copy_nonoverlapping(payload.as_ptr(), ptr.add(payload_off), payload.len());
|
||||
ret.inner.set_len(total_len);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -494,12 +503,12 @@ impl ZCPacket {
|
||||
let mut ret = Self::new_nic_packet();
|
||||
ret.inner.reserve(cap);
|
||||
let total_len = ret.packet_type.get_packet_offsets().payload_offset - packet_info_len;
|
||||
unsafe { ret.inner.set_len(total_len) };
|
||||
ret.inner.resize(total_len, 0);
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn new_for_foreign_network(
|
||||
network_name: &String,
|
||||
network_name: &str,
|
||||
dst_peer_id: u32,
|
||||
foreign_zc_packet: &ZCPacket,
|
||||
) -> Self {
|
||||
@@ -508,26 +517,71 @@ impl ZCPacket {
|
||||
foreign_network_hdr.get_header_len() + foreign_zc_packet.tunnel_payload().len();
|
||||
|
||||
let mut ret = Self::new_nic_packet();
|
||||
let payload_off = ret.packet_type.get_packet_offsets().payload_offset;
|
||||
ret.inner.reserve(payload_off + total_payload_len);
|
||||
unsafe { ret.inner.set_len(payload_off + total_payload_len) };
|
||||
let offsets = ret.packet_type.get_packet_offsets();
|
||||
let payload_off = offsets.payload_offset;
|
||||
let pm_hdr_off = offsets.peer_manager_header_offset;
|
||||
let total_len = payload_off + total_payload_len;
|
||||
ret.inner.reserve(total_len);
|
||||
|
||||
let fixed_hdr_len = std::mem::size_of::<ForeignNetworkPacketHeader>();
|
||||
ret.mut_payload()[..fixed_hdr_len].copy_from_slice(foreign_network_hdr.as_bytes());
|
||||
|
||||
let name_offset = foreign_network_hdr.network_name_offset.get() as usize;
|
||||
let name_len = foreign_network_hdr.network_name_len.get() as usize;
|
||||
ret.mut_payload()[name_offset..name_offset + name_len]
|
||||
.copy_from_slice(network_name.as_bytes());
|
||||
let foreign_payload = foreign_zc_packet.tunnel_payload();
|
||||
|
||||
ret.mut_payload()[foreign_network_hdr.get_header_len()..]
|
||||
.copy_from_slice(foreign_zc_packet.tunnel_payload());
|
||||
// Construct the PeerManagerHeader on the stack so we can write it
|
||||
// directly into the buffer, avoiding a separate mut_peer_manager_header()
|
||||
// call after set_len.
|
||||
let pm_hdr = PeerManagerHeader {
|
||||
from_peer_id: 0.into(),
|
||||
to_peer_id: 0.into(),
|
||||
packet_type: PacketType::ForeignNetworkPacket as u8,
|
||||
flags: 0,
|
||||
forward_counter: 0,
|
||||
reserved: 0,
|
||||
len: U32::new(total_payload_len as u32),
|
||||
};
|
||||
|
||||
let hdr = ret.mut_peer_manager_header().unwrap();
|
||||
hdr.from_peer_id = 0.into();
|
||||
hdr.to_peer_id = 0.into();
|
||||
hdr.packet_type = PacketType::ForeignNetworkPacket as u8;
|
||||
hdr.len.set(total_payload_len as u32);
|
||||
// SAFETY: `reserve` guarantees capacity >= total_len.
|
||||
// We zero only the tunnel-header reserved space [0..pm_hdr_off], write
|
||||
// the PeerManagerHeader directly at pm_hdr_off, then copy the foreign
|
||||
// network header, network name, and payload. Every byte in [0..total_len)
|
||||
// is initialized before set_len.
|
||||
unsafe {
|
||||
let ptr = ret.inner.as_mut_ptr();
|
||||
|
||||
// Zero the tunnel header reserved space only (not the PM header region)
|
||||
std::ptr::write_bytes(ptr, 0, pm_hdr_off);
|
||||
|
||||
// Write PeerManagerHeader directly
|
||||
std::ptr::copy_nonoverlapping(
|
||||
pm_hdr.as_bytes().as_ptr(),
|
||||
ptr.add(pm_hdr_off),
|
||||
std::mem::size_of::<PeerManagerHeader>(),
|
||||
);
|
||||
|
||||
// Copy foreign network fixed header
|
||||
std::ptr::copy_nonoverlapping(
|
||||
foreign_network_hdr.as_bytes().as_ptr(),
|
||||
ptr.add(payload_off),
|
||||
fixed_hdr_len,
|
||||
);
|
||||
|
||||
// Copy network name
|
||||
std::ptr::copy_nonoverlapping(
|
||||
network_name.as_ptr(),
|
||||
ptr.add(payload_off + name_offset),
|
||||
name_len,
|
||||
);
|
||||
|
||||
// Copy foreign payload
|
||||
std::ptr::copy_nonoverlapping(
|
||||
foreign_payload.as_ptr(),
|
||||
ptr.add(payload_off + foreign_network_hdr.get_header_len()),
|
||||
foreign_payload.len(),
|
||||
);
|
||||
|
||||
ret.inner.set_len(total_len);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
@@ -587,7 +641,8 @@ impl ZCPacket {
|
||||
}
|
||||
|
||||
pub fn payload_bytes(mut self) -> BytesMut {
|
||||
self.inner.split_off(self.payload_offset())
|
||||
self.inner.advance(self.payload_offset());
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn peer_manager_header(&self) -> Option<&PeerManagerHeader> {
|
||||
@@ -652,11 +707,12 @@ impl ZCPacket {
|
||||
}
|
||||
|
||||
pub fn tunnel_payload_bytes(mut self) -> BytesMut {
|
||||
self.inner.split_off(
|
||||
self.inner.advance(
|
||||
self.packet_type
|
||||
.get_packet_offsets()
|
||||
.peer_manager_header_offset,
|
||||
)
|
||||
);
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn convert_type(mut self, target_packet_type: ZCPacketType) -> Self {
|
||||
@@ -697,12 +753,13 @@ impl ZCPacket {
|
||||
.get_packet_offsets()
|
||||
.peer_manager_header_offset;
|
||||
let mut buf = BytesMut::with_capacity(new_pm_offset + tunnel_payload.len());
|
||||
unsafe { buf.set_len(new_pm_offset) };
|
||||
buf.resize(new_pm_offset, 0);
|
||||
buf.extend_from_slice(tunnel_payload);
|
||||
return Self::new_from_buf(buf, target_packet_type);
|
||||
}
|
||||
|
||||
Self::new_from_buf(self.inner.split_off(new_offset), target_packet_type)
|
||||
self.inner.advance(new_offset);
|
||||
Self::new_from_buf(self.inner, target_packet_type)
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Bytes {
|
||||
@@ -748,8 +805,10 @@ impl ZCPacket {
|
||||
let foreign_hdr_len = hdr.get_header_len();
|
||||
|
||||
Self::new_from_buf(
|
||||
self.inner
|
||||
.split_off(foreign_hdr_len + self.payload_offset()),
|
||||
{
|
||||
self.inner.advance(foreign_hdr_len + self.payload_offset());
|
||||
self.inner
|
||||
},
|
||||
ZCPacketType::DummyTunnel,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
cell::UnsafeCell,
|
||||
sync::atomic::{AtomicU32, Ordering::Relaxed},
|
||||
};
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering::Relaxed};
|
||||
|
||||
pub struct WindowLatency {
|
||||
latency_us_window: Vec<AtomicU32>,
|
||||
@@ -63,34 +60,30 @@ impl WindowLatency {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Throughput {
|
||||
tx_bytes: UnsafeCell<u64>,
|
||||
rx_bytes: UnsafeCell<u64>,
|
||||
tx_packets: UnsafeCell<u64>,
|
||||
rx_packets: UnsafeCell<u64>,
|
||||
tx_bytes: AtomicU64,
|
||||
rx_bytes: AtomicU64,
|
||||
tx_packets: AtomicU64,
|
||||
rx_packets: AtomicU64,
|
||||
}
|
||||
|
||||
impl Clone for Throughput {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
tx_bytes: UnsafeCell::new(unsafe { *self.tx_bytes.get() }),
|
||||
rx_bytes: UnsafeCell::new(unsafe { *self.rx_bytes.get() }),
|
||||
tx_packets: UnsafeCell::new(unsafe { *self.tx_packets.get() }),
|
||||
rx_packets: UnsafeCell::new(unsafe { *self.rx_packets.get() }),
|
||||
tx_bytes: AtomicU64::new(self.tx_bytes.load(Relaxed)),
|
||||
rx_bytes: AtomicU64::new(self.rx_bytes.load(Relaxed)),
|
||||
tx_packets: AtomicU64::new(self.tx_packets.load(Relaxed)),
|
||||
rx_packets: AtomicU64::new(self.rx_packets.load(Relaxed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add sync::Send and sync::Sync traits to Throughput
|
||||
unsafe impl Send for Throughput {}
|
||||
unsafe impl Sync for Throughput {}
|
||||
|
||||
impl Default for Throughput {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tx_bytes: UnsafeCell::new(0),
|
||||
rx_bytes: UnsafeCell::new(0),
|
||||
tx_packets: UnsafeCell::new(0),
|
||||
rx_packets: UnsafeCell::new(0),
|
||||
tx_bytes: AtomicU64::new(0),
|
||||
rx_bytes: AtomicU64::new(0),
|
||||
tx_packets: AtomicU64::new(0),
|
||||
rx_packets: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,32 +94,28 @@ impl Throughput {
|
||||
}
|
||||
|
||||
pub fn tx_bytes(&self) -> u64 {
|
||||
unsafe { *self.tx_bytes.get() }
|
||||
self.tx_bytes.load(Relaxed)
|
||||
}
|
||||
|
||||
pub fn rx_bytes(&self) -> u64 {
|
||||
unsafe { *self.rx_bytes.get() }
|
||||
self.rx_bytes.load(Relaxed)
|
||||
}
|
||||
|
||||
pub fn tx_packets(&self) -> u64 {
|
||||
unsafe { *self.tx_packets.get() }
|
||||
self.tx_packets.load(Relaxed)
|
||||
}
|
||||
|
||||
pub fn rx_packets(&self) -> u64 {
|
||||
unsafe { *self.rx_packets.get() }
|
||||
self.rx_packets.load(Relaxed)
|
||||
}
|
||||
|
||||
pub fn record_tx_bytes(&self, bytes: u64) {
|
||||
unsafe {
|
||||
*self.tx_bytes.get() += bytes;
|
||||
*self.tx_packets.get() += 1;
|
||||
}
|
||||
self.tx_bytes.fetch_add(bytes, Relaxed);
|
||||
self.tx_packets.fetch_add(1, Relaxed);
|
||||
}
|
||||
|
||||
pub fn record_rx_bytes(&self, bytes: u64) {
|
||||
unsafe {
|
||||
*self.rx_bytes.get() += bytes;
|
||||
*self.rx_packets.get() += 1;
|
||||
}
|
||||
self.rx_bytes.fetch_add(bytes, Relaxed);
|
||||
self.rx_packets.fetch_add(1, Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user