feat(net): scaffold sunbeam-net crate with foundations
Add the workspace crate that will host a pure Rust Headscale/Tailscale- compatible VPN client. This first commit lands the crate skeleton plus the leaf modules that the rest of the stack builds on: - error: thiserror Error enum + Result alias - config: VpnConfig - keys: Curve25519 node/disco/wg key types with on-disk persistence - proto/types: PascalCase serde wire types matching Tailscale's JSON
This commit is contained in:
392
Cargo.lock
generated
392
Cargo.lock
generated
@@ -318,6 +318,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -341,6 +347,12 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@@ -387,6 +399,30 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boringtun"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8dc4267b0c97985d9b089b19ff965b959e61870640d2f0842a97552e030fa43f"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"base64 0.13.1",
|
||||||
|
"blake2",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"hex",
|
||||||
|
"hmac",
|
||||||
|
"ip_network",
|
||||||
|
"ip_network_table",
|
||||||
|
"libc",
|
||||||
|
"nix",
|
||||||
|
"parking_lot",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"ring",
|
||||||
|
"tracing",
|
||||||
|
"untrusted",
|
||||||
|
"x25519-dalek",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.2"
|
version = "3.20.2"
|
||||||
@@ -455,6 +491,19 @@ dependencies = [
|
|||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20poly1305"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"chacha20",
|
||||||
|
"cipher",
|
||||||
|
"poly1305",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
@@ -487,6 +536,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"inout",
|
"inout",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -705,6 +755,35 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto_box"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"crypto_secretbox",
|
||||||
|
"curve25519-dalek",
|
||||||
|
"salsa20",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto_secretbox"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"cipher",
|
||||||
|
"generic-array",
|
||||||
|
"poly1305",
|
||||||
|
"salsa20",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctr"
|
name = "ctr"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
@@ -800,6 +879,47 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt"
|
||||||
|
version = "0.3.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"defmt-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt-macros"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e"
|
||||||
|
dependencies = [
|
||||||
|
"defmt-parser",
|
||||||
|
"proc-macro-error2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt-parser"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "delegate"
|
name = "delegate"
|
||||||
version = "0.13.5"
|
version = "0.13.5"
|
||||||
@@ -885,6 +1005,12 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -1034,7 +1160,7 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
|
checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1428,6 +1554,15 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hash32"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -1470,7 +1605,7 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers-core",
|
"headers-core",
|
||||||
"http",
|
"http",
|
||||||
@@ -1488,6 +1623,16 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heapless"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
||||||
|
dependencies = [
|
||||||
|
"hash32",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1676,7 +1821,7 @@ version = "0.1.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -1867,6 +2012,28 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ip_network"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ip_network_table"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0"
|
||||||
|
dependencies = [
|
||||||
|
"ip_network",
|
||||||
|
"ip_network_table-deps-treebitmap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ip_network_table-deps-treebitmap"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
@@ -2000,7 +2167,7 @@ version = "0.24.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5"
|
checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-value",
|
"serde-value",
|
||||||
@@ -2026,7 +2193,7 @@ version = "0.99.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fc2ed952042df20d15ac2fe9614d0ec14b6118eab89633985d4b36e688dccf1"
|
checksum = "7fc2ed952042df20d15ac2fe9614d0ec14b6118eab89633985d4b36e688dccf1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"either",
|
"either",
|
||||||
@@ -2140,7 +2307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f"
|
checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"chumsky",
|
"chumsky",
|
||||||
"email-encoding",
|
"email-encoding",
|
||||||
"email_address",
|
"email_address",
|
||||||
@@ -2180,7 +2347,7 @@ version = "0.1.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"libc",
|
"libc",
|
||||||
"plain",
|
"plain",
|
||||||
"redox_syscall 0.7.3",
|
"redox_syscall 0.7.3",
|
||||||
@@ -2230,6 +2397,12 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "managed"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2294,6 +2467,18 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -2573,7 +2758,7 @@ version = "3.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2770,6 +2955,16 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -2789,6 +2984,28 @@ dependencies = [
|
|||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr2"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -2970,7 +3187,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2979,7 +3196,7 @@ version = "0.7.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3028,7 +3245,7 @@ version = "0.12.28"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -3068,7 +3285,7 @@ version = "0.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http",
|
"http",
|
||||||
@@ -3151,7 +3368,7 @@ dependencies = [
|
|||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"cbc",
|
"cbc",
|
||||||
"chacha20",
|
"chacha20",
|
||||||
@@ -3257,7 +3474,7 @@ version = "2.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3bb94393cafad0530145b8f626d8687f1ee1dedb93d7ba7740d6ae81868b13b5"
|
checksum = "3bb94393cafad0530145b8f626d8687f1ee1dedb93d7ba7740d6ae81868b13b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"flurry",
|
"flurry",
|
||||||
@@ -3344,7 +3561,7 @@ version = "1.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@@ -3559,7 +3776,7 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3572,7 +3789,7 @@ version = "3.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3768,6 +3985,20 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smoltcp"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac729b0a77bd092a3f06ddaddc59fe0d67f48ba0de45a9abe707c2842c7f8767"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"byteorder",
|
||||||
|
"cfg-if",
|
||||||
|
"defmt 0.3.100",
|
||||||
|
"heapless",
|
||||||
|
"managed",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@@ -3816,7 +4047,7 @@ version = "0.8.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
@@ -3893,8 +4124,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3937,8 +4168,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
@@ -4095,7 +4326,7 @@ dependencies = [
|
|||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -4120,6 +4351,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"sunbeam-sdk",
|
||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -4135,13 +4367,44 @@ dependencies = [
|
|||||||
"wiremock",
|
"wiremock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sunbeam-net"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"blake2",
|
||||||
|
"boringtun",
|
||||||
|
"bytes",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"crypto_box",
|
||||||
|
"futures",
|
||||||
|
"h2",
|
||||||
|
"hkdf",
|
||||||
|
"hmac",
|
||||||
|
"http",
|
||||||
|
"ipnet",
|
||||||
|
"pretty_assertions",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"smoltcp",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
"x25519-dalek",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"argon2",
|
"argon2",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -4416,6 +4679,17 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-test"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.26.2"
|
version = "0.26.2"
|
||||||
@@ -4465,8 +4739,8 @@ version = "0.6.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
@@ -4865,7 +5139,7 @@ version = "0.244.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -5406,7 +5680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"deadpool",
|
"deadpool",
|
||||||
"futures",
|
"futures",
|
||||||
"http",
|
"http",
|
||||||
@@ -5480,7 +5754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5516,6 +5790,18 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x25519-dalek"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x509-parser"
|
name = "x509-parser"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -5568,6 +5854,12 @@ dependencies = [
|
|||||||
"hashlink",
|
"hashlink",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yasna"
|
name = "yasna"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -5646,6 +5938,20 @@ name = "zeroize"
|
|||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize_derive"
|
||||||
|
version = "1.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
@@ -5685,3 +5991,31 @@ name = "zmij"
|
|||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "7.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.16+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ name = "sunbeam"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["sunbeam-sdk"]
|
members = ["sunbeam-sdk", "sunbeam-net"]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
38
sunbeam-net/Cargo.toml
Normal file
38
sunbeam-net/Cargo.toml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "sunbeam-net"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Pure Rust Headscale/Tailscale-compatible VPN client"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
futures = "0.3"
|
||||||
|
blake2 = "0.10"
|
||||||
|
chacha20poly1305 = "0.10"
|
||||||
|
hkdf = "0.12"
|
||||||
|
hmac = "0.12"
|
||||||
|
h2 = "0.4"
|
||||||
|
http = "1"
|
||||||
|
boringtun = "0.7"
|
||||||
|
smoltcp = { version = "0.13", default-features = false, features = ["medium-ip", "proto-ipv4", "proto-ipv6", "socket-tcp", "std"] }
|
||||||
|
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||||
|
crypto_box = "0.9"
|
||||||
|
rand = "0.8"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
zstd = "0.13"
|
||||||
|
bytes = "1"
|
||||||
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
tracing = "0.1"
|
||||||
|
thiserror = "2"
|
||||||
|
ipnet = "2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
integration = []
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-test = "0.4"
|
||||||
|
pretty_assertions = "1"
|
||||||
|
tempfile = "3"
|
||||||
26
sunbeam-net/src/config.rs
Normal file
26
sunbeam-net/src/config.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Top-level configuration for a sunbeam-net VPN instance.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VpnConfig {
|
||||||
|
/// URL of the Headscale/Tailscale coordination server.
|
||||||
|
pub coordination_url: String,
|
||||||
|
/// Pre-auth key for automatic registration.
|
||||||
|
pub auth_key: String,
|
||||||
|
/// Directory for persisting keys and state.
|
||||||
|
pub state_dir: PathBuf,
|
||||||
|
/// Address to bind the SOCKS/TCP proxy on.
|
||||||
|
pub proxy_bind: SocketAddr,
|
||||||
|
/// Cluster API server IP (inside the VPN).
|
||||||
|
pub cluster_api_addr: IpAddr,
|
||||||
|
/// Cluster API server port.
|
||||||
|
pub cluster_api_port: u16,
|
||||||
|
/// Path for the daemon control socket.
|
||||||
|
pub control_socket: PathBuf,
|
||||||
|
/// Hostname to register with the coordination server.
|
||||||
|
pub hostname: String,
|
||||||
|
/// The coordination server's Noise public key (32 bytes).
|
||||||
|
/// If `None`, it will be fetched from the server's `/key` endpoint.
|
||||||
|
pub server_public_key: Option<[u8; 32]>,
|
||||||
|
}
|
||||||
80
sunbeam-net/src/error.rs
Normal file
80
sunbeam-net/src/error.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/// Errors produced by sunbeam-net.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("noise handshake failed: {0}")]
|
||||||
|
Noise(String),
|
||||||
|
|
||||||
|
#[error("control protocol error: {0}")]
|
||||||
|
Control(String),
|
||||||
|
|
||||||
|
#[error("wireguard error: {0}")]
|
||||||
|
WireGuard(String),
|
||||||
|
|
||||||
|
#[error("DERP relay error: {0}")]
|
||||||
|
Derp(String),
|
||||||
|
|
||||||
|
#[error("authentication failed: {0}")]
|
||||||
|
Auth(String),
|
||||||
|
|
||||||
|
#[error("daemon error: {0}")]
|
||||||
|
Daemon(String),
|
||||||
|
|
||||||
|
#[error("IPC error: {0}")]
|
||||||
|
Ipc(String),
|
||||||
|
|
||||||
|
#[error("{context}: {source}")]
|
||||||
|
Io {
|
||||||
|
context: String,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("connection closed")]
|
||||||
|
ConnectionClosed,
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
impl From<h2::Error> for Error {
|
||||||
|
fn from(e: h2::Error) -> Self {
|
||||||
|
Error::Control(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait for adding context to `Result` types.
|
||||||
|
pub trait ResultExt<T> {
|
||||||
|
fn ctx(self, context: &str) -> Result<T>;
|
||||||
|
fn with_ctx<F: FnOnce() -> String>(self, f: F) -> Result<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ResultExt<T> for std::result::Result<T, std::io::Error> {
|
||||||
|
fn ctx(self, context: &str) -> Result<T> {
|
||||||
|
self.map_err(|source| Error::Io {
|
||||||
|
context: context.to_string(),
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_ctx<F: FnOnce() -> String>(self, f: F) -> Result<T> {
|
||||||
|
self.map_err(|source| Error::Io {
|
||||||
|
context: f(),
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ResultExt<T> for Result<T> {
|
||||||
|
fn ctx(self, context: &str) -> Result<T> {
|
||||||
|
self.map_err(|e| Error::Other(format!("{context}: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_ctx<F: FnOnce() -> String>(self, f: F) -> Result<T> {
|
||||||
|
self.map_err(|e| Error::Other(format!("{}: {e}", f())))
|
||||||
|
}
|
||||||
|
}
|
||||||
178
sunbeam-net/src/keys.rs
Normal file
178
sunbeam-net/src/keys.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use x25519_dalek::{PublicKey, StaticSecret};
|
||||||
|
|
||||||
|
use crate::error::ResultExt;
|
||||||
|
|
||||||
|
const KEYS_FILE: &str = "keys.json";
|
||||||
|
|
||||||
|
/// The three x25519 key pairs used by a node.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NodeKeys {
|
||||||
|
pub node_private: StaticSecret,
|
||||||
|
pub node_public: PublicKey,
|
||||||
|
pub disco_private: StaticSecret,
|
||||||
|
pub disco_public: PublicKey,
|
||||||
|
pub wg_private: StaticSecret,
|
||||||
|
pub wg_public: PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for NodeKeys {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("NodeKeys")
|
||||||
|
.field("node_public", &self.node_key_str())
|
||||||
|
.field("disco_public", &self.disco_key_str())
|
||||||
|
.field("wg_public", &hex::encode(self.wg_public.as_bytes()))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On-disk representation of persisted keys.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct PersistedKeys {
|
||||||
|
node_private: String,
|
||||||
|
disco_private: String,
|
||||||
|
wg_private: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex helpers — we avoid pulling in a hex crate by implementing locally.
|
||||||
|
mod hex {
|
||||||
|
pub fn encode(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(s: &str) -> Result<Vec<u8>, String> {
|
||||||
|
if s.len() % 2 != 0 {
|
||||||
|
return Err("odd length hex string".into());
|
||||||
|
}
|
||||||
|
(0..s.len())
|
||||||
|
.step_by(2)
|
||||||
|
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeKeys {
|
||||||
|
/// Generate fresh random key pairs.
|
||||||
|
pub fn generate() -> Self {
|
||||||
|
let node_private = StaticSecret::random_from_rng(OsRng);
|
||||||
|
let node_public = PublicKey::from(&node_private);
|
||||||
|
|
||||||
|
let disco_private = StaticSecret::random_from_rng(OsRng);
|
||||||
|
let disco_public = PublicKey::from(&disco_private);
|
||||||
|
|
||||||
|
let wg_private = StaticSecret::random_from_rng(OsRng);
|
||||||
|
let wg_public = PublicKey::from(&wg_private);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
node_private,
|
||||||
|
node_public,
|
||||||
|
disco_private,
|
||||||
|
disco_public,
|
||||||
|
wg_private,
|
||||||
|
wg_public,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load keys from `state_dir/keys.json`, or generate and persist new ones.
|
||||||
|
pub fn load_or_generate(state_dir: &Path) -> crate::Result<Self> {
|
||||||
|
let path = state_dir.join(KEYS_FILE);
|
||||||
|
if path.exists() {
|
||||||
|
let data = std::fs::read_to_string(&path).ctx("reading keys file")?;
|
||||||
|
let persisted: PersistedKeys = serde_json::from_str(&data)?;
|
||||||
|
return Self::from_persisted(&persisted);
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys = Self::generate();
|
||||||
|
std::fs::create_dir_all(state_dir).ctx("creating state directory")?;
|
||||||
|
let persisted = keys.to_persisted();
|
||||||
|
let data = serde_json::to_string_pretty(&persisted)?;
|
||||||
|
std::fs::write(&path, data).ctx("writing keys file")?;
|
||||||
|
tracing::debug!("generated new node keys at {}", path.display());
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tailscale-style node key string: `nodekey:<hex>`.
|
||||||
|
pub fn node_key_str(&self) -> String {
|
||||||
|
format!("nodekey:{}", hex::encode(self.node_public.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tailscale-style disco key string: `discokey:<hex>`.
|
||||||
|
pub fn disco_key_str(&self) -> String {
|
||||||
|
format!("discokey:{}", hex::encode(self.disco_public.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_persisted(&self) -> PersistedKeys {
|
||||||
|
PersistedKeys {
|
||||||
|
node_private: hex::encode(self.node_private.as_bytes()),
|
||||||
|
disco_private: hex::encode(self.disco_private.as_bytes()),
|
||||||
|
wg_private: hex::encode(self.wg_private.as_bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_persisted(p: &PersistedKeys) -> crate::Result<Self> {
|
||||||
|
let node_bytes = parse_key_hex(&p.node_private, "node")?;
|
||||||
|
let disco_bytes = parse_key_hex(&p.disco_private, "disco")?;
|
||||||
|
let wg_bytes = parse_key_hex(&p.wg_private, "wg")?;
|
||||||
|
|
||||||
|
let node_private = StaticSecret::from(node_bytes);
|
||||||
|
let node_public = PublicKey::from(&node_private);
|
||||||
|
|
||||||
|
let disco_private = StaticSecret::from(disco_bytes);
|
||||||
|
let disco_public = PublicKey::from(&disco_private);
|
||||||
|
|
||||||
|
let wg_private = StaticSecret::from(wg_bytes);
|
||||||
|
let wg_public = PublicKey::from(&wg_private);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
node_private,
|
||||||
|
node_public,
|
||||||
|
disco_private,
|
||||||
|
disco_public,
|
||||||
|
wg_private,
|
||||||
|
wg_public,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_key_hex(s: &str, name: &str) -> crate::Result<[u8; 32]> {
|
||||||
|
let bytes = hex::decode(s).map_err(|e| crate::Error::Other(format!("bad {name} key hex: {e}")))?;
|
||||||
|
let arr: [u8; 32] = bytes
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| crate::Error::Other(format!("{name} key must be 32 bytes")))?;
|
||||||
|
Ok(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_produces_valid_keys() {
|
||||||
|
let keys = NodeKeys::generate();
|
||||||
|
assert!(keys.node_key_str().starts_with("nodekey:"));
|
||||||
|
assert!(keys.disco_key_str().starts_with("discokey:"));
|
||||||
|
assert_eq!(keys.node_key_str().len(), "nodekey:".len() + 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_or_generate_round_trips() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let keys1 = NodeKeys::load_or_generate(dir.path()).unwrap();
|
||||||
|
let keys2 = NodeKeys::load_or_generate(dir.path()).unwrap();
|
||||||
|
assert_eq!(keys1.node_key_str(), keys2.node_key_str());
|
||||||
|
assert_eq!(keys1.disco_key_str(), keys2.disco_key_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hex_round_trip() {
|
||||||
|
let input = [0xde, 0xad, 0xbe, 0xef];
|
||||||
|
let encoded = super::hex::encode(&input);
|
||||||
|
assert_eq!(encoded, "deadbeef");
|
||||||
|
let decoded = super::hex::decode(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
sunbeam-net/src/lib.rs
Normal file
9
sunbeam-net/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//! sunbeam-net: Pure Rust Headscale/Tailscale-compatible VPN client.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
|
pub mod keys;
|
||||||
|
pub(crate) mod proto;
|
||||||
|
|
||||||
|
pub use config::VpnConfig;
|
||||||
|
pub use error::{Error, Result};
|
||||||
1
sunbeam-net/src/proto/mod.rs
Normal file
1
sunbeam-net/src/proto/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod types;
|
||||||
434
sunbeam-net/src/proto/types.rs
Normal file
434
sunbeam-net/src/proto/types.rs
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Registration request sent to POST /machine/register.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
pub version: u16,
|
||||||
|
pub node_key: String,
|
||||||
|
pub old_node_key: String,
|
||||||
|
pub auth: Option<AuthInfo>,
|
||||||
|
pub hostinfo: HostInfo,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub followup: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timestamp: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct AuthInfo {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auth_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct HostInfo {
|
||||||
|
#[serde(rename = "GoArch")]
|
||||||
|
pub go_arch: String,
|
||||||
|
#[serde(rename = "GoOS")]
|
||||||
|
pub go_os: String,
|
||||||
|
#[serde(rename = "GoVersion")]
|
||||||
|
pub go_version: String,
|
||||||
|
pub hostname: String,
|
||||||
|
#[serde(rename = "OS")]
|
||||||
|
pub os: String,
|
||||||
|
#[serde(rename = "OSVersion")]
|
||||||
|
pub os_version: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub device_model: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub frontend_log_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub backend_log_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registration response from POST /machine/register.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct RegisterResponse {
|
||||||
|
#[serde(default)]
|
||||||
|
pub user: User,
|
||||||
|
#[serde(default)]
|
||||||
|
pub login: Login,
|
||||||
|
#[serde(default)]
|
||||||
|
pub node_key_expired: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub machine_authorized: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auth_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct User {
|
||||||
|
#[serde(rename = "ID", default)]
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub login_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct Login {
|
||||||
|
#[serde(rename = "ID", default)]
|
||||||
|
pub id: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub login_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map request sent to POST /machine/map.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct MapRequest {
|
||||||
|
pub version: u16,
|
||||||
|
pub node_key: String,
|
||||||
|
pub disco_key: String,
|
||||||
|
pub stream: bool,
|
||||||
|
pub hostinfo: HostInfo,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub endpoints: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map response -- can be a full snapshot or a delta update.
|
||||||
|
/// Fields are all optional because deltas only include changed fields.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct MapResponse {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub node: Option<Node>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub peers: Option<Vec<Node>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub peers_changed: Option<Vec<Node>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub peers_removed: Option<Vec<String>>,
|
||||||
|
#[serde(rename = "DERPMap")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub derp_map: Option<DerpMap>,
|
||||||
|
#[serde(rename = "DNSConfig")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub dns_config: Option<DnsConfig>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub packet_filter: Option<Vec<FilterRule>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub domain: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub collection_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct Node {
|
||||||
|
#[serde(rename = "ID")]
|
||||||
|
pub id: u64,
|
||||||
|
pub key: String,
|
||||||
|
pub disco_key: String,
|
||||||
|
pub addresses: Vec<String>,
|
||||||
|
#[serde(rename = "AllowedIPs")]
|
||||||
|
pub allowed_ips: Vec<String>,
|
||||||
|
pub endpoints: Vec<String>,
|
||||||
|
#[serde(rename = "DERP")]
|
||||||
|
pub derp: String,
|
||||||
|
pub hostinfo: HostInfo,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub online: Option<bool>,
|
||||||
|
pub machine_authorized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct DerpMap {
|
||||||
|
pub regions: HashMap<String, DerpRegion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct DerpRegion {
|
||||||
|
pub region_id: u16,
|
||||||
|
pub region_code: String,
|
||||||
|
pub region_name: String,
|
||||||
|
pub nodes: Vec<DerpNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct DerpNode {
|
||||||
|
pub name: String,
|
||||||
|
pub region_id: u16,
|
||||||
|
pub host_name: String,
|
||||||
|
#[serde(rename = "IPv4")]
|
||||||
|
pub ipv4: String,
|
||||||
|
#[serde(rename = "IPv6")]
|
||||||
|
pub ipv6: String,
|
||||||
|
pub derp_port: u16,
|
||||||
|
pub stun_port: i32,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub stun_only: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct DnsConfig {
|
||||||
|
pub resolvers: Vec<DnsResolver>,
|
||||||
|
pub domains: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct DnsResolver {
|
||||||
|
pub addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct FilterRule {
|
||||||
|
#[serde(rename = "SrcIPs")]
|
||||||
|
pub src_ips: Vec<String>,
|
||||||
|
#[serde(rename = "DstPorts")]
|
||||||
|
pub dst_ports: Vec<FilterPort>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct FilterPort {
|
||||||
|
#[serde(rename = "IP")]
|
||||||
|
pub ip: String,
|
||||||
|
pub ports: PortRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase", default)]
|
||||||
|
pub struct PortRange {
|
||||||
|
pub first: u16,
|
||||||
|
pub last: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sample_hostinfo() -> HostInfo {
|
||||||
|
HostInfo {
|
||||||
|
go_arch: "arm64".into(),
|
||||||
|
go_os: "linux".into(),
|
||||||
|
go_version: "sunbeam-net/0.1".into(),
|
||||||
|
hostname: "myhost".into(),
|
||||||
|
os: "linux".into(),
|
||||||
|
os_version: "6.1".into(),
|
||||||
|
device_model: None,
|
||||||
|
frontend_log_id: None,
|
||||||
|
backend_log_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_register_request_serialize() {
|
||||||
|
let req = RegisterRequest {
|
||||||
|
version: 74,
|
||||||
|
node_key: "nodekey:aabb".into(),
|
||||||
|
old_node_key: "".into(),
|
||||||
|
auth: Some(AuthInfo {
|
||||||
|
auth_key: Some("tskey-abc".into()),
|
||||||
|
}),
|
||||||
|
hostinfo: sample_hostinfo(),
|
||||||
|
followup: None,
|
||||||
|
timestamp: None,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&req).unwrap();
|
||||||
|
// Verify PascalCase keys
|
||||||
|
assert!(json.contains("\"Version\""));
|
||||||
|
assert!(json.contains("\"NodeKey\""));
|
||||||
|
assert!(json.contains("\"OldNodeKey\""));
|
||||||
|
assert!(json.contains("\"Hostinfo\""));
|
||||||
|
assert!(json.contains("\"GoArch\""));
|
||||||
|
assert!(json.contains("\"GoOS\""));
|
||||||
|
assert!(json.contains("\"GoVersion\""));
|
||||||
|
assert!(json.contains("\"AuthKey\""));
|
||||||
|
// Optional None fields should be absent
|
||||||
|
assert!(!json.contains("\"Followup\""));
|
||||||
|
assert!(!json.contains("\"Timestamp\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_register_response_deserialize() {
|
||||||
|
let json = r#"{
|
||||||
|
"User": {"ID": 1, "LoginName": "user@example.com", "DisplayName": "User"},
|
||||||
|
"Login": {"ID": 2, "LoginName": "user@example.com", "DisplayName": "User"},
|
||||||
|
"NodeKeyExpired": false,
|
||||||
|
"MachineAuthorized": true,
|
||||||
|
"AuthUrl": "https://login.example.com/a/xyz"
|
||||||
|
}"#;
|
||||||
|
let resp: RegisterResponse = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(resp.user.id, 1);
|
||||||
|
assert_eq!(resp.login.id, 2);
|
||||||
|
assert!(!resp.node_key_expired);
|
||||||
|
assert!(resp.machine_authorized);
|
||||||
|
assert_eq!(resp.auth_url.as_deref(), Some("https://login.example.com/a/xyz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_response_full_snapshot() {
|
||||||
|
let json = r#"{
|
||||||
|
"Node": {
|
||||||
|
"ID": 1,
|
||||||
|
"Key": "nodekey:aa",
|
||||||
|
"DiscoKey": "discokey:bb",
|
||||||
|
"Addresses": ["100.64.0.1/32"],
|
||||||
|
"AllowedIPs": ["100.64.0.0/10"],
|
||||||
|
"Endpoints": [],
|
||||||
|
"DERP": "127.3.3.40:1",
|
||||||
|
"Hostinfo": {
|
||||||
|
"GoArch": "arm64", "GoOS": "linux", "GoVersion": "sunbeam-net/0.1",
|
||||||
|
"Hostname": "self", "OS": "linux", "OSVersion": "6.1"
|
||||||
|
},
|
||||||
|
"Name": "self.example.com",
|
||||||
|
"Online": true,
|
||||||
|
"MachineAuthorized": true
|
||||||
|
},
|
||||||
|
"Peers": [{
|
||||||
|
"ID": 2,
|
||||||
|
"Key": "nodekey:cc",
|
||||||
|
"DiscoKey": "discokey:dd",
|
||||||
|
"Addresses": ["100.64.0.2/32"],
|
||||||
|
"AllowedIPs": ["100.64.0.2/32"],
|
||||||
|
"DERP": "127.3.3.40:1",
|
||||||
|
"Hostinfo": {
|
||||||
|
"GoArch": "amd64", "GoOS": "linux", "GoVersion": "sunbeam-net/0.1",
|
||||||
|
"Hostname": "peer", "OS": "linux", "OSVersion": "6.1"
|
||||||
|
},
|
||||||
|
"Name": "peer.example.com",
|
||||||
|
"Online": true,
|
||||||
|
"MachineAuthorized": true
|
||||||
|
}],
|
||||||
|
"DERPMap": {
|
||||||
|
"Regions": {
|
||||||
|
"1": {
|
||||||
|
"RegionId": 1,
|
||||||
|
"RegionCode": "default",
|
||||||
|
"RegionName": "Default",
|
||||||
|
"Nodes": [{
|
||||||
|
"Name": "1a",
|
||||||
|
"RegionId": 1,
|
||||||
|
"HostName": "derp.example.com",
|
||||||
|
"IPv4": "1.2.3.4",
|
||||||
|
"IPv6": "::1",
|
||||||
|
"DerpPort": 443,
|
||||||
|
"StunPort": 3478
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DNSConfig": {
|
||||||
|
"Resolvers": [{"Addr": "100.64.0.1"}],
|
||||||
|
"Domains": ["example.com"]
|
||||||
|
},
|
||||||
|
"Domain": "example.com"
|
||||||
|
}"#;
|
||||||
|
let resp: MapResponse = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(resp.node.is_some());
|
||||||
|
let peers = resp.peers.as_ref().unwrap();
|
||||||
|
assert_eq!(peers.len(), 1);
|
||||||
|
assert_eq!(peers[0].key, "nodekey:cc");
|
||||||
|
let derp_map = resp.derp_map.as_ref().unwrap();
|
||||||
|
assert!(derp_map.regions.contains_key("1"));
|
||||||
|
let dns = resp.dns_config.as_ref().unwrap();
|
||||||
|
assert_eq!(dns.resolvers.len(), 1);
|
||||||
|
assert_eq!(dns.domains, vec!["example.com"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_response_delta() {
|
||||||
|
let json = r#"{
|
||||||
|
"PeersChanged": [{
|
||||||
|
"ID": 3,
|
||||||
|
"Key": "nodekey:ee",
|
||||||
|
"DiscoKey": "discokey:ff",
|
||||||
|
"Addresses": ["100.64.0.3/32"],
|
||||||
|
"AllowedIPs": ["100.64.0.3/32"],
|
||||||
|
"DERP": "127.3.3.40:1",
|
||||||
|
"Hostinfo": {
|
||||||
|
"GoArch": "amd64", "GoOS": "linux", "GoVersion": "sunbeam-net/0.1",
|
||||||
|
"Hostname": "new-peer", "OS": "linux", "OSVersion": "6.1"
|
||||||
|
},
|
||||||
|
"Name": "new-peer.example.com",
|
||||||
|
"MachineAuthorized": true
|
||||||
|
}],
|
||||||
|
"PeersRemoved": ["nodekey:cc"]
|
||||||
|
}"#;
|
||||||
|
let resp: MapResponse = serde_json::from_str(json).unwrap();
|
||||||
|
assert!(resp.node.is_none());
|
||||||
|
assert!(resp.peers.is_none());
|
||||||
|
let changed = resp.peers_changed.as_ref().unwrap();
|
||||||
|
assert_eq!(changed.len(), 1);
|
||||||
|
assert_eq!(changed[0].key, "nodekey:ee");
|
||||||
|
let removed = resp.peers_removed.as_ref().unwrap();
|
||||||
|
assert_eq!(removed, &["nodekey:cc"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_response_keepalive() {
|
||||||
|
let resp: MapResponse = serde_json::from_str("{}").unwrap();
|
||||||
|
assert!(resp.node.is_none());
|
||||||
|
assert!(resp.peers.is_none());
|
||||||
|
assert!(resp.peers_changed.is_none());
|
||||||
|
assert!(resp.peers_removed.is_none());
|
||||||
|
assert!(resp.derp_map.is_none());
|
||||||
|
assert!(resp.dns_config.is_none());
|
||||||
|
assert!(resp.packet_filter.is_none());
|
||||||
|
assert!(resp.domain.is_none());
|
||||||
|
assert!(resp.collection_name.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_node_serde_round_trip() {
|
||||||
|
let node = Node {
|
||||||
|
id: 42,
|
||||||
|
key: "nodekey:abcd".into(),
|
||||||
|
disco_key: "discokey:1234".into(),
|
||||||
|
addresses: vec!["100.64.0.5/32".into()],
|
||||||
|
allowed_ips: vec!["100.64.0.0/10".into()],
|
||||||
|
endpoints: vec!["1.2.3.4:41641".into()],
|
||||||
|
derp: "127.3.3.40:1".into(),
|
||||||
|
hostinfo: sample_hostinfo(),
|
||||||
|
name: "test.example.com".into(),
|
||||||
|
online: Some(true),
|
||||||
|
machine_authorized: true,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&node).unwrap();
|
||||||
|
let back: Node = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(back.id, 42);
|
||||||
|
assert_eq!(back.key, "nodekey:abcd");
|
||||||
|
assert_eq!(back.disco_key, "discokey:1234");
|
||||||
|
assert_eq!(back.derp, "127.3.3.40:1");
|
||||||
|
assert_eq!(back.name, "test.example.com");
|
||||||
|
assert_eq!(back.online, Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hostinfo_platform_fields() {
|
||||||
|
let hi = sample_hostinfo();
|
||||||
|
let json = serde_json::to_string(&hi).unwrap();
|
||||||
|
// Verify the explicit renames
|
||||||
|
assert!(json.contains("\"GoArch\""));
|
||||||
|
assert!(json.contains("\"GoOS\""));
|
||||||
|
assert!(json.contains("\"GoVersion\""));
|
||||||
|
assert!(json.contains("\"OS\""));
|
||||||
|
assert!(json.contains("\"OSVersion\""));
|
||||||
|
// Verify PascalCase on normal fields
|
||||||
|
assert!(json.contains("\"Hostname\""));
|
||||||
|
// Should not contain snake_case
|
||||||
|
assert!(!json.contains("go_arch"));
|
||||||
|
assert!(!json.contains("go_os"));
|
||||||
|
assert!(!json.contains("os_version"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user