feat: System DNS, Node Enrollment, and CDN Steering
- osds: Added system DNS forwarder on 127.0.0.1:53 - SystemDnsManager for Windows/Linux DNS configuration - Auto-restore original DNS on exit - *.ospab.internal routing to master node - Encrypted DNS forwarding through OSTP tunnel - oncp: Implemented node enrollment system - EnrollmentRegistry with state machine (Pending->Approved->Active) - SQLite-backed enrollment storage - Node PSK generation on approval - REST API endpoints for enrollment workflow - oncp-master: Added enrollment CLI commands - 'node pending' - List pending enrollment requests - 'node approve <id>' - Approve and generate PSK - 'node reject <id>' - Reject enrollment - ostp-server: Auto-registration on startup - Submits enrollment request to master node - Exits if PSK='AUTO' and awaits approval - Integrates with ONCP enrollment API - oncp API: Enhanced CDN steering - Best nodes by country_code with fallback - Steering metadata (matched, fallback status) - Load-based node selection
This commit is contained in:
394
Cargo.lock
generated
394
Cargo.lock
generated
@@ -200,10 +200,10 @@ dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
@@ -216,7 +216,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
@@ -233,13 +233,13 @@ dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -594,6 +594,16 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -617,9 +627,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -630,7 +640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1066,6 +1076,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1073,7 +1092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1087,6 +1106,12 @@ dependencies = [
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1494,6 +1519,25 @@ dependencies = [
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.12.1",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.12"
|
||||
@@ -1505,7 +1549,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"http 1.4.0",
|
||||
"indexmap 2.12.1",
|
||||
"slab",
|
||||
"tokio",
|
||||
@@ -1582,6 +1626,17 @@ dependencies = [
|
||||
"match_token",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -1592,6 +1647,17 @@ dependencies = [
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
@@ -1599,7 +1665,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 1.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1610,8 +1676,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -1627,6 +1693,30 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
@@ -1637,9 +1727,9 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.4.12",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
@@ -1650,6 +1740,19 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper 0.14.32",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.19"
|
||||
@@ -1661,14 +1764,14 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.8.1",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.6.1",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -2261,6 +2364,23 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2587,10 +2707,12 @@ dependencies = [
|
||||
"axum",
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"hyper",
|
||||
"hex",
|
||||
"hyper 1.8.1",
|
||||
"image 0.24.9",
|
||||
"ostp",
|
||||
"qrcode",
|
||||
"rand 0.8.5",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2641,6 +2763,50 @@ dependencies = [
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2740,6 +2906,7 @@ dependencies = [
|
||||
"ostp",
|
||||
"ostp-guard",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -3330,6 +3497,46 @@ version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 0.1.2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
@@ -3340,10 +3547,10 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -3352,7 +3559,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
@@ -3401,6 +3608,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -3422,6 +3638,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
@@ -3479,6 +3704,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
@@ -3792,6 +4040,16 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
@@ -3926,6 +4184,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -3946,6 +4210,27 @@ dependencies = [
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -3967,7 +4252,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
@@ -4032,7 +4317,7 @@ dependencies = [
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http 1.4.0",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4046,7 +4331,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -4177,7 +4462,7 @@ dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.4.0",
|
||||
"jni",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
@@ -4200,7 +4485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.4.0",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2",
|
||||
@@ -4233,7 +4518,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 1.4.0",
|
||||
"infer",
|
||||
"json-patch",
|
||||
"kuchikiki",
|
||||
@@ -4395,7 +4680,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"socket2 0.6.1",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -4411,6 +4696,16 @@ dependencies = [
|
||||
"syn 2.0.112",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.17"
|
||||
@@ -4540,7 +4835,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -4555,8 +4850,8 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
@@ -4573,8 +4868,8 @@ dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower 0.5.2",
|
||||
@@ -5277,6 +5572,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
@@ -5583,6 +5887,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
@@ -5631,7 +5945,7 @@ dependencies = [
|
||||
"gdkx11",
|
||||
"gtk",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 1.4.0",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"kuchikiki",
|
||||
|
||||
@@ -87,6 +87,18 @@ enum NodeCommands {
|
||||
/// Node ID
|
||||
id: String,
|
||||
},
|
||||
/// List pending enrollment requests
|
||||
Pending,
|
||||
/// Approve enrollment request
|
||||
Approve {
|
||||
/// Node ID to approve
|
||||
id: String,
|
||||
},
|
||||
/// Reject enrollment request
|
||||
Reject {
|
||||
/// Node ID to reject
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -328,6 +340,71 @@ async fn handle_node_command(state: Arc<AppState>, action: NodeCommands) -> Resu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeCommands::Pending => {
|
||||
use oncp::EnrollmentState;
|
||||
|
||||
let pending = state.enrollment.list_by_state(EnrollmentState::Pending)?;
|
||||
|
||||
println!("{}", style("Pending Enrollment Requests").green().bold());
|
||||
println!("{}", style("─").dim().to_string().repeat(80));
|
||||
|
||||
if pending.is_empty() {
|
||||
println!("{}", style("No pending requests").dim());
|
||||
} else {
|
||||
println!("{:<36} {:<15} {:<20} {:<8}",
|
||||
style("Node ID").bold(),
|
||||
style("Name").bold(),
|
||||
style("Address").bold(),
|
||||
style("Country").bold()
|
||||
);
|
||||
|
||||
for node in pending {
|
||||
println!("{:<36} {:<15} {:<20} {:<8}",
|
||||
node.node_id,
|
||||
node.name,
|
||||
node.address,
|
||||
node.country_code
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", style("Use 'node approve <id>' to approve").dim());
|
||||
}
|
||||
}
|
||||
|
||||
NodeCommands::Approve { id } => {
|
||||
let uuid = uuid::Uuid::parse_str(&id)?;
|
||||
|
||||
match state.enrollment.approve(&uuid) {
|
||||
Ok(node_psk) => {
|
||||
println!("{} Node approved", style("✓").green().bold());
|
||||
println!();
|
||||
println!(" Node ID: {}", style(uuid).yellow());
|
||||
println!(" Node PSK: {}", style(&node_psk).yellow().bold());
|
||||
println!();
|
||||
println!("{}", style("⚠ IMPORTANT: Save this PSK securely!").red().bold());
|
||||
println!("{}", style(" Send it to the node operator via secure channel").dim());
|
||||
println!("{}", style(" The node must use this PSK to connect").dim());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} Failed to approve: {}", style("✗").red().bold(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeCommands::Reject { id } => {
|
||||
let uuid = uuid::Uuid::parse_str(&id)?;
|
||||
|
||||
match state.enrollment.reject(&uuid) {
|
||||
Ok(()) => {
|
||||
println!("{} Node enrollment rejected", style("✓").green().bold());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} Failed to reject: {}", style("✗").red().bold(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -21,3 +21,5 @@ hyper = { version = "1.0", features = ["full"] }
|
||||
base64 = "0.21"
|
||||
qrcode = "0.14"
|
||||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
hex = "0.4"
|
||||
rand = "0.8"
|
||||
|
||||
167
oncp/src/api.rs
167
oncp/src/api.rs
@@ -15,6 +15,7 @@ use tower_http::trace::TraceLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::billing::{SqliteRegistry, User, UserRegistry};
|
||||
use crate::enrollment::{EnrollmentRegistry, EnrollmentRequest, EnrollmentState};
|
||||
use crate::node::{NetworkStats, Node, NodeCheckin, NodeRegistry};
|
||||
use crate::session::SessionManager;
|
||||
use crate::sni::{SniManager, SniUpdate};
|
||||
@@ -25,6 +26,7 @@ pub struct AppState {
|
||||
pub users: SqliteRegistry,
|
||||
pub sessions: SessionManager,
|
||||
pub sni_manager: SniManager,
|
||||
pub enrollment: EnrollmentRegistry,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -34,6 +36,7 @@ impl AppState {
|
||||
users: SqliteRegistry::new(db_path)?,
|
||||
sessions: SessionManager::new(300), // 5 minute heartbeat timeout
|
||||
sni_manager: SniManager::new(),
|
||||
enrollment: EnrollmentRegistry::new(db_path)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -47,6 +50,12 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
.route("/api/v1/nodes/:id/checkin", post(node_checkin))
|
||||
.route("/api/v1/nodes/best", get(best_nodes))
|
||||
|
||||
// Node enrollment
|
||||
.route("/api/v1/enrollment/request", post(submit_enrollment))
|
||||
.route("/api/v1/enrollment/pending", get(list_pending_enrollments))
|
||||
.route("/api/v1/enrollment/:id/approve", post(approve_enrollment))
|
||||
.route("/api/v1/enrollment/:id/reject", post(reject_enrollment))
|
||||
|
||||
// User management
|
||||
.route("/api/v1/users", get(list_users).post(create_user))
|
||||
.route("/api/v1/users/:id", get(get_user).delete(delete_user))
|
||||
@@ -147,25 +156,62 @@ async fn node_checkin(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get best nodes for client connection
|
||||
/// Get best nodes for client connection (CDN Steering)
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BestNodesQuery {
|
||||
country: Option<String>,
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BestNodesResponse {
|
||||
nodes: Vec<Node>,
|
||||
steering_info: SteeringInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SteeringInfo {
|
||||
requested_country: Option<String>,
|
||||
matched_country: bool,
|
||||
fallback_used: bool,
|
||||
total_available: usize,
|
||||
}
|
||||
|
||||
async fn best_nodes(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<BestNodesQuery>,
|
||||
) -> Json<Vec<Node>> {
|
||||
) -> Json<BestNodesResponse> {
|
||||
let limit = query.limit.unwrap_or(3);
|
||||
|
||||
let nodes = match &query.country {
|
||||
Some(country) => state.nodes.best_for_country(country, limit).await,
|
||||
None => state.nodes.best_global(limit).await,
|
||||
let (nodes, matched, fallback) = match &query.country {
|
||||
Some(country) => {
|
||||
let country_nodes = state.nodes.best_for_country(country, limit).await;
|
||||
|
||||
if country_nodes.is_empty() {
|
||||
// No nodes in requested country - fallback to global
|
||||
let global_nodes = state.nodes.best_global(limit).await;
|
||||
(global_nodes, false, true)
|
||||
} else {
|
||||
(country_nodes, true, false)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let global = state.nodes.best_global(limit).await;
|
||||
(global, false, false)
|
||||
}
|
||||
};
|
||||
|
||||
Json(nodes)
|
||||
let total = state.nodes.list_online().await.len();
|
||||
|
||||
Json(BestNodesResponse {
|
||||
nodes: nodes.clone(),
|
||||
steering_info: SteeringInfo {
|
||||
requested_country: query.country.clone(),
|
||||
matched_country: matched,
|
||||
fallback_used: fallback,
|
||||
total_available: total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -376,6 +422,115 @@ async fn emergency_sni_update(
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Enrollment Endpoints
|
||||
// ============================================================================
|
||||
|
||||
/// Submit node enrollment request
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SubmitEnrollmentRequest {
|
||||
name: String,
|
||||
address: String,
|
||||
country_code: String,
|
||||
hardware_id: String,
|
||||
region: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SubmitEnrollmentResponse {
|
||||
node_id: Uuid,
|
||||
state: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
async fn submit_enrollment(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SubmitEnrollmentRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let node_id = Uuid::new_v4();
|
||||
let enrollment_req = EnrollmentRequest {
|
||||
node_id,
|
||||
name: req.name,
|
||||
address: req.address,
|
||||
country_code: req.country_code,
|
||||
hardware_id: req.hardware_id,
|
||||
region: req.region,
|
||||
requested_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
match state.enrollment.submit_request(enrollment_req) {
|
||||
Ok(()) => (
|
||||
StatusCode::CREATED,
|
||||
Json(SubmitEnrollmentResponse {
|
||||
node_id,
|
||||
state: "pending".into(),
|
||||
message: "Enrollment request submitted. Awaiting approval.".into(),
|
||||
}),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(SubmitEnrollmentResponse {
|
||||
node_id,
|
||||
state: "error".into(),
|
||||
message: format!("Failed to submit: {}", e),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// List pending enrollments
|
||||
async fn list_pending_enrollments(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
match state.enrollment.list_by_state(EnrollmentState::Pending) {
|
||||
Ok(nodes) => (StatusCode::OK, Json(nodes)),
|
||||
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, Json(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Approve enrollment
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ApproveEnrollmentResponse {
|
||||
node_id: Uuid,
|
||||
node_psk: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
async fn approve_enrollment(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match state.enrollment.approve(&id) {
|
||||
Ok(node_psk) => (
|
||||
StatusCode::OK,
|
||||
Json(ApproveEnrollmentResponse {
|
||||
node_id: id,
|
||||
node_psk,
|
||||
message: "Node approved. Use provided PSK to connect.".into(),
|
||||
}),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ApproveEnrollmentResponse {
|
||||
node_id: id,
|
||||
node_psk: String::new(),
|
||||
message: format!("Failed to approve: {}", e),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject enrollment
|
||||
async fn reject_enrollment(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match state.enrollment.reject(&id) {
|
||||
Ok(()) => StatusCode::NO_CONTENT,
|
||||
Err(_) => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Statistics
|
||||
// ============================================================================
|
||||
|
||||
386
oncp/src/enrollment.rs
Normal file
386
oncp/src/enrollment.rs
Normal file
@@ -0,0 +1,386 @@
|
||||
//! Node enrollment and dynamic registration system
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{params, Connection, Result as SqliteResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EnrollmentError {
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
#[error("node not found")]
|
||||
NotFound,
|
||||
#[error("invalid state transition")]
|
||||
InvalidTransition,
|
||||
#[error("crypto error: {0}")]
|
||||
Crypto(String),
|
||||
}
|
||||
|
||||
/// Node enrollment state machine
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum EnrollmentState {
|
||||
Pending,
|
||||
Approved,
|
||||
Active,
|
||||
Rejected,
|
||||
Suspended,
|
||||
}
|
||||
|
||||
impl EnrollmentState {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Pending => "pending",
|
||||
Self::Approved => "approved",
|
||||
Self::Active => "active",
|
||||
Self::Rejected => "rejected",
|
||||
Self::Suspended => "suspended",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"pending" => Some(Self::Pending),
|
||||
"approved" => Some(Self::Approved),
|
||||
"active" => Some(Self::Active),
|
||||
"rejected" => Some(Self::Rejected),
|
||||
"suspended" => Some(Self::Suspended),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node enrollment request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnrollmentRequest {
|
||||
pub node_id: Uuid,
|
||||
pub name: String,
|
||||
pub address: String, // Public IP:port
|
||||
pub country_code: String,
|
||||
pub hardware_id: String, // Unique hardware fingerprint
|
||||
pub region: String, // Geographic region
|
||||
pub requested_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Enrolled node record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnrolledNode {
|
||||
pub node_id: Uuid,
|
||||
pub name: String,
|
||||
pub address: String,
|
||||
pub country_code: String,
|
||||
pub hardware_id: String,
|
||||
pub region: String,
|
||||
pub state: EnrollmentState,
|
||||
pub node_psk: Option<String>, // Generated PSK for approved nodes
|
||||
pub requested_at: DateTime<Utc>,
|
||||
pub approved_at: Option<DateTime<Utc>>,
|
||||
pub activated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl EnrolledNode {
|
||||
/// Check if node can transition to new state
|
||||
pub fn can_transition_to(&self, new_state: EnrollmentState) -> bool {
|
||||
match (self.state, new_state) {
|
||||
(EnrollmentState::Pending, EnrollmentState::Approved) => true,
|
||||
(EnrollmentState::Pending, EnrollmentState::Rejected) => true,
|
||||
(EnrollmentState::Approved, EnrollmentState::Active) => true,
|
||||
(EnrollmentState::Approved, EnrollmentState::Rejected) => true,
|
||||
(EnrollmentState::Active, EnrollmentState::Suspended) => true,
|
||||
(EnrollmentState::Suspended, EnrollmentState::Active) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node enrollment registry with SQLite backend
|
||||
pub struct EnrollmentRegistry {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl EnrollmentRegistry {
|
||||
/// Create new enrollment registry with SQLite database
|
||||
pub fn new(db_path: impl AsRef<std::path::Path>) -> Result<Self, EnrollmentError> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS enrollments (
|
||||
node_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
country_code TEXT NOT NULL,
|
||||
hardware_id TEXT NOT NULL,
|
||||
region TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
node_psk TEXT,
|
||||
requested_at TEXT NOT NULL,
|
||||
approved_at TEXT,
|
||||
activated_at TEXT
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Submit enrollment request (server wants to join network)
|
||||
pub fn submit_request(&self, req: EnrollmentRequest) -> Result<(), EnrollmentError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO enrollments (
|
||||
node_id, name, address, country_code, hardware_id, region,
|
||||
state, node_psk, requested_at, approved_at, activated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8, NULL, NULL)",
|
||||
params![
|
||||
req.node_id.to_string(),
|
||||
req.name,
|
||||
req.address,
|
||||
req.country_code,
|
||||
req.hardware_id,
|
||||
req.region,
|
||||
EnrollmentState::Pending.as_str(),
|
||||
req.requested_at.to_rfc3339(),
|
||||
],
|
||||
)?;
|
||||
|
||||
tracing::info!("Enrollment request submitted for node {}", req.node_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get enrollment by node ID
|
||||
pub fn get(&self, node_id: &Uuid) -> Result<Option<EnrolledNode>, EnrollmentError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT node_id, name, address, country_code, hardware_id, region,
|
||||
state, node_psk, requested_at, approved_at, activated_at
|
||||
FROM enrollments WHERE node_id = ?1"
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![node_id.to_string()])?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
Ok(Some(Self::row_to_node(row)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// List all enrollments by state
|
||||
pub fn list_by_state(&self, state: EnrollmentState) -> Result<Vec<EnrolledNode>, EnrollmentError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT node_id, name, address, country_code, hardware_id, region,
|
||||
state, node_psk, requested_at, approved_at, activated_at
|
||||
FROM enrollments WHERE state = ?1"
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map(params![state.as_str()], |row| {
|
||||
Ok(Self::row_to_node(row).unwrap())
|
||||
})?;
|
||||
|
||||
Ok(rows.collect::<SqliteResult<Vec<_>>>()?)
|
||||
}
|
||||
|
||||
/// Approve enrollment request and generate Node PSK
|
||||
pub fn approve(&self, node_id: &Uuid) -> Result<String, EnrollmentError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
// Generate unique PSK for this node
|
||||
let node_psk = Self::generate_node_psk();
|
||||
let now = Utc::now().to_rfc3339();
|
||||
|
||||
let updated = conn.execute(
|
||||
"UPDATE enrollments
|
||||
SET state = ?1, node_psk = ?2, approved_at = ?3
|
||||
WHERE node_id = ?4 AND state = ?5",
|
||||
params![
|
||||
EnrollmentState::Approved.as_str(),
|
||||
node_psk,
|
||||
now,
|
||||
node_id.to_string(),
|
||||
EnrollmentState::Pending.as_str(),
|
||||
],
|
||||
)?;
|
||||
|
||||
if updated == 0 {
|
||||
return Err(EnrollmentError::InvalidTransition);
|
||||
}
|
||||
|
||||
tracing::info!("Node {} approved", node_id);
|
||||
Ok(node_psk)
|
||||
}
|
||||
|
||||
/// Reject enrollment request
|
||||
pub fn reject(&self, node_id: &Uuid) -> Result<(), EnrollmentError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let updated = conn.execute(
|
||||
"UPDATE enrollments SET state = ?1 WHERE node_id = ?2 AND state = ?3",
|
||||
params![
|
||||
EnrollmentState::Rejected.as_str(),
|
||||
node_id.to_string(),
|
||||
EnrollmentState::Pending.as_str(),
|
||||
],
|
||||
)?;
|
||||
|
||||
if updated == 0 {
|
||||
return Err(EnrollmentError::InvalidTransition);
|
||||
}
|
||||
|
||||
tracing::info!("Node {} rejected", node_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Activate approved node (node has successfully connected)
|
||||
pub fn activate(&self, node_id: &Uuid) -> Result<(), EnrollmentError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let now = Utc::now().to_rfc3339();
|
||||
|
||||
let updated = conn.execute(
|
||||
"UPDATE enrollments
|
||||
SET state = ?1, activated_at = ?2
|
||||
WHERE node_id = ?3 AND state = ?4",
|
||||
params![
|
||||
EnrollmentState::Active.as_str(),
|
||||
now,
|
||||
node_id.to_string(),
|
||||
EnrollmentState::Approved.as_str(),
|
||||
],
|
||||
)?;
|
||||
|
||||
if updated == 0 {
|
||||
return Err(EnrollmentError::InvalidTransition);
|
||||
}
|
||||
|
||||
tracing::info!("Node {} activated", node_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Suspend active node
|
||||
pub fn suspend(&self, node_id: &Uuid) -> Result<(), EnrollmentError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let updated = conn.execute(
|
||||
"UPDATE enrollments SET state = ?1 WHERE node_id = ?2 AND state = ?3",
|
||||
params![
|
||||
EnrollmentState::Suspended.as_str(),
|
||||
node_id.to_string(),
|
||||
EnrollmentState::Active.as_str(),
|
||||
],
|
||||
)?;
|
||||
|
||||
if updated == 0 {
|
||||
return Err(EnrollmentError::InvalidTransition);
|
||||
}
|
||||
|
||||
tracing::info!("Node {} suspended", node_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate secure Node PSK (32 bytes hex)
|
||||
fn generate_node_psk() -> String {
|
||||
use rand::RngCore;
|
||||
let mut psk = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut psk);
|
||||
hex::encode(psk)
|
||||
}
|
||||
|
||||
/// Convert SQLite row to EnrolledNode
|
||||
fn row_to_node(row: &rusqlite::Row) -> SqliteResult<EnrolledNode> {
|
||||
Ok(EnrolledNode {
|
||||
node_id: Uuid::parse_str(&row.get::<_, String>(0)?).unwrap(),
|
||||
name: row.get(1)?,
|
||||
address: row.get(2)?,
|
||||
country_code: row.get(3)?,
|
||||
hardware_id: row.get(4)?,
|
||||
region: row.get(5)?,
|
||||
state: EnrollmentState::from_str(&row.get::<_, String>(6)?).unwrap(),
|
||||
node_psk: row.get(7)?,
|
||||
requested_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(8)?)
|
||||
.unwrap()
|
||||
.into(),
|
||||
approved_at: row
|
||||
.get::<_, Option<String>>(9)?
|
||||
.and_then(|s| DateTime::parse_from_rfc3339(&s).ok())
|
||||
.map(|dt| dt.into()),
|
||||
activated_at: row
|
||||
.get::<_, Option<String>>(10)?
|
||||
.and_then(|s| DateTime::parse_from_rfc3339(&s).ok())
|
||||
.map(|dt| dt.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_state_transitions() {
|
||||
let node = EnrolledNode {
|
||||
node_id: Uuid::new_v4(),
|
||||
name: "test".into(),
|
||||
address: "1.2.3.4:8443".into(),
|
||||
country_code: "US".into(),
|
||||
hardware_id: "hwid".into(),
|
||||
region: "us-west".into(),
|
||||
state: EnrollmentState::Pending,
|
||||
node_psk: None,
|
||||
requested_at: Utc::now(),
|
||||
approved_at: None,
|
||||
activated_at: None,
|
||||
};
|
||||
|
||||
assert!(node.can_transition_to(EnrollmentState::Approved));
|
||||
assert!(node.can_transition_to(EnrollmentState::Rejected));
|
||||
assert!(!node.can_transition_to(EnrollmentState::Active));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enrollment_workflow() -> Result<(), EnrollmentError> {
|
||||
let registry = EnrollmentRegistry::new(":memory:")?;
|
||||
let node_id = Uuid::new_v4();
|
||||
|
||||
// Submit request
|
||||
let req = EnrollmentRequest {
|
||||
node_id,
|
||||
name: "test-node".into(),
|
||||
address: "1.2.3.4:8443".into(),
|
||||
country_code: "US".into(),
|
||||
hardware_id: "test-hw".into(),
|
||||
region: "us-west".into(),
|
||||
requested_at: Utc::now(),
|
||||
};
|
||||
registry.submit_request(req)?;
|
||||
|
||||
// Check pending
|
||||
let pending = registry.list_by_state(EnrollmentState::Pending)?;
|
||||
assert_eq!(pending.len(), 1);
|
||||
|
||||
// Approve
|
||||
let psk = registry.approve(&node_id)?;
|
||||
assert_eq!(psk.len(), 64); // 32 bytes hex
|
||||
|
||||
// Check approved
|
||||
let approved = registry.list_by_state(EnrollmentState::Approved)?;
|
||||
assert_eq!(approved.len(), 1);
|
||||
|
||||
// Activate
|
||||
registry.activate(&node_id)?;
|
||||
|
||||
// Check active
|
||||
let active = registry.list_by_state(EnrollmentState::Active)?;
|
||||
assert_eq!(active.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
pub mod api;
|
||||
pub mod billing;
|
||||
pub mod enrollment;
|
||||
pub mod node;
|
||||
pub mod session;
|
||||
pub mod sni;
|
||||
|
||||
pub use api::{create_router, run_server, AppState};
|
||||
pub use billing::{BillingError, SqliteRegistry, User, UserRegistry};
|
||||
pub use enrollment::{
|
||||
EnrolledNode, EnrollmentError, EnrollmentRegistry, EnrollmentRequest, EnrollmentState,
|
||||
};
|
||||
pub use node::{NetworkStats, Node, NodeCheckin, NodeRegistry, NodeStatus};
|
||||
pub use session::{Session, SessionManager};
|
||||
pub use sni::{SniManager, SniUpdate};
|
||||
|
||||
@@ -62,6 +62,8 @@ pub struct StealthDnsForwarder {
|
||||
listen_addr: SocketAddr,
|
||||
/// Upstream resolver (will be tunneled through OSTP)
|
||||
upstream: SocketAddr,
|
||||
/// Master node internal IP for *.ospab.internal queries
|
||||
master_node_ip: std::net::IpAddr,
|
||||
}
|
||||
|
||||
impl StealthDnsForwarder {
|
||||
@@ -69,9 +71,15 @@ impl StealthDnsForwarder {
|
||||
Self {
|
||||
listen_addr: listen,
|
||||
upstream,
|
||||
master_node_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 8, 0, 1)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_master_ip(mut self, master_ip: std::net::IpAddr) -> Self {
|
||||
self.master_node_ip = master_ip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Start DNS listener (intercepts local queries)
|
||||
pub async fn run(&self) -> Result<(), DnsError> {
|
||||
let socket = UdpSocket::bind(self.listen_addr).await?;
|
||||
@@ -82,6 +90,12 @@ impl StealthDnsForwarder {
|
||||
let (len, src) = socket.recv_from(&mut buf).await?;
|
||||
let query = &buf[..len];
|
||||
|
||||
// Check if query is for *.ospab.internal
|
||||
if let Some(response) = self.handle_internal_query(query) {
|
||||
let _ = socket.send_to(&response, src).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Encrypt and forward through OSTP tunnel instead of direct
|
||||
// For now, direct forward (to be replaced with tunnel)
|
||||
match self.forward_query(query).await {
|
||||
@@ -95,6 +109,87 @@ impl StealthDnsForwarder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle *.ospab.internal queries - resolve to master node IP
|
||||
fn handle_internal_query(&self, query: &[u8]) -> Option<Vec<u8>> {
|
||||
if query.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse domain from query (simplified)
|
||||
let qname_start = 12;
|
||||
let mut pos = qname_start;
|
||||
let mut domain = String::new();
|
||||
|
||||
while pos < query.len() && query[pos] != 0 {
|
||||
let len = query[pos] as usize;
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
if !domain.is_empty() {
|
||||
domain.push('.');
|
||||
}
|
||||
pos += 1;
|
||||
if pos + len > query.len() {
|
||||
return None;
|
||||
}
|
||||
domain.push_str(&String::from_utf8_lossy(&query[pos..pos + len]));
|
||||
pos += len;
|
||||
}
|
||||
|
||||
// Check if domain ends with .ospab.internal
|
||||
if !domain.ends_with(".ospab.internal") {
|
||||
return None;
|
||||
}
|
||||
|
||||
tracing::debug!("Resolving internal domain: {}", domain);
|
||||
|
||||
// Build DNS response
|
||||
let mut response = Vec::from(query);
|
||||
|
||||
// Set response flags (QR=1, AA=1, RCODE=0)
|
||||
response[2] = 0x84; // QR + AA
|
||||
response[3] = 0x00; // No error
|
||||
|
||||
// Set answer count to 1
|
||||
response[6] = 0x00;
|
||||
response[7] = 0x01;
|
||||
|
||||
// Add answer section (pointer to question + A record)
|
||||
response.push(0xC0); // Pointer to question name
|
||||
response.push(0x0C); // Offset 12
|
||||
|
||||
// Type A
|
||||
response.push(0x00);
|
||||
response.push(0x01);
|
||||
|
||||
// Class IN
|
||||
response.push(0x00);
|
||||
response.push(0x01);
|
||||
|
||||
// TTL (300 seconds)
|
||||
response.push(0x00);
|
||||
response.push(0x00);
|
||||
response.push(0x01);
|
||||
response.push(0x2C);
|
||||
|
||||
// Data length (4 bytes for IPv4)
|
||||
response.push(0x00);
|
||||
response.push(0x04);
|
||||
|
||||
// IP address
|
||||
match self.master_node_ip {
|
||||
std::net::IpAddr::V4(ip) => {
|
||||
response.extend_from_slice(&ip.octets());
|
||||
}
|
||||
std::net::IpAddr::V6(_) => {
|
||||
// For IPv6, would need AAAA record - fallback to IPv4 loopback
|
||||
response.extend_from_slice(&[127, 0, 0, 1]);
|
||||
}
|
||||
}
|
||||
|
||||
Some(response)
|
||||
}
|
||||
|
||||
async fn forward_query(&self, query: &[u8]) -> Result<Vec<u8>, DnsError> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
socket.send_to(query, self.upstream).await?;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod dns;
|
||||
pub mod system;
|
||||
|
||||
pub use dns::{build_dns_query, detect_hijack, DnsError, QueryType, StealthDnsForwarder};
|
||||
pub use system::{DnsBackup, SystemDnsError, SystemDnsManager};
|
||||
|
||||
260
osds/src/system.rs
Normal file
260
osds/src/system.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
//! System DNS configuration management for Windows/Linux
|
||||
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SystemDnsError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("command failed: {0}")]
|
||||
CommandFailed(String),
|
||||
#[error("unsupported platform")]
|
||||
UnsupportedPlatform,
|
||||
#[error("permission denied")]
|
||||
PermissionDenied,
|
||||
}
|
||||
|
||||
/// Stores original DNS settings for restoration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DnsBackup {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub interface: String,
|
||||
#[cfg(target_os = "windows")]
|
||||
pub original_dns: Vec<String>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub resolv_conf_backup: String,
|
||||
}
|
||||
|
||||
/// System DNS manager - sets DNS to 127.0.0.1 and restores on drop
|
||||
pub struct SystemDnsManager {
|
||||
backup: Option<DnsBackup>,
|
||||
}
|
||||
|
||||
impl SystemDnsManager {
|
||||
pub fn new() -> Self {
|
||||
Self { backup: None }
|
||||
}
|
||||
|
||||
/// Set system DNS to 127.0.0.1 (requires admin/root)
|
||||
pub async fn set_local_dns(&mut self) -> Result<(), SystemDnsError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.set_windows_dns().await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.set_linux_dns().await
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
Err(SystemDnsError::UnsupportedPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore original DNS settings
|
||||
pub async fn restore(&self) -> Result<(), SystemDnsError> {
|
||||
if self.backup.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.restore_windows_dns().await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.restore_linux_dns().await
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
Err(SystemDnsError::UnsupportedPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn set_windows_dns(&mut self) -> Result<(), SystemDnsError> {
|
||||
// Get active network interface
|
||||
let output = Command::new("netsh")
|
||||
.args(&["interface", "show", "interface"])
|
||||
.output()?;
|
||||
|
||||
let interfaces = String::from_utf8_lossy(&output.stdout);
|
||||
let active_interface = interfaces
|
||||
.lines()
|
||||
.find(|line| line.contains("Connected") && !line.contains("Disconnected"))
|
||||
.and_then(|line| line.split_whitespace().last())
|
||||
.ok_or_else(|| SystemDnsError::CommandFailed("No active interface found".into()))?;
|
||||
|
||||
// Backup current DNS settings
|
||||
let output = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"show",
|
||||
"dnsservers",
|
||||
active_interface,
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let dns_output = String::from_utf8_lossy(&output.stdout);
|
||||
let original_dns: Vec<String> = dns_output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if line.trim().starts_with(|c: char| c.is_numeric()) {
|
||||
Some(line.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.backup = Some(DnsBackup {
|
||||
interface: active_interface.to_string(),
|
||||
original_dns,
|
||||
});
|
||||
|
||||
// Set DNS to 127.0.0.1
|
||||
let status = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"set",
|
||||
"dnsservers",
|
||||
active_interface,
|
||||
"static",
|
||||
"127.0.0.1",
|
||||
"primary",
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(SystemDnsError::CommandFailed(
|
||||
"Failed to set DNS".into(),
|
||||
));
|
||||
}
|
||||
|
||||
tracing::info!("System DNS set to 127.0.0.1 on interface {}", active_interface);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn restore_windows_dns(&self) -> Result<(), SystemDnsError> {
|
||||
let backup = self.backup.as_ref().unwrap();
|
||||
|
||||
if backup.original_dns.is_empty() {
|
||||
// Set to DHCP
|
||||
let status = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"set",
|
||||
"dnsservers",
|
||||
&backup.interface,
|
||||
"dhcp",
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(SystemDnsError::CommandFailed(
|
||||
"Failed to restore DNS to DHCP".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// Restore first DNS as primary
|
||||
let status = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"set",
|
||||
"dnsservers",
|
||||
&backup.interface,
|
||||
"static",
|
||||
&backup.original_dns[0],
|
||||
"primary",
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(SystemDnsError::CommandFailed(
|
||||
"Failed to restore primary DNS".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Add additional DNS servers
|
||||
for dns in backup.original_dns.iter().skip(1) {
|
||||
let _ = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"add",
|
||||
"dnsservers",
|
||||
&backup.interface,
|
||||
dns,
|
||||
])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("System DNS restored on interface {}", backup.interface);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn set_linux_dns(&mut self) -> Result<(), SystemDnsError> {
|
||||
use tokio::fs;
|
||||
|
||||
// Backup /etc/resolv.conf
|
||||
let resolv_conf = fs::read_to_string("/etc/resolv.conf").await?;
|
||||
self.backup = Some(DnsBackup {
|
||||
resolv_conf_backup: resolv_conf,
|
||||
});
|
||||
|
||||
// Write new resolv.conf
|
||||
let new_conf = "# Managed by OSTP\nnameserver 127.0.0.1\n";
|
||||
fs::write("/etc/resolv.conf", new_conf).await?;
|
||||
|
||||
tracing::info!("System DNS set to 127.0.0.1 in /etc/resolv.conf");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn restore_linux_dns(&self) -> Result<(), SystemDnsError> {
|
||||
use tokio::fs;
|
||||
|
||||
let backup = self.backup.as_ref().unwrap();
|
||||
fs::write("/etc/resolv.conf", &backup.resolv_conf_backup).await?;
|
||||
|
||||
tracing::info!("System DNS restored from backup");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SystemDnsManager {
|
||||
fn drop(&mut self) {
|
||||
if self.backup.is_some() {
|
||||
// Best effort restore - use blocking runtime
|
||||
let rt = tokio::runtime::Handle::try_current();
|
||||
if let Ok(handle) = rt {
|
||||
let _ = handle.block_on(self.restore());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires admin/root
|
||||
async fn test_set_and_restore_dns() {
|
||||
let mut manager = SystemDnsManager::new();
|
||||
manager.set_local_dns().await.unwrap();
|
||||
manager.restore().await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,4 @@ hex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
@@ -55,6 +55,13 @@ struct ConfigFile {
|
||||
psk: String,
|
||||
max_connections: Option<usize>,
|
||||
log_level: Option<String>,
|
||||
|
||||
// Node enrollment settings
|
||||
master_node_url: Option<String>,
|
||||
node_name: Option<String>,
|
||||
hardware_id: Option<String>,
|
||||
region: Option<String>,
|
||||
country_code: Option<String>,
|
||||
}
|
||||
|
||||
fn setup_logging(level: &str) {
|
||||
@@ -120,7 +127,7 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// Load config from file if specified
|
||||
let (listen, psk, log_level) = if let Some(config_path) = cli.config {
|
||||
let (listen, psk, log_level, config_path_opt) = if let Some(config_path) = cli.config.clone() {
|
||||
let content = std::fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read config file: {:?}", config_path))?;
|
||||
let config: ConfigFile = serde_json::from_str(&content)
|
||||
@@ -131,7 +138,7 @@ async fn main() -> Result<()> {
|
||||
let psk = parse_psk(&config.psk)?;
|
||||
let level = config.log_level.unwrap_or_else(|| cli.log_level.clone());
|
||||
|
||||
(addr, psk, level)
|
||||
(addr, psk, level, Some(config_path))
|
||||
} else {
|
||||
// Use CLI args
|
||||
let psk_str = cli.psk.ok_or_else(|| {
|
||||
@@ -139,7 +146,7 @@ async fn main() -> Result<()> {
|
||||
})?;
|
||||
let psk = parse_psk(&psk_str)?;
|
||||
|
||||
(cli.listen, psk, cli.log_level)
|
||||
(cli.listen, psk, cli.log_level, None)
|
||||
};
|
||||
|
||||
setup_logging(&log_level);
|
||||
@@ -153,6 +160,47 @@ async fn main() -> Result<()> {
|
||||
tracing::info!(" PSK: {}...{}", &hex::encode(&psk[..4]), &hex::encode(&psk[28..]));
|
||||
tracing::info!("");
|
||||
|
||||
// Check if node enrollment is configured
|
||||
if let Some(config_path) = config_path_opt {
|
||||
let content = std::fs::read_to_string(&config_path)?;
|
||||
let config: ConfigFile = serde_json::from_str(&content)?;
|
||||
|
||||
if let Some(master_url) = config.master_node_url {
|
||||
if config.psk == "AUTO" {
|
||||
// Node needs to enroll - request PSK from master
|
||||
tracing::info!("Node not enrolled - requesting to join network...");
|
||||
|
||||
let enrollment_result = request_enrollment(
|
||||
&master_url,
|
||||
config.node_name.as_deref().unwrap_or("ostp-node"),
|
||||
&listen.to_string(),
|
||||
config.country_code.as_deref().unwrap_or("US"),
|
||||
config.hardware_id.as_deref().unwrap_or("unknown"),
|
||||
config.region.as_deref().unwrap_or("default"),
|
||||
).await;
|
||||
|
||||
match enrollment_result {
|
||||
Ok(node_id) => {
|
||||
tracing::info!("✓ Enrollment request submitted");
|
||||
tracing::info!(" Node ID: {}", node_id);
|
||||
tracing::info!("");
|
||||
tracing::info!("Waiting for master node approval...");
|
||||
tracing::info!("Contact the administrator to approve node: {}", node_id);
|
||||
tracing::info!("Once approved, update config with provided PSK");
|
||||
|
||||
// Exit - node must wait for approval
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to submit enrollment: {}", e);
|
||||
tracing::error!("Server will not start until enrolled");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config = ServerConfig::new(listen, psk);
|
||||
let server = OstpServer::new(config);
|
||||
|
||||
@@ -161,3 +209,59 @@ async fn main() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Submit enrollment request to master node
|
||||
async fn request_enrollment(
|
||||
master_url: &str,
|
||||
name: &str,
|
||||
address: &str,
|
||||
country_code: &str,
|
||||
hardware_id: &str,
|
||||
region: &str,
|
||||
) -> Result<String> {
|
||||
#[derive(serde::Serialize)]
|
||||
struct EnrollmentRequest {
|
||||
name: String,
|
||||
address: String,
|
||||
country_code: String,
|
||||
hardware_id: String,
|
||||
region: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct EnrollmentResponse {
|
||||
node_id: String,
|
||||
state: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()?;
|
||||
|
||||
let request = EnrollmentRequest {
|
||||
name: name.to_string(),
|
||||
address: address.to_string(),
|
||||
country_code: country_code.to_string(),
|
||||
hardware_id: hardware_id.to_string(),
|
||||
region: region.to_string(),
|
||||
};
|
||||
|
||||
let url = format!("{}/api/v1/enrollment/request", master_url);
|
||||
let response = client
|
||||
.post(&url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to connect to master node")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Enrollment request failed: {} - {}", status, text);
|
||||
}
|
||||
|
||||
let enrollment: EnrollmentResponse = response.json().await?;
|
||||
Ok(enrollment.node_id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user