mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-29 08:03:54 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30926aec55 | ||
|
|
7af7bdc16a | ||
|
|
983afda177 | ||
|
|
4837468d43 | ||
|
|
eb32d2a930 | ||
|
|
728ba94b50 |
Generated
+36
-112
@@ -75,25 +75,11 @@ dependencies = [
|
||||
"aead 0.5.2",
|
||||
"aes 0.8.4",
|
||||
"cipher 0.4.4",
|
||||
"ctr 0.9.2",
|
||||
"ghash 0.5.1",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f"
|
||||
dependencies = [
|
||||
"aead 0.6.1",
|
||||
"aes 0.9.3",
|
||||
"cipher 0.5.2",
|
||||
"ctr 0.10.1",
|
||||
"ctutils",
|
||||
"ghash 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
@@ -1957,15 +1943,6 @@ dependencies = [
|
||||
"cipher 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21"
|
||||
dependencies = [
|
||||
"cipher 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
@@ -2537,7 +2514,7 @@ dependencies = [
|
||||
"atomic-shim",
|
||||
"atomic-write-file",
|
||||
"atomic_refcell",
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"bon",
|
||||
"boringtun-easytier",
|
||||
"bytecodec",
|
||||
@@ -2570,7 +2547,7 @@ dependencies = [
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"hkdf 0.13.0",
|
||||
"hkdf",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"humansize",
|
||||
@@ -2609,7 +2586,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"service-manager",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.10.9",
|
||||
"shellexpand",
|
||||
"smoltcp",
|
||||
"socket2",
|
||||
@@ -2663,7 +2640,7 @@ dependencies = [
|
||||
name = "easytier-core"
|
||||
version = "2.7.0"
|
||||
dependencies = [
|
||||
"aes-gcm 0.11.1",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"ariadne",
|
||||
@@ -2671,22 +2648,22 @@ dependencies = [
|
||||
"async-trait",
|
||||
"atomic-shim",
|
||||
"auto_impl",
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.13.2",
|
||||
"bon",
|
||||
"bytecodec",
|
||||
"bytes",
|
||||
"chacha20poly1305 0.11.0",
|
||||
"chacha20poly1305 0.10.1",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"crossbeam",
|
||||
"crossbeam-utils",
|
||||
"dashmap",
|
||||
"easytier-proto",
|
||||
"futures",
|
||||
"getrandom 0.2.17",
|
||||
"getrandom 0.3.4",
|
||||
"guarden",
|
||||
"hmac 0.13.0",
|
||||
"hmac 0.12.1",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
@@ -2704,9 +2681,10 @@ dependencies = [
|
||||
"rand 0.8.8",
|
||||
"ring",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.10.9",
|
||||
"smoltcp",
|
||||
"snow",
|
||||
"strum 0.28.0",
|
||||
@@ -2811,7 +2789,7 @@ dependencies = [
|
||||
name = "easytier-ohos-features"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"easytier",
|
||||
"flate2",
|
||||
"gethostname",
|
||||
@@ -2858,11 +2836,11 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"auto_impl",
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"hmac 0.13.0",
|
||||
"hmac 0.12.1",
|
||||
"indoc",
|
||||
"pbjson",
|
||||
"pbjson-build",
|
||||
@@ -2872,10 +2850,10 @@ dependencies = [
|
||||
"prost-types 0.14.4",
|
||||
"prost-wkt-types",
|
||||
"quote",
|
||||
"reqwest 0.13.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"url",
|
||||
@@ -2924,7 +2902,7 @@ dependencies = [
|
||||
"axum-embed",
|
||||
"axum-login",
|
||||
"axum-messages",
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"clap",
|
||||
@@ -2937,18 +2915,19 @@ dependencies = [
|
||||
"imageproc",
|
||||
"maxminddb",
|
||||
"mimalloc",
|
||||
"oauth2-reqwest",
|
||||
"openidconnect",
|
||||
"password-auth",
|
||||
"prost 0.14.4",
|
||||
"rand 0.8.8",
|
||||
"reqwest 0.13.5",
|
||||
"reqwest",
|
||||
"rust-embed",
|
||||
"rust-i18n",
|
||||
"sea-orm",
|
||||
"sea-orm-migration",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.10.9",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"sys-locale",
|
||||
@@ -3025,7 +3004,7 @@ dependencies = [
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"hkdf 0.12.4",
|
||||
"hkdf",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
@@ -3778,16 +3757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5"
|
||||
dependencies = [
|
||||
"polyval 0.7.3",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4297,15 +4267,6 @@ dependencies = [
|
||||
"hmac 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018"
|
||||
dependencies = [
|
||||
"hmac 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@@ -6070,7 +6031,6 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"http",
|
||||
"rand 0.8.8",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
@@ -6079,6 +6039,16 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oauth2-reqwest"
|
||||
version = "0.1.0-alpha.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234fb5c965bbce983ee5de636a7a51d6a3223da8067ea02f9ab2d2d78ac08be2"
|
||||
dependencies = [
|
||||
"oauth2",
|
||||
"reqwest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
@@ -7001,17 +6971,6 @@ dependencies = [
|
||||
"universal-hash 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd"
|
||||
dependencies = [
|
||||
"cpubits",
|
||||
"cpufeatures 0.3.1",
|
||||
"universal-hash 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
@@ -7743,38 +7702,6 @@ version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http 0.6.11",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.5"
|
||||
@@ -8979,12 +8906,9 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82"
|
||||
dependencies = [
|
||||
"aes-gcm 0.10.3",
|
||||
"blake2 0.10.6",
|
||||
"chacha20poly1305 0.10.1",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"getrandom 0.3.4",
|
||||
"ring",
|
||||
"rustc_version",
|
||||
"sha2 0.10.9",
|
||||
"subtle",
|
||||
@@ -9182,7 +9106,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"generic-array",
|
||||
"hex",
|
||||
"hkdf 0.12.4",
|
||||
"hkdf",
|
||||
"hmac 0.12.1",
|
||||
"itoa",
|
||||
"log",
|
||||
@@ -9225,7 +9149,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hkdf 0.12.4",
|
||||
"hkdf",
|
||||
"hmac 0.12.1",
|
||||
"home",
|
||||
"itoa",
|
||||
@@ -9633,7 +9557,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest 0.13.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
|
||||
+9
-4
@@ -29,7 +29,8 @@ async-trait = "0.1.92"
|
||||
atomic-shim = "0.2.0"
|
||||
auto_impl = "1.3.0"
|
||||
axum = "0.8"
|
||||
base64 = "0.23"
|
||||
# Match pbjson 0.9's base64 dependency; upgrade together to avoid two codecs.
|
||||
base64 = "0.22.1"
|
||||
bon = "3.10.1"
|
||||
bytecodec = "0.5.0"
|
||||
bytes = "1.12.1"
|
||||
@@ -37,6 +38,7 @@ chrono = "0.4.45"
|
||||
cidr = "0.3.2"
|
||||
clap = "4.6.7"
|
||||
crossbeam = "0.8.5"
|
||||
crossbeam-utils = "0.8.23"
|
||||
dashmap = "6.2.1"
|
||||
easytier = { version = "2.7.0", path = "easytier", default-features = false }
|
||||
easytier-core = { version = "2.7.0", path = "easytier-core", default-features = false }
|
||||
@@ -44,10 +46,12 @@ easytier-ffi = { version = "0.1.0", path = "easytier-contrib/easytier-ffi", defa
|
||||
easytier-ohos-core = { version = "0.1.0", path = "easytier-contrib/easytier-ohrs/crates/easytier-ohos-core", default-features = false }
|
||||
easytier-ohos-features = { version = "0.1.0", path = "easytier-contrib/easytier-ohrs/crates/easytier-ohos-features", default-features = false }
|
||||
easytier-proto = { version = "2.7.0", path = "easytier-proto", default-features = false }
|
||||
futures = "0.3"
|
||||
futures = { version = "0.3", default-features = false }
|
||||
gethostname = "1.1"
|
||||
guarden = "0.3"
|
||||
hmac = "0.13.0"
|
||||
# Keep HMAC/SHA2 on digest 0.10, shared by stun_codec 0.4 and snow 0.10.
|
||||
# Upgrade this crypto family together when those upstream constraints move.
|
||||
hmac = "0.12.1"
|
||||
http-body-util = "0.1"
|
||||
hyper = { version = "1", default-features = false }
|
||||
hyper-util = { version = "0.1", default-features = false }
|
||||
@@ -71,7 +75,8 @@ sea-orm = "1.1.20"
|
||||
sea-orm-migration = "1.1.20"
|
||||
serde = "1.0.229"
|
||||
serde_json = "1.0"
|
||||
sha2 = "0.11.0"
|
||||
# See the HMAC constraint above; SHA2 must use the same digest generation.
|
||||
sha2 = "0.10.9"
|
||||
smoltcp = { version = "0.14.0", default-features = false }
|
||||
sqlx = "0.8.6"
|
||||
strum = "0.28.0"
|
||||
|
||||
@@ -71,6 +71,8 @@ cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
|
||||
[Install pre-built binary](https://github.com/EasyTier/EasyTier/releases) (Recommended, All platforms supported)
|
||||
|
||||
For a smaller executable, try [easytier-mini](https://github.com/EasyTier/easytier-mini), which supports basic TCP/UDP networking. See its documentation for supported features and usage.
|
||||
|
||||
[Install via Docker](https://easytier.cn/en/guide/installation.html#installation-methods)
|
||||
|
||||
[Install OpenWrt ipk package](https://github.com/EasyTier/luci-app-easytier)
|
||||
|
||||
@@ -71,6 +71,8 @@ cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
|
||||
[下载预编译文件](https://github.com/EasyTier/EasyTier/releases)(推荐,支持所有平台)
|
||||
|
||||
需要更小的可执行文件,可使用 [easytier-mini](https://github.com/EasyTier/easytier-mini):支持基础 TCP/UDP 组网,功能范围与使用方式见其文档。
|
||||
|
||||
[通过 Docker 安装](https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F)
|
||||
|
||||
[安装 OpenWrt ipk 软件包](https://github.com/EasyTier/luci-app-easytier)
|
||||
|
||||
@@ -6,6 +6,11 @@ dynamic IPv4 allocation, the smoltcp userspace path and STUN/UDP hole-punching
|
||||
core with the full binary. It includes AES-GCM so its default encryption
|
||||
setting interoperates with the full binary's default configuration.
|
||||
|
||||
Download [pre-built binaries](https://github.com/EasyTier/easytier-mini/releases)
|
||||
from the separate [easytier-mini repository](https://github.com/EasyTier/easytier-mini),
|
||||
which handles builds and releases. The source code is maintained here in the
|
||||
main EasyTier repository.
|
||||
|
||||
Build it with:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -20,7 +20,7 @@ easytier-proto = { workspace = true, features = [
|
||||
"core",
|
||||
"json-rpc",
|
||||
] }
|
||||
futures.workspace = true
|
||||
futures = { workspace = true, features = ["default"] }
|
||||
napi-derive-ohos = "1.1"
|
||||
napi-ohos = { version = "1.1", default-features = false, features = [
|
||||
"serde-json",
|
||||
|
||||
+25
-10
@@ -30,11 +30,11 @@ bytecodec.workspace = true
|
||||
bytes.workspace = true
|
||||
chrono = { workspace = true, features = ["clock"] }
|
||||
cidr = { workspace = true, features = ["serde"] }
|
||||
crossbeam.workspace = true
|
||||
crossbeam-utils.workspace = true
|
||||
dashmap.workspace = true
|
||||
bon.workspace = true
|
||||
easytier-proto = { workspace = true, features = ["core"] }
|
||||
futures.workspace = true
|
||||
futures = { workspace = true, features = ["std", "async-await"] }
|
||||
guarden.workspace = true
|
||||
hmac.workspace = true
|
||||
http-body-util = { workspace = true, optional = true }
|
||||
@@ -58,6 +58,14 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
smoltcp = { workspace = true, optional = true }
|
||||
# All core Noise handshakes use 25519_ChaChaPoly_SHA256. Snow's std feature
|
||||
# also enables unused ring and BLAKE2 dependencies, so use its alloc support.
|
||||
snow = { version = "0.10.0", default-features = false, features = [
|
||||
"use-chacha20poly1305",
|
||||
"use-sha2",
|
||||
"use-curve25519",
|
||||
"use-getrandom",
|
||||
] }
|
||||
stun_codec.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -74,25 +82,25 @@ tokio-util = { workspace = true, features = ["io", "rt"] }
|
||||
tokio-rustls = { workspace = true, optional = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
wildmatch = "2.6.1"
|
||||
uuid = { workspace = true, features = ["v4", "fast-rng", "serde"] }
|
||||
uuid = { workspace = true, features = ["v4", "serde"] }
|
||||
webpki-roots = { version = "1.0", optional = true }
|
||||
x25519-dalek = { workspace = true, features = ["static_secrets"] }
|
||||
zerocopy = { workspace = true, features = ["derive", "simd"] }
|
||||
zstd = { version = "0.14", optional = true }
|
||||
aes-gcm = { version = "0.11.1", optional = true }
|
||||
chacha20poly1305 = { version = "0.11.0", optional = true }
|
||||
# Match snow 0.10's AEAD generation to share aead, cipher and crypto-common.
|
||||
# Upgrade together with snow rather than pulling in a second crypto stack.
|
||||
aes-gcm = { version = "0.10.3", optional = true }
|
||||
chacha20poly1305 = { version = "0.10.1", optional = true }
|
||||
openssl = { version = "0.10", optional = true, features = ["vendored"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
getrandom-02 = { package = "getrandom", version = "0.2.17", features = ["js"] }
|
||||
getrandom-03 = { package = "getrandom", version = "0.3.4", features = ["wasm_js"] }
|
||||
snow = { version = "0.10.0", default-features = false, features = ["default-resolver", "default-resolver-crypto"] }
|
||||
ring = { version = "0.17", features = ["wasm32_unknown_unknown_js"], optional = true }
|
||||
rustls-pki-types = { version = "1.15.1", features = ["web"], optional = true }
|
||||
uuid = { workspace = true, features = ["js"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
|
||||
snow = "0.10.0"
|
||||
|
||||
[features]
|
||||
default = ["aes-gcm", "endpoint-discovery", "extended-services", "management", "tcp-hole-punch"]
|
||||
aes-gcm = ["dep:aes-gcm"]
|
||||
@@ -106,7 +114,9 @@ endpoint-discovery = [
|
||||
"dep:http-body-util",
|
||||
"dep:hyper",
|
||||
"dep:hyper-util",
|
||||
"dep:ring",
|
||||
"dep:rustls",
|
||||
"dep:rustls-pki-types",
|
||||
"dep:tokio-rustls",
|
||||
"dep:webpki-roots",
|
||||
]
|
||||
@@ -152,12 +162,17 @@ test-utils = []
|
||||
tracing-log = ["tracing/log"]
|
||||
zstd = ["dep:zstd"]
|
||||
|
||||
[dev-dependencies]
|
||||
futures = { workspace = true, features = ["executor"] }
|
||||
|
||||
[target.'cfg(not(target_os = "wasi"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "test-util"] }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = [
|
||||
# Enable browser entropy backends for transitive rand/snow dependencies.
|
||||
# Enable browser entropy backends for transitive rand/snow/AEAD dependencies.
|
||||
"getrandom-02",
|
||||
"getrandom-03",
|
||||
# Enable browser time for rustls certificate validation.
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use quanta::Instant;
|
||||
use rand::{Rng, seq::SliceRandom as _};
|
||||
use tokio::{
|
||||
|
||||
@@ -187,9 +187,16 @@ where
|
||||
let root_store = rustls::RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||
};
|
||||
let tls_config = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
// A host application can enable another rustls backend through Cargo
|
||||
// feature unification. Select ring explicitly instead of requiring a
|
||||
// process-wide provider to have been initialized first (#2607).
|
||||
let tls_config = rustls::ClientConfig::builder_with_provider(Arc::new(
|
||||
rustls::crypto::ring::default_provider(),
|
||||
))
|
||||
.with_safe_default_protocol_versions()
|
||||
.context("selecting HTTPS protocol versions failed")?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let stream = TlsConnector::from(Arc::new(tls_config))
|
||||
.connect(server_name, socket)
|
||||
.await
|
||||
@@ -631,6 +638,44 @@ mod tests {
|
||||
assert_eq!(options.bind.context.socket_mark, Some(9));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn https_fetch_reaches_tls_handshake_without_global_provider() {
|
||||
// Also run with rustls/aws_lc_rs enabled: feature unification must not
|
||||
// make HTTPS discovery panic before it can send a ClientHello (#2607).
|
||||
let (client, mut server) = tokio::io::duplex(8192);
|
||||
let host = Arc::new(HttpTestHost {
|
||||
stream: Mutex::new(Some(client)),
|
||||
connects: Mutex::new(Vec::new()),
|
||||
});
|
||||
let dns = HttpTestDns {
|
||||
queries: Mutex::new(Vec::new()),
|
||||
};
|
||||
let server_task = tokio::spawn(async move {
|
||||
let mut record_header = [0; 5];
|
||||
server.read_exact(&mut record_header).await.unwrap();
|
||||
assert_eq!(record_header[0], 22, "expected a TLS handshake record");
|
||||
// Close without replying; discovery should report a handshake
|
||||
// error rather than panic while selecting a crypto provider.
|
||||
});
|
||||
|
||||
let error = fetch_http_discovery(
|
||||
host,
|
||||
&dns,
|
||||
HttpDiscoveryRequest {
|
||||
url: "https://discovery.example/lookup".parse().unwrap(),
|
||||
user_agent: "easytier/test".to_owned(),
|
||||
network_name: "test-network".to_owned(),
|
||||
timeout: Duration::from_secs(5),
|
||||
ip_version: IpVersion::V4,
|
||||
tcp_bind: TcpBindOptions::default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect_err("server closed during the TLS handshake");
|
||||
assert!(format!("{error:#}").contains("HTTPS handshake failed"));
|
||||
server_task.await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_discovery_interprets_redirect_and_body_forms() {
|
||||
let query = resolve_http_endpoint(HttpDiscoveryResponse {
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
use quanta::Instant;
|
||||
use tokio::{
|
||||
@@ -892,15 +892,8 @@ mod tests {
|
||||
response_tasks.insert(replacement.clone(), pending_response_task(replacement_slot));
|
||||
drop(admission_guard);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), async {
|
||||
while clients.contains_key(&replacement) {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!response_tasks.contains_key(&replacement));
|
||||
|
||||
// Release the test's permit reference and wait for the full eviction;
|
||||
// the client and response-task entries are removed separately.
|
||||
drop(replacement_client);
|
||||
let second_slot = tokio::time::timeout(Duration::from_secs(1), second)
|
||||
.await
|
||||
@@ -908,6 +901,9 @@ mod tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(!clients.contains_key(&replacement));
|
||||
assert!(!response_tasks.contains_key(&replacement));
|
||||
|
||||
drop(second_slot);
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(slots.available_permits(), 1);
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
};
|
||||
|
||||
use cidr::Ipv4Inet;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use dashmap::{DashMap, mapref::entry::Entry};
|
||||
use smoltcp::wire::{IpAddress, IpProtocol, Ipv4Packet, TcpPacket};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwapOption;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use arc_swap::ArcSwapOption;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use futures::{StreamExt, TryFutureExt};
|
||||
use std::{
|
||||
any::Any,
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::{
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
|
||||
use crate::peers::util::shrink_dashmap;
|
||||
|
||||
@@ -16,7 +16,7 @@ use easytier_proto::{
|
||||
common::{FlagsInConfig, PeerFeatureFlag, SecureModeConfig, StunInfo, TunnelInfo},
|
||||
peer_rpc::{PeerGroupInfo, TrustedCredentialPubkeyProof},
|
||||
};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
pub use crate::config::{NetworkIdentity, NetworkSecretDigest};
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use futures::Future;
|
||||
use std::sync::RwLock;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
use arc_swap::ArcSwap;
|
||||
use atomic_shim::AtomicU64;
|
||||
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr, Ipv6Inet};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
use ordered_hash_map::OrderedHashMap;
|
||||
use parking_lot::{RwLock, lock_api::RwLockUpgradableReadGuard};
|
||||
|
||||
@@ -15,23 +15,24 @@ impl<T: Buf> BufList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn push(&mut self, buf: T) {
|
||||
debug_assert!(buf.has_remaining());
|
||||
self.bufs.push_back(buf);
|
||||
if buf.has_remaining() {
|
||||
self.bufs.push_back(buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
self.bufs.pop_front()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.bufs.len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bufs.is_empty()
|
||||
}
|
||||
@@ -39,17 +40,15 @@ impl<T: Buf> BufList<T> {
|
||||
|
||||
impl<T: Buf> Extend<T> for BufList<T> {
|
||||
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
|
||||
self.bufs.extend(
|
||||
iter.into_iter()
|
||||
.inspect(|buf| debug_assert!(buf.has_remaining())),
|
||||
);
|
||||
self.bufs
|
||||
.extend(iter.into_iter().filter(Buf::has_remaining));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Buf> Buf for BufList<T> {
|
||||
#[inline]
|
||||
fn remaining(&self) -> usize {
|
||||
self.bufs.iter().map(|buf| buf.remaining()).sum()
|
||||
self.bufs.iter().map(Buf::remaining).sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -59,33 +58,30 @@ impl<T: Buf> Buf for BufList<T> {
|
||||
|
||||
#[inline]
|
||||
fn chunks_vectored<'t>(&'t self, dst: &mut [IoSlice<'t>]) -> usize {
|
||||
if dst.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let mut vecs = 0;
|
||||
|
||||
for buf in &self.bufs {
|
||||
vecs += buf.chunks_vectored(&mut dst[vecs..]);
|
||||
if vecs == dst.len() {
|
||||
let n = buf.chunks_vectored(&mut dst[vecs..]);
|
||||
vecs += n;
|
||||
|
||||
if dst[vecs - n..vecs].iter().map(|s| s.len()).sum::<usize>() < buf.remaining() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
vecs
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn advance(&mut self, mut cnt: usize) {
|
||||
while cnt > 0 {
|
||||
{
|
||||
let front = &mut self.bufs[0];
|
||||
let rem = front.remaining();
|
||||
if rem > cnt {
|
||||
front.advance(cnt);
|
||||
return;
|
||||
} else {
|
||||
front.advance(rem);
|
||||
cnt -= rem;
|
||||
}
|
||||
let front = &mut self.bufs[0];
|
||||
let rem = front.remaining();
|
||||
front.advance(cnt.min(rem));
|
||||
if rem > cnt {
|
||||
return;
|
||||
}
|
||||
cnt -= rem;
|
||||
self.bufs.pop_front();
|
||||
}
|
||||
}
|
||||
@@ -94,20 +90,21 @@ impl<T: Buf> Buf for BufList<T> {
|
||||
fn copy_to_bytes(&mut self, len: usize) -> Bytes {
|
||||
// Our inner buffer may have an optimized version of copy_to_bytes, and if the whole
|
||||
// request can be fulfilled by the front buffer, we can take advantage.
|
||||
match self.bufs.front_mut() {
|
||||
Some(front) if front.remaining() == len => {
|
||||
let b = front.copy_to_bytes(len);
|
||||
if let Some(front) = self.bufs.front_mut()
|
||||
&& let rem = front.remaining()
|
||||
&& len <= rem
|
||||
{
|
||||
let bytes = front.copy_to_bytes(len);
|
||||
if len == rem {
|
||||
self.bufs.pop_front();
|
||||
b
|
||||
}
|
||||
Some(front) if front.remaining() > len => front.copy_to_bytes(len),
|
||||
_ => {
|
||||
assert!(len <= self.remaining(), "`len` greater than remaining");
|
||||
let mut bm = BytesMut::with_capacity(len);
|
||||
bm.put(self.take(len));
|
||||
bm.freeze()
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
assert!(len <= self.remaining());
|
||||
let mut bytes = BytesMut::with_capacity(len);
|
||||
bytes.put(self.take(len));
|
||||
bytes.freeze()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,4 +122,35 @@ mod tests {
|
||||
assert_eq!(&bytes[..], b"hello world");
|
||||
assert_eq!(list.remaining(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunks_vectored_partial_inner_buf() {
|
||||
#[derive(Debug)]
|
||||
struct PartialBuf(&'static [u8]);
|
||||
|
||||
impl Buf for PartialBuf {
|
||||
fn remaining(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn chunk(&self) -> &[u8] {
|
||||
&self.0[..self.0.len().min(1)]
|
||||
}
|
||||
|
||||
fn advance(&mut self, cnt: usize) {
|
||||
self.0 = &self.0[cnt..];
|
||||
}
|
||||
}
|
||||
|
||||
let mut list = BufList::new();
|
||||
list.push(PartialBuf(b"ab"));
|
||||
list.push(PartialBuf(b"c"));
|
||||
|
||||
let mut dst = [IoSlice::new(&[]); 3];
|
||||
|
||||
let n = list.chunks_vectored(&mut dst);
|
||||
|
||||
assert_eq!(n, 1);
|
||||
assert_eq!(&*dst[0], b"a");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use aes_gcm::{AeadInOut, Aes128Gcm, Aes256Gcm, Key, KeyInit};
|
||||
use aes_gcm::{AeadInPlace, Aes128Gcm, Aes256Gcm, Key, KeyInit};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use zerocopy::{AsBytes, FromBytes};
|
||||
|
||||
@@ -54,16 +54,16 @@ impl Encryptor for AesGcmCipher {
|
||||
let tag = aes_tail.tag.into();
|
||||
|
||||
let rs = match &self.cipher {
|
||||
AesGcmEnum::AES128GCM(aes_gcm) => aes_gcm.decrypt_inout_detached(
|
||||
AesGcmEnum::AES128GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached(
|
||||
&nonce,
|
||||
&[],
|
||||
(&mut zc_packet.mut_payload()[..text_len]).into(),
|
||||
&mut zc_packet.mut_payload()[..text_len],
|
||||
&tag,
|
||||
),
|
||||
AesGcmEnum::AES256GCM(aes_gcm) => aes_gcm.decrypt_inout_detached(
|
||||
AesGcmEnum::AES256GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached(
|
||||
&nonce,
|
||||
&[],
|
||||
(&mut zc_packet.mut_payload()[..text_len]).into(),
|
||||
&mut zc_packet.mut_payload()[..text_len],
|
||||
&tag,
|
||||
),
|
||||
};
|
||||
@@ -113,7 +113,7 @@ impl Encryptor for AesGcmCipher {
|
||||
nonce.into()
|
||||
});
|
||||
(
|
||||
aes_gcm.encrypt_inout_detached(&nonce, &[], zc_packet.mut_payload().into()),
|
||||
aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()),
|
||||
nonce,
|
||||
)
|
||||
}
|
||||
@@ -124,7 +124,7 @@ impl Encryptor for AesGcmCipher {
|
||||
nonce.into()
|
||||
});
|
||||
(
|
||||
aes_gcm.encrypt_inout_detached(&nonce, &[], zc_packet.mut_payload().into()),
|
||||
aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()),
|
||||
nonce,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chacha20poly1305::{AeadInOut, ChaCha20Poly1305, Key, KeyInit};
|
||||
use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, Key, KeyInit};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use zerocopy::{AsBytes, FromBytes};
|
||||
|
||||
@@ -42,12 +42,7 @@ impl Encryptor for ChaCha20Cipher {
|
||||
let tag = tail.tag.into();
|
||||
|
||||
self.cipher
|
||||
.decrypt_inout_detached(
|
||||
&nonce,
|
||||
&[],
|
||||
(&mut zc_packet.mut_payload()[..text_len]).into(),
|
||||
&tag,
|
||||
)
|
||||
.decrypt_in_place_detached(&nonce, &[], &mut zc_packet.mut_payload()[..text_len], &tag)
|
||||
.map_err(|_| Error::DecryptionFailed)?;
|
||||
|
||||
let pm_header = zc_packet.mut_peer_manager_header().unwrap();
|
||||
@@ -89,7 +84,7 @@ impl Encryptor for ChaCha20Cipher {
|
||||
|
||||
let tag = self
|
||||
.cipher
|
||||
.encrypt_inout_detached(&nonce, &[], zc_packet.mut_payload().into())
|
||||
.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload())
|
||||
.map_err(|_| Error::EncryptionFailed)?;
|
||||
|
||||
let tail = StandardAeadTail {
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
|
||||
use anyhow::anyhow;
|
||||
use atomic_shim::AtomicU64;
|
||||
use hmac::{Hmac, KeyInit as _, Mac as _};
|
||||
use hmac::{Hmac, Mac as _};
|
||||
use rand::RngCore as _;
|
||||
use sha2::Sha256;
|
||||
use zerocopy::FromBytes;
|
||||
|
||||
@@ -1443,6 +1443,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
}
|
||||
|
||||
setup_panic_handler();
|
||||
easytier::utils::init_crypto_provider();
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"createUpdaterArtifacts": false
|
||||
"createUpdaterArtifacts": false,
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": [
|
||||
"pkexec | policykit-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.7.0",
|
||||
|
||||
@@ -540,4 +540,71 @@ mod tests {
|
||||
|
||||
assert_eq!(pair.get_loss_rate(), Some(0.0));
|
||||
}
|
||||
|
||||
#[cfg(feature = "json-rpc")]
|
||||
#[test]
|
||||
fn peer_route_pair_json_omits_zero_loss_rate_and_preserves_nonzero_loss_rate() {
|
||||
let pair = PeerRoutePair {
|
||||
peer: Some(PeerInfo {
|
||||
conns: vec![
|
||||
PeerConnInfo {
|
||||
conn_id: "zero-loss".to_owned(),
|
||||
loss_rate: 0.0,
|
||||
..Default::default()
|
||||
},
|
||||
PeerConnInfo {
|
||||
conn_id: "nonzero-loss".to_owned(),
|
||||
loss_rate: 0.25,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(pair).unwrap();
|
||||
let conns = json["peer"]["conns"].as_array().unwrap();
|
||||
assert!(!conns[0].as_object().unwrap().contains_key("loss_rate"));
|
||||
assert_eq!(conns[1]["loss_rate"], serde_json::json!(0.25));
|
||||
}
|
||||
|
||||
#[cfg(feature = "json-rpc")]
|
||||
#[test]
|
||||
fn peer_route_pair_json_stun_info_uses_enum_names_and_omits_unknown_nat() {
|
||||
use super::instance::Route;
|
||||
use crate::proto::common::{NatType, StunInfo};
|
||||
|
||||
let known = PeerRoutePair {
|
||||
route: Some(Route {
|
||||
stun_info: Some(StunInfo {
|
||||
udp_nat_type: NatType::FullCone as i32,
|
||||
tcp_nat_type: NatType::NoPat as i32,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let unknown = PeerRoutePair {
|
||||
route: Some(Route {
|
||||
stun_info: Some(StunInfo::default()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let known_json = serde_json::to_value(known).unwrap();
|
||||
assert_eq!(
|
||||
known_json["route"]["stun_info"]["udp_nat_type"],
|
||||
serde_json::json!("FullCone")
|
||||
);
|
||||
assert_eq!(
|
||||
known_json["route"]["stun_info"]["tcp_nat_type"],
|
||||
serde_json::json!("NoPAT")
|
||||
);
|
||||
|
||||
let unknown_json = serde_json::to_value(unknown).unwrap();
|
||||
assert_eq!(unknown_json["route"]["stun_info"], serde_json::json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use hmac::{Hmac, Mac};
|
||||
use prost::Message;
|
||||
use sha2::Sha256;
|
||||
#[cfg(feature = "api")]
|
||||
|
||||
@@ -16,7 +16,7 @@ tokio-util = { workspace = true, features = ["rt"] }
|
||||
dashmap.workspace = true
|
||||
url.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
futures = { workspace = true, features = ["default"] }
|
||||
prost.workspace = true
|
||||
|
||||
maxminddb = "0.32"
|
||||
@@ -74,8 +74,14 @@ uuid = { workspace = true, features = [
|
||||
] }
|
||||
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
openidconnect = { version = "4.0", default-features = false, features = ["accept-rfc3339-timestamps", "reqwest"] }
|
||||
reqwest = { workspace = true, features = ["json", "rustls"] }
|
||||
# Keep the bundled reqwest 0.12 client disabled; use the official 0.13 adapter.
|
||||
openidconnect = { version = "4.0", default-features = false, features = ["accept-rfc3339-timestamps"] }
|
||||
# Pin the adapter until its API stabilizes.
|
||||
oauth2-reqwest = "=0.1.0-alpha.3"
|
||||
# Reuse EasyTier's ring provider. reqwest 0.13's rustls feature adds aws-lc,
|
||||
# making rustls backend selection ambiguous when core/web are built together.
|
||||
# Initialize the process provider before constructing a reqwest client.
|
||||
reqwest = { workspace = true, features = ["json", "rustls-no-provider"] }
|
||||
subtle = "2.6"
|
||||
|
||||
mimalloc.workspace = true
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { RemoteClient } from '../modules/api'
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
||||
import { latencyMs, lossRate, numericValue, peerConns } from '../modules/statusDisplay';
|
||||
import { latencyMs, lossRate, numericValue, peerConns, udpNatTypeName } from '../modules/statusDisplay';
|
||||
import { Badge, DataTable, Column, Tag, Chip, Button, Dialog, Timeline, Divider, Card, } from 'primevue';
|
||||
import NetworkChart from './NetworkChart.vue';
|
||||
import VpnPortalDialog from './VpnPortalDialog.vue';
|
||||
@@ -144,34 +144,6 @@ interface Chip {
|
||||
icon: string
|
||||
}
|
||||
|
||||
// udp nat type
|
||||
enum NatType {
|
||||
// has NAT; but own a single public IP, port is not changed
|
||||
Unknown = 0,
|
||||
OpenInternet = 1,
|
||||
NoPAT = 2,
|
||||
FullCone = 3,
|
||||
Restricted = 4,
|
||||
PortRestricted = 5,
|
||||
Symmetric = 6,
|
||||
SymUdpFirewall = 7,
|
||||
SymmetricEasyInc = 8,
|
||||
SymmetricEasyDec = 9,
|
||||
};
|
||||
|
||||
const udpNatTypeStrMap = {
|
||||
[NatType.Unknown]: 'Unknown',
|
||||
[NatType.OpenInternet]: 'Open Internet',
|
||||
[NatType.NoPAT]: 'No PAT',
|
||||
[NatType.FullCone]: 'Full Cone',
|
||||
[NatType.Restricted]: 'Restricted',
|
||||
[NatType.PortRestricted]: 'Port Restricted',
|
||||
[NatType.Symmetric]: 'Symmetric',
|
||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||
}
|
||||
|
||||
const myNodeInfoChips = computed(() => {
|
||||
if (!props.curNetworkInst)
|
||||
return []
|
||||
@@ -246,10 +218,10 @@ const myNodeInfoChips = computed(() => {
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
||||
if (udpNatType !== undefined) {
|
||||
const udpNatType = udpNatTypeName(my_node_info.stun_info)
|
||||
if (udpNatType) {
|
||||
chips.push({
|
||||
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
|
||||
label: `UDP NAT Type: ${udpNatType}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -279,11 +251,7 @@ function rxGlobalSum() {
|
||||
}
|
||||
|
||||
function natType(info: PeerRoutePair): string {
|
||||
const udpNatType = info.route?.stun_info?.udp_nat_type;
|
||||
if (udpNatType !== undefined)
|
||||
return udpNatTypeStrMap[udpNatType as NatType]
|
||||
|
||||
return ''
|
||||
return udpNatTypeName(info.route?.stun_info)
|
||||
}
|
||||
|
||||
function isPublicServerRoute(info: PeerRoutePair): boolean {
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import type { PeerRoutePair } from '../types/network'
|
||||
import { NatType, type PeerRoutePair, type StunInfo } from '../types/network'
|
||||
|
||||
const udpNatTypeStrMap: Record<NatType, string> = {
|
||||
[NatType.Unknown]: 'Unknown',
|
||||
[NatType.OpenInternet]: 'Open Internet',
|
||||
[NatType.NoPAT]: 'No PAT',
|
||||
[NatType.FullCone]: 'Full Cone',
|
||||
[NatType.Restricted]: 'Restricted',
|
||||
[NatType.PortRestricted]: 'Port Restricted',
|
||||
[NatType.Symmetric]: 'Symmetric',
|
||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||
}
|
||||
|
||||
export function numericValue(value: unknown): number | undefined {
|
||||
if (typeof value === 'number')
|
||||
@@ -71,7 +84,7 @@ export function latencyMs(info: PeerRoutePair) {
|
||||
|
||||
export function lossRate(info: PeerRoutePair) {
|
||||
for (const conn of defaultConnFirst(info)) {
|
||||
const loss = numericValue(conn.loss_rate)
|
||||
const loss = numericValue(conn.loss_rate ?? 0)
|
||||
if (loss === undefined)
|
||||
continue
|
||||
|
||||
@@ -80,3 +93,12 @@ export function lossRate(info: PeerRoutePair) {
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function udpNatTypeName(stunInfo: StunInfo | undefined) {
|
||||
if (!stunInfo)
|
||||
return ''
|
||||
|
||||
const value = stunInfo.udp_nat_type ?? NatType.Unknown
|
||||
const natType = numericValue(value) ?? NatType[value as keyof typeof NatType]
|
||||
return udpNatTypeStrMap[natType as NatType] ?? udpNatTypeStrMap[NatType.Unknown]
|
||||
}
|
||||
|
||||
@@ -437,9 +437,9 @@ export interface NodeInfo {
|
||||
}
|
||||
|
||||
export interface StunInfo {
|
||||
udp_nat_type: number
|
||||
tcp_nat_type: number
|
||||
last_update_time: number
|
||||
udp_nat_type?: number | string
|
||||
tcp_nat_type?: number | string
|
||||
last_update_time?: number | string
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { latencyMs, lossRate } from '../src/modules/statusDisplay'
|
||||
import { latencyMs, lossRate, udpNatTypeName } from '../src/modules/statusDisplay'
|
||||
import { ipv4ToString, ipv6ToString } from '../src/modules/utils'
|
||||
|
||||
function peerRoutePair(conns: any[]) {
|
||||
@@ -49,7 +49,7 @@ describe('status display helpers', () => {
|
||||
expect(ipv6ToString({ part4: 1 } as any)).toBe('::1')
|
||||
})
|
||||
|
||||
it('skips missing latency and loss values', () => {
|
||||
it('skips missing latency and invalid loss values', () => {
|
||||
expect(latencyMs(peerRoutePair([
|
||||
{ conn_id: 'missing', stats: {} },
|
||||
{ conn_id: 'valid', stats: { latency_us: '2500' } },
|
||||
@@ -61,16 +61,25 @@ describe('status display helpers', () => {
|
||||
]))).toBe('')
|
||||
|
||||
expect(lossRate(peerRoutePair([
|
||||
{ conn_id: 'missing' },
|
||||
{ conn_id: 'valid', loss_rate: '0.25' },
|
||||
{ conn_id: 'invalid', loss_rate: 'unknown' },
|
||||
]))).toBe('25%')
|
||||
expect(lossRate(peerRoutePair([
|
||||
{ conn_id: 'missing' },
|
||||
{ conn_id: 'invalid', loss_rate: 'unknown' },
|
||||
]))).toBe('')
|
||||
})
|
||||
|
||||
it('renders omitted protobuf loss as zero only for an existing connection', () => {
|
||||
expect(lossRate(peerRoutePair([{ conn_id: 'zero' }]))).toBe('0%')
|
||||
expect(lossRate(peerRoutePair([{ conn_id: 'zero', loss_rate: 0 }]))).toBe('0%')
|
||||
expect(lossRate(peerRoutePair([
|
||||
{ conn_id: 'zero' },
|
||||
{ conn_id: 'lossy', loss_rate: 0.7 },
|
||||
]))).toBe('0%')
|
||||
expect(lossRate(peerRoutePair([]))).toBe('')
|
||||
expect(lossRate({ route: {} } as any)).toBe('')
|
||||
})
|
||||
|
||||
it('prefers the default connection when its metric is valid', () => {
|
||||
const defaultConnId = '00000001-0002-0003-0004-000000000005'
|
||||
const conns = [
|
||||
@@ -81,4 +90,36 @@ describe('status display helpers', () => {
|
||||
expect(latencyMs(peerRoutePairWithDefaultConn(conns, defaultConnId))).toBe('9ms')
|
||||
expect(lossRate(peerRoutePairWithDefaultConn(conns, defaultConnId))).toBe('50%')
|
||||
})
|
||||
|
||||
it('keeps an omitted zero loss on the default connection', () => {
|
||||
const defaultConnId = '00000001-0002-0003-0004-000000000005'
|
||||
expect(lossRate(peerRoutePairWithDefaultConn([
|
||||
{ conn_id: 'fallback', loss_rate: 0.25 },
|
||||
{ conn_id: defaultConnId },
|
||||
], defaultConnId))).toBe('0%')
|
||||
})
|
||||
|
||||
it.each([
|
||||
[0, 'Unknown', 'Unknown'],
|
||||
[1, 'OpenInternet', 'Open Internet'],
|
||||
[2, 'NoPAT', 'No PAT'],
|
||||
[3, 'FullCone', 'Full Cone'],
|
||||
[4, 'Restricted', 'Restricted'],
|
||||
[5, 'PortRestricted', 'Port Restricted'],
|
||||
[6, 'Symmetric', 'Symmetric'],
|
||||
[7, 'SymUdpFirewall', 'Symmetric UDP Firewall'],
|
||||
[8, 'SymmetricEasyInc', 'Symmetric Easy Inc'],
|
||||
[9, 'SymmetricEasyDec', 'Symmetric Easy Dec'],
|
||||
])('renders numeric and protobuf enum NAT type %s', (value, name, label) => {
|
||||
expect(udpNatTypeName({ udp_nat_type: value })).toBe(label)
|
||||
expect(udpNatTypeName({ udp_nat_type: name })).toBe(label)
|
||||
expect(udpNatTypeName({ udp_nat_type: String(value) })).toBe(label)
|
||||
})
|
||||
|
||||
it('distinguishes missing STUN data from default or unrecognized NAT types', () => {
|
||||
expect(udpNatTypeName(undefined)).toBe('')
|
||||
expect(udpNatTypeName({})).toBe('Unknown')
|
||||
expect(udpNatTypeName({ udp_nat_type: 100 })).toBe('Unknown')
|
||||
expect(udpNatTypeName({ udp_nat_type: 'FutureNatType' })).toBe('Unknown')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Status from '../src/components/Status.vue'
|
||||
import type { NetworkInstance } from '../src/types/network'
|
||||
|
||||
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
|
||||
|
||||
const wrappers: VueWrapper[] = []
|
||||
|
||||
afterEach(() => {
|
||||
wrappers.splice(0).forEach(wrapper => wrapper.unmount())
|
||||
})
|
||||
|
||||
async function render(localStun: unknown, peerStun: unknown, lossRate?: number) {
|
||||
const instance = {
|
||||
instance_id: '12345678-9abc-def0-fedc-ba9876543210',
|
||||
running: true,
|
||||
detail: {
|
||||
my_node_info: {
|
||||
hostname: 'local-node',
|
||||
version: 'test',
|
||||
peer_id: 1,
|
||||
stun_info: localStun,
|
||||
},
|
||||
peer_route_pairs: [{
|
||||
route: {
|
||||
hostname: 'remote-node',
|
||||
version: 'test',
|
||||
cost: 1,
|
||||
stun_info: peerStun,
|
||||
},
|
||||
peer: {
|
||||
conns: [{
|
||||
conn_id: 'connection',
|
||||
...(lossRate === undefined ? {} : { loss_rate: lossRate }),
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
} as unknown as NetworkInstance
|
||||
const wrapper = mount(Status, {
|
||||
props: {
|
||||
curNetworkInst: instance,
|
||||
api: { get_network_config: vi.fn(async () => ({})) } as any,
|
||||
},
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
directives: { tooltip: () => {} },
|
||||
stubs: { HumanEvent: true, NetworkChart: true, VpnPortalDialog: true },
|
||||
},
|
||||
})
|
||||
wrappers.push(wrapper)
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('status protobuf JSON rendering', () => {
|
||||
it('renders enum names and omitted zero loss in the real peer table', async () => {
|
||||
const wrapper = await render({ udp_nat_type: 'FullCone' }, { udp_nat_type: 'PortRestricted' })
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows[0].text()).toContain('Full Cone')
|
||||
expect(rows[0].text()).not.toContain('0%')
|
||||
expect(rows[1].text()).toContain('Port Restricted')
|
||||
expect(rows[1].text()).toContain('0%')
|
||||
await wrapper.findAll('button').find(button => button.text() === 'show_node_details')!.trigger('click')
|
||||
expect(wrapper.text()).toContain('UDP NAT Type: Full Cone')
|
||||
expect(wrapper.text()).not.toContain('UDP NAT Type: undefined')
|
||||
})
|
||||
|
||||
it('renders omitted enum defaults as Unknown but leaves missing STUN data blank', async () => {
|
||||
const wrapper = await render({}, undefined, 0.25)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows[0].text()).toContain('Unknown')
|
||||
expect(rows[1].text()).not.toContain('Unknown')
|
||||
expect(rows[1].text()).toContain('25%')
|
||||
expect(wrapper.text()).toContain('UDP NAT Type: Unknown')
|
||||
})
|
||||
|
||||
it('preserves numeric NAT values from older backends', async () => {
|
||||
const wrapper = await render({ udp_nat_type: 1 }, { udp_nat_type: 6 }, 0.5)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows[0].text()).toContain('Open Internet')
|
||||
expect(rows[1].text()).toContain('Symmetric')
|
||||
expect(rows[1].text()).toContain('50%')
|
||||
})
|
||||
})
|
||||
@@ -1499,7 +1499,7 @@ const switchTab = async (tab: string) => {
|
||||
{{ data.conn.stats ? latencyLabel(data.conn.stats.latency_us ? data.conn.stats.latency_us / 1000 : undefined) : '—' }}
|
||||
</template></Column>
|
||||
<Column :header="t('web.network_detail.loss_rate')"><template #body="{ data }">
|
||||
{{ data.conn.loss_rate != null ? (data.conn.loss_rate * 100).toFixed(1) + '%' : '—' }}
|
||||
{{ ((data.conn.loss_rate ?? 0) * 100).toFixed(1) + '%' }}
|
||||
</template></Column>
|
||||
<Column :header="t('web.network_detail.traffic')"><template #body="{ data }">
|
||||
<span class="font-mono text-xs">↓{{ ((data.conn.stats?.rx_bytes ?? 0) / 1024).toFixed(0) }}K ↑{{ ((data.conn.stats?.tx_bytes ?? 0) / 1024).toFixed(0) }}K</span>
|
||||
|
||||
@@ -562,14 +562,17 @@ test('node detail groups populated peers, routes, connections and ACL stats', as
|
||||
const { page } = await open(t, `/h/networks/${id(100)}`, { viewport: { width: 390, height: 844 }, colorScheme: 'dark' }, state => {
|
||||
state.networks[0].network_id = id(100);
|
||||
state.nodeRoutes = [{ peer_id: 2, hostname: 'Build server', ipv4_addr: { address: { addr: 175005442 }, network_length: 24 }, cost: 1, path_latency: 8, proxy_cidrs: [], version: '2.4.5', next_hop_peer_id: 2 }];
|
||||
state.nodePeers = [{ peer_id: 2, conns: [{ conn_id: 'conn-1', tunnel: { tunnel_type: 'tcp', remote_addr: { url: 'tcp://192.0.2.11:11010' } }, stats: { latency_us: 8000, rx_bytes: 1024, tx_bytes: 2048 }, loss_rate: 0 }] }];
|
||||
state.nodePeers = [{ peer_id: 2, conns: [
|
||||
{ conn_id: 'conn-1', tunnel: { tunnel_type: 'tcp', remote_addr: { url: 'tcp://192.0.2.11:11010' } }, stats: { latency_us: 8000, rx_bytes: 1024, tx_bytes: 2048 } },
|
||||
{ conn_id: 'conn-2', tunnel: { tunnel_type: 'udp', remote_addr: { url: 'udp://192.0.2.11:11010' } }, stats: { latency_us: 9000 }, loss_rate: 0.125 },
|
||||
] }];
|
||||
state.nodeAclStats = [{ rule: { name: 'Allow build server' }, stat: { packet_count: 42, byte_count: 4096 } }];
|
||||
});
|
||||
await page.locator('.mobile-list').getByRole('button', { name: 'Node Detail' }).click();
|
||||
const drawer = page.locator('.console-node-drawer');
|
||||
await drawer.locator('.node-drawer-mobile-peer').getByText('Build server').waitFor();
|
||||
await page.waitForFunction(() => Math.abs(document.querySelector('.console-node-drawer').getBoundingClientRect().right - innerWidth) < 1);
|
||||
assert.deepEqual(await drawer.locator('.node-drawer-metric strong').allTextContents().then(values => values.map(s => s.trim())), ['Running', '1', '1', '1']);
|
||||
assert.deepEqual(await drawer.locator('.node-drawer-metric strong').allTextContents().then(values => values.map(s => s.trim())), ['Running', '1', '1', '2']);
|
||||
assert.equal(await drawer.getByRole('tab').count(), 2);
|
||||
const drawerWidth = await drawer.evaluate(el => ({ drawer: el.getBoundingClientRect().width, viewport: innerWidth }));
|
||||
assert.ok(drawerWidth.drawer <= drawerWidth.viewport + 1, JSON.stringify(drawerWidth));
|
||||
@@ -580,6 +583,8 @@ test('node detail groups populated peers, routes, connections and ACL stats', as
|
||||
await drawer.locator('details').filter({ hasText: 'Routes' }).locator('summary').click();
|
||||
await drawer.getByText('10.110.95.2/24').first().waitFor();
|
||||
await drawer.locator('details').filter({ hasText: 'Connections' }).locator('summary').click();
|
||||
await drawer.getByRole('cell', { name: '0.0%', exact: true }).waitFor();
|
||||
await drawer.getByRole('cell', { name: '12.5%', exact: true }).waitFor();
|
||||
await drawer.locator('details').filter({ hasText: 'ACL Stats' }).locator('summary').click();
|
||||
await drawer.getByRole('cell', { name: 'Allow build server' }).waitFor();
|
||||
});
|
||||
|
||||
@@ -303,6 +303,7 @@ async fn main() {
|
||||
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
|
||||
rust_i18n::set_locale(&locale);
|
||||
setup_panic_handler();
|
||||
easytier::utils::init_crypto_provider();
|
||||
|
||||
let cli = Cli::parse();
|
||||
log::init_with_default_console_targets(&cli, false, &["CORE", "easytier_web"]).unwrap();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use openidconnect::reqwest;
|
||||
use oauth2_reqwest::ReqwestClient;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -180,7 +180,7 @@ pub struct OidcConfig {
|
||||
pub scopes: Vec<String>,
|
||||
pub pkce_enabled: bool,
|
||||
pub frontend_base_url: Option<String>,
|
||||
pub http_client: Option<reqwest::Client>,
|
||||
pub http_client: Option<ReqwestClient>,
|
||||
cached_client: Option<Arc<ConfiguredAppClient>>,
|
||||
}
|
||||
|
||||
@@ -224,10 +224,14 @@ impl OidcConfig {
|
||||
if oidc_username_claim.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty"));
|
||||
}
|
||||
let http_client = reqwest::ClientBuilder::new()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()?;
|
||||
// OIDC configuration can also be created outside the web entry point.
|
||||
easytier::utils::init_crypto_provider();
|
||||
let http_client = ReqwestClient::from(
|
||||
reqwest::ClientBuilder::new()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()?,
|
||||
);
|
||||
|
||||
let issuer_url = oidc_issuer_url.ok_or_else(|| {
|
||||
anyhow::anyhow!("--oidc-issuer-url is required when using OIDC authentication")
|
||||
@@ -697,6 +701,121 @@ mod route {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reqwest_adapter_supports_discovery_and_token_exchange() {
|
||||
use axum::{
|
||||
Form, Json,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::post,
|
||||
};
|
||||
use openidconnect::{AuthorizationCode, OAuth2TokenResponse, RequestTokenError};
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let issuer = format!("http://{}", listener.local_addr().unwrap());
|
||||
let metadata = json!({
|
||||
"issuer": issuer,
|
||||
"authorization_endpoint": format!("{issuer}/authorize"),
|
||||
"token_endpoint": format!("{issuer}/token"),
|
||||
"jwks_uri": format!("{issuer}/jwks"),
|
||||
"response_types_supported": ["code"],
|
||||
"subject_types_supported": ["public"],
|
||||
"id_token_signing_alg_values_supported": ["RS256"],
|
||||
});
|
||||
let redirect_hits = Arc::new(AtomicUsize::new(0));
|
||||
let hits = redirect_hits.clone();
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/.well-known/openid-configuration",
|
||||
get(move || async move { Json(metadata) }),
|
||||
)
|
||||
.route("/jwks", get(|| async { Json(json!({ "keys": [] })) }))
|
||||
.route(
|
||||
"/token",
|
||||
post(
|
||||
|headers: HeaderMap, Form(form): Form<HashMap<String, String>>| async move {
|
||||
assert_eq!(
|
||||
headers["authorization"],
|
||||
"Basic dGVzdC1jbGllbnQ6dGVzdC1zZWNyZXQ="
|
||||
);
|
||||
assert_eq!(form["grant_type"], "authorization_code");
|
||||
assert_eq!(form["redirect_uri"], "http://localhost/callback");
|
||||
match form["code"].as_str() {
|
||||
"valid" => Json(json!({
|
||||
"access_token": "test-access-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
}))
|
||||
.into_response(),
|
||||
"redirect" => Redirect::temporary("/unexpected").into_response(),
|
||||
_ => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": "invalid_grant" })),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/unexpected",
|
||||
post(move || async move {
|
||||
hits.fetch_add(1, Ordering::SeqCst);
|
||||
Json(json!({ "access_token": "unexpected", "token_type": "Bearer" }))
|
||||
}),
|
||||
);
|
||||
let _server = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}));
|
||||
|
||||
let config = OidcConfig::from_params(OidcOptions {
|
||||
oidc_issuer_url: Some(issuer),
|
||||
oidc_client_id: Some("test-client".to_owned()),
|
||||
oidc_client_secret: Some("test-secret".to_owned()),
|
||||
oidc_username_claim: "preferred_username".to_owned(),
|
||||
oidc_scopes: vec!["openid".to_owned()],
|
||||
oidc_redirect_url: Some("http://localhost/callback".to_owned()),
|
||||
oidc_disable_pkce: false,
|
||||
oidc_frontend_base_url: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let client = config.client().unwrap();
|
||||
let http_client = config.http_client.as_ref().unwrap();
|
||||
let token = client
|
||||
.exchange_code(AuthorizationCode::new("valid".to_owned()))
|
||||
.unwrap()
|
||||
.request_async(http_client)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(token.access_token().secret(), "test-access-token");
|
||||
assert_eq!(token.expires_in(), Some(Duration::from_secs(3600)));
|
||||
|
||||
let error = client
|
||||
.exchange_code(AuthorizationCode::new("invalid".to_owned()))
|
||||
.unwrap()
|
||||
.request_async(http_client)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(error, RequestTokenError::ServerResponse(ref response)
|
||||
if response.error() == &CoreErrorResponseType::InvalidGrant)
|
||||
);
|
||||
|
||||
assert!(
|
||||
client
|
||||
.exchange_code(AuthorizationCode::new("redirect".to_owned()))
|
||||
.unwrap()
|
||||
.request_async(http_client)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(redirect_hits.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dot_path_to_json_pointer() {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -281,6 +281,10 @@ impl WebhookConfig {
|
||||
web_instance_id: Option<String>,
|
||||
web_instance_api_base_url: Option<String>,
|
||||
) -> Self {
|
||||
// This constructor is also used without main (for example in tests).
|
||||
// reqwest's rustls-no-provider needs a process default even if ring
|
||||
// is the only compiled backend.
|
||||
easytier::utils::init_crypto_provider();
|
||||
WebhookConfig {
|
||||
webhook_url,
|
||||
webhook_secret,
|
||||
|
||||
+4
-2
@@ -61,7 +61,7 @@ strum = { workspace = true, features = ["derive"] }
|
||||
|
||||
gethostname.workspace = true
|
||||
|
||||
futures = { workspace = true, features = ["bilock", "unstable"] }
|
||||
futures = { workspace = true, features = ["default", "bilock", "unstable"] }
|
||||
|
||||
tokio = { workspace = true, features = [
|
||||
"fs",
|
||||
@@ -178,7 +178,8 @@ network-interface = "2.0.5"
|
||||
|
||||
# for wireguard
|
||||
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
|
||||
hkdf = { version = "0.13", optional = true }
|
||||
# Share the workspace HMAC/SHA2 digest generation for WireGuard key derivation.
|
||||
hkdf = { version = "0.12.4", optional = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
|
||||
# for cli
|
||||
@@ -415,6 +416,7 @@ upnp = [
|
||||
]
|
||||
endpoint-discovery = [
|
||||
"dns-resolver",
|
||||
"dep:rustls",
|
||||
"easytier-core/endpoint-discovery",
|
||||
]
|
||||
dhcp-ipv4 = ["easytier-core/dhcp-ipv4"]
|
||||
|
||||
@@ -13,6 +13,9 @@ core_clap:
|
||||
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. by default it is loaded from persisted local state; on first start it may be migrated from system information or generated, then remains fixed.
|
||||
zh-CN: |+
|
||||
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从本地持久化状态读取;首次启动时可能基于系统信息迁移或生成,之后保持固定不变。
|
||||
state_dir:
|
||||
en: "directory for persistent local state, currently the Web config server machine ID. Uses the platform default when unset; ignored when --machine-id is specified."
|
||||
zh-CN: "本地持久化状态目录,目前用于保存 Web 配置服务器的机器 ID。未指定时使用平台默认目录;指定 --machine-id 时不使用此目录。"
|
||||
config_file:
|
||||
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
|
||||
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
|
||||
|
||||
@@ -100,6 +100,13 @@ struct Cli {
|
||||
)]
|
||||
machine_id: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_STATE_DIR",
|
||||
help = t!("core_clap.state_dir").to_string()
|
||||
)]
|
||||
state_dir: Option<PathBuf>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
@@ -1536,7 +1543,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
config_server_url_s,
|
||||
crate::common::MachineIdOptions {
|
||||
explicit_machine_id: cli.machine_id.clone(),
|
||||
state_dir: None,
|
||||
state_dir: cli.state_dir.clone(),
|
||||
},
|
||||
cli.network_options.hostname.clone(),
|
||||
cli.network_options.secure_mode.unwrap_or(false),
|
||||
@@ -1733,6 +1740,7 @@ pub async fn main() -> ExitCode {
|
||||
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
|
||||
rust_i18n::set_locale(&locale);
|
||||
setup_panic_handler();
|
||||
crate::utils::init_crypto_provider();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
match windows_service::service_dispatcher::start(String::new(), ffi_service_main) {
|
||||
|
||||
@@ -182,17 +182,15 @@ impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
}
|
||||
}
|
||||
|
||||
fn init_crypto_provider() {
|
||||
let _ =
|
||||
rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider());
|
||||
}
|
||||
|
||||
fn get_insecure_tls_client_config() -> rustls::ClientConfig {
|
||||
init_crypto_provider();
|
||||
let provider = rustls::crypto::CryptoProvider::get_default().unwrap();
|
||||
let mut config = rustls::ClientConfig::builder()
|
||||
// Library callers may not have initialized a process-wide provider.
|
||||
// Use the same explicit backend for TLS and signature verification.
|
||||
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
||||
let mut config = rustls::ClientConfig::builder_with_provider(provider.clone())
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("ring supports the default TLS protocol versions")
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new(provider.clone()))
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new(provider))
|
||||
.with_no_client_auth();
|
||||
config.enable_sni = true;
|
||||
config.enable_early_data = false;
|
||||
@@ -220,12 +218,16 @@ where
|
||||
let peer_addr = stream.peer_addr()?;
|
||||
let mut remote_url = socket_url(local_url.scheme(), peer_addr);
|
||||
let stream = if is_wss(&local_url)? {
|
||||
init_crypto_provider();
|
||||
let (certificates, private_key) = get_insecure_tls_cert();
|
||||
let config = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certificates, private_key)
|
||||
.with_context(|| "Failed to create server config")?;
|
||||
// Do not rely on another tunnel initializing the global provider.
|
||||
let config = rustls::ServerConfig::builder_with_provider(Arc::new(
|
||||
rustls::crypto::ring::default_provider(),
|
||||
))
|
||||
.with_safe_default_protocol_versions()
|
||||
.with_context(|| "Failed to select TLS protocol versions")?
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certificates, private_key)
|
||||
.with_context(|| "Failed to create server config")?;
|
||||
Either::Left(TlsAcceptor::from(Arc::new(config)).accept(stream).await?)
|
||||
} else {
|
||||
Either::Right(stream)
|
||||
@@ -390,7 +392,6 @@ where
|
||||
let client = ClientBuilder::from_uri(http::Uri::try_from(remote_url.to_string()).unwrap())
|
||||
.max_headers(128);
|
||||
let stream: MaybeTlsStream<S> = if is_wss {
|
||||
init_crypto_provider();
|
||||
let tls = tokio_rustls::TlsConnector::from(Arc::new(get_insecure_tls_client_config()));
|
||||
let sni = remote_url.domain().unwrap_or("localhost").to_owned();
|
||||
let server_name = rustls::pki_types::ServerName::try_from(sni)
|
||||
|
||||
+48
-14
@@ -14,10 +14,17 @@ pub struct BufMargins {
|
||||
impl BufMargins {
|
||||
#[inline(always)]
|
||||
pub fn size(&self) -> usize {
|
||||
self.header + self.trailer
|
||||
self.header.checked_add(self.trailer).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A reusable packet buffer pool.
|
||||
///
|
||||
/// # Initialization contract
|
||||
///
|
||||
/// Buffers may contain uninitialized bytes in their [`BufMargins::header`] and
|
||||
/// [`BufMargins::trailer`] regions. These regions must be fully initialized
|
||||
/// before they are read or otherwise consumed.
|
||||
#[derive(Debug)]
|
||||
pub struct BufPool {
|
||||
pool: BytesMut,
|
||||
@@ -40,6 +47,10 @@ impl BufPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the accumulated buffer.
|
||||
///
|
||||
/// The caller must ensure all reserved header and trailer regions have been
|
||||
/// initialized before reading or consuming the returned buffer.
|
||||
#[inline(always)]
|
||||
pub fn split(&mut self) -> BytesMut {
|
||||
self.pool.split()
|
||||
@@ -47,8 +58,10 @@ impl BufPool {
|
||||
|
||||
#[inline]
|
||||
pub fn write(&mut self, chunk: &[u8], margins: BufMargins) {
|
||||
let len = margins.size() + chunk.len();
|
||||
let len = margins.size().checked_add(chunk.len()).unwrap();
|
||||
self.reserve(len);
|
||||
// Header and trailer are intentionally left uninitialized. Users of this
|
||||
// buffer must initialize both regions before reading or consuming it.
|
||||
unsafe {
|
||||
copy_nonoverlapping(
|
||||
chunk.as_ptr(),
|
||||
@@ -59,6 +72,10 @@ impl BufPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes `chunk` with the requested margins and returns the resulting buffer.
|
||||
///
|
||||
/// The header and trailer regions are left uninitialized and must be filled
|
||||
/// before the returned buffer is read or consumed.
|
||||
#[inline(always)]
|
||||
pub fn buf(&mut self, chunk: &[u8], margins: BufMargins) -> BytesMut {
|
||||
self.write(chunk, margins);
|
||||
@@ -77,6 +94,12 @@ impl BufPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer into a [`BufPool`].
|
||||
///
|
||||
/// # Initialization contract
|
||||
///
|
||||
/// Committing data does not initialize the reserved header or trailer regions.
|
||||
/// They must be fully initialized before the resulting buffer is read or consumed.
|
||||
#[derive(Debug)]
|
||||
pub struct BufPoolWriter<'t> {
|
||||
pool: &'t mut BufPool,
|
||||
@@ -87,32 +110,43 @@ pub struct BufPoolWriter<'t> {
|
||||
impl<'t> BufPoolWriter<'t> {
|
||||
#[inline(always)]
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
if self.capacity < additional {
|
||||
self.pool.reserve(additional);
|
||||
self.capacity += additional;
|
||||
}
|
||||
let capacity = self.capacity.checked_add(additional).unwrap();
|
||||
self.pool.reserve(capacity);
|
||||
self.capacity = capacity;
|
||||
}
|
||||
|
||||
/// Returns the accumulated buffer.
|
||||
///
|
||||
/// Reserved header and trailer regions must be initialized before the returned
|
||||
/// buffer is read or consumed.
|
||||
#[inline(always)]
|
||||
pub fn split(&mut self) -> BytesMut {
|
||||
self.pool.split()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.capacity.saturating_sub(self.margins.size())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_slice(&mut self) -> &mut [MaybeUninit<u8>] {
|
||||
unsafe {
|
||||
self.pool
|
||||
.pool
|
||||
.spare_capacity_mut()
|
||||
.get_unchecked_mut(self.margins.header..self.capacity - self.margins.trailer)
|
||||
let remaining = self.remaining();
|
||||
if remaining == 0 {
|
||||
&mut []
|
||||
} else {
|
||||
&mut self.pool.pool.spare_capacity_mut()
|
||||
[self.margins.header..self.margins.header + remaining]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn commit(&mut self, written: usize) {
|
||||
let len = self.margins.size() + written;
|
||||
let len = self.margins.size().checked_add(written).unwrap();
|
||||
assert!(self.capacity >= len);
|
||||
self.capacity -= len;
|
||||
// `commit` marks the margins as part of the buffer without initializing them.
|
||||
// They must be initialized before the resulting buffer is consumed.
|
||||
unsafe {
|
||||
self.pool.pool.advance_mut(len);
|
||||
}
|
||||
@@ -141,8 +175,8 @@ impl<const SIZE: usize> FixedBufPool<SIZE> {
|
||||
#[derive(Debug, Deref, DerefMut, AsRef, AsMut)]
|
||||
pub struct FixedBufGuard<'p, const SIZE: usize> {
|
||||
pool: &'p FixedBufPool<SIZE>,
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
#[deref(forward)]
|
||||
#[deref_mut(forward)]
|
||||
#[as_ref([u8])]
|
||||
#[as_mut([u8])]
|
||||
buf: Vec<u8>,
|
||||
|
||||
@@ -9,6 +9,21 @@ use std::sync::{Arc, Weak};
|
||||
#[cfg(feature = "management")]
|
||||
pub type PeerRoutePair = crate::proto::api::instance::PeerRoutePair;
|
||||
|
||||
/// Select the process-wide TLS backend before starting application services.
|
||||
pub fn init_crypto_provider() {
|
||||
#[cfg(any(
|
||||
feature = "endpoint-discovery",
|
||||
feature = "quic",
|
||||
feature = "websocket"
|
||||
))]
|
||||
{
|
||||
// Cargo features are additive: another dependency can enable aws-lc
|
||||
// alongside ring, making rustls's automatic selection panic (#2607).
|
||||
// Keep an existing provider chosen by an embedding application.
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_tcp_available(port: u16) -> bool {
|
||||
let s = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port);
|
||||
TcpListener::bind(s).is_ok()
|
||||
|
||||
@@ -395,6 +395,15 @@ mod tests {
|
||||
fn named_wireguard_keys_are_stable_and_client_scoped() {
|
||||
let master = [7; 32];
|
||||
let client = derive_named_key(&master, b"wireguard-client", "laptop").unwrap();
|
||||
// Pin the derived key across crypto dependency upgrades/downgrades.
|
||||
// Independently calculated with RFC 5869 HKDF-SHA256.
|
||||
assert_eq!(
|
||||
client,
|
||||
[
|
||||
5, 98, 244, 32, 245, 111, 41, 24, 163, 149, 201, 218, 22, 228, 8, 224, 134, 16,
|
||||
173, 29, 62, 138, 202, 41, 172, 230, 189, 237, 207, 100, 51, 236,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
client,
|
||||
derive_named_key(&master, b"wireguard-client", "laptop").unwrap()
|
||||
|
||||
Reference in New Issue
Block a user