refactor!: use rtnetlink for client interfaces, add deregistration

This commit is contained in:
2026-02-17 22:59:50 -08:00
parent a022c18ff9
commit 03f38b9ee3
20 changed files with 975 additions and 167 deletions

345
Cargo.lock generated
View File

@@ -8,7 +8,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@@ -29,8 +29,8 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"base64", "base64 0.22.1",
"bitflags", "bitflags 2.10.0",
"brotli", "brotli",
"bytes", "bytes",
"bytestring", "bytestring",
@@ -229,6 +229,12 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "anyhow"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.8.1" version = "1.8.1"
@@ -287,12 +293,24 @@ dependencies = [
"fastrand", "fastrand",
] ]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[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.10.0" version = "2.10.0"
@@ -400,13 +418,17 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
name = "client" name = "client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1",
"console", "console",
"dirs", "dirs",
"futures", "futures",
"futures-util", "futures-util",
"ipnetwork", "ipnetwork",
"libc",
"netlink-packet-route 0.19.0",
"registry", "registry",
"reqwest", "reqwest",
"rtnetlink",
"serde", "serde",
"serde_json", "serde_json",
"stunclient", "stunclient",
@@ -418,6 +440,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
"wireguard-control",
] ]
[[package]] [[package]]
@@ -545,6 +568,32 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.10.0" version = "2.10.0"
@@ -675,6 +724,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -888,6 +943,12 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.12.1" version = "0.12.1"
@@ -997,7 +1058,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",
@@ -1231,7 +1292,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
"libc", "libc",
] ]
@@ -1300,6 +1361,24 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -1334,6 +1413,164 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577"
[[package]]
name = "netlink-packet-core"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
dependencies = [
"anyhow",
"byteorder",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-generic"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b"
dependencies = [
"anyhow",
"byteorder",
"netlink-packet-core",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-route"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74c171cd77b4ee8c7708da746ce392440cb7bcf618d122ec9ecc607b12938bf4"
dependencies = [
"anyhow",
"byteorder",
"libc",
"log",
"netlink-packet-core",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-route"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e"
dependencies = [
"anyhow",
"bitflags 2.10.0",
"byteorder",
"libc",
"log",
"netlink-packet-core",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-utils"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
dependencies = [
"anyhow",
"byteorder",
"paste",
"thiserror 1.0.69",
]
[[package]]
name = "netlink-packet-wireguard"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b25b050ff1f6a1e23c6777b72db22790fe5b6b5ccfd3858672587a79876c8f"
dependencies = [
"anyhow",
"byteorder",
"libc",
"log",
"netlink-packet-generic",
"netlink-packet-utils",
]
[[package]]
name = "netlink-proto"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60"
dependencies = [
"bytes",
"futures",
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.18",
]
[[package]]
name = "netlink-request"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d83636acdf5aba609ff27a66f6a235f1bcfafe20ccdaad6a5bf9d7dfa64a811"
dependencies = [
"netlink-packet-core",
"netlink-packet-generic",
"netlink-packet-route 0.21.0",
"netlink-packet-utils",
"netlink-sys",
"nix 0.25.1",
"once_cell",
]
[[package]]
name = "netlink-sys"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae"
dependencies = [
"bytes",
"futures-util",
"libc",
"log",
"tokio",
]
[[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",
"memoffset 0.6.5",
"pin-utils",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset 0.9.1",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -1418,6 +1655,12 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -1573,7 +1816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha",
"rand_core", "rand_core 0.9.5",
] ]
[[package]] [[package]]
@@ -1583,7 +1826,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.9.5",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
] ]
[[package]] [[package]]
@@ -1628,7 +1880,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.10.0",
] ]
[[package]] [[package]]
@@ -1683,7 +1935,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"actix-ws", "actix-ws",
"base64", "base64 0.22.1",
"console", "console",
"futures", "futures",
"futures-util", "futures-util",
@@ -1698,6 +1950,7 @@ dependencies = [
"tracing", "tracing",
"tracing-actix-web", "tracing-actix-web",
"tracing-subscriber", "tracing-subscriber",
"wireguard-control",
] ]
[[package]] [[package]]
@@ -1706,7 +1959,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",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -1754,6 +2007,24 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rtnetlink"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b684475344d8df1859ddb2d395dd3dac4f8f3422a1aa0725993cb375fc5caba5"
dependencies = [
"futures",
"log",
"netlink-packet-core",
"netlink-packet-route 0.19.0",
"netlink-packet-utils",
"netlink-proto",
"netlink-sys",
"nix 0.27.1",
"thiserror 1.0.69",
"tokio",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.1" version = "2.1.1"
@@ -1887,7 +2158,7 @@ version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -2141,7 +2412,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"system-configuration-sys", "system-configuration-sys",
] ]
@@ -2410,7 +2681,7 @@ 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 = [
"bitflags", "bitflags 2.10.0",
"bytes", "bytes",
"futures-util", "futures-util",
"http 1.4.0", "http 1.4.0",
@@ -3030,6 +3301,28 @@ version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
[[package]]
name = "wireguard-control"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a1ff6cde8cce93098564c1020e59da217c87e37b9f1bac3f539a6261a9af128"
dependencies = [
"base64 0.13.1",
"hex",
"libc",
"log",
"netlink-packet-core",
"netlink-packet-generic",
"netlink-packet-route 0.21.0",
"netlink-packet-utils",
"netlink-packet-wireguard",
"netlink-request",
"netlink-sys",
"nix 0.30.1",
"rand_core 0.6.4",
"x25519-dalek",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"
@@ -3042,6 +3335,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 = "xxhash-rust" name = "xxhash-rust"
version = "0.8.15" version = "0.8.15"
@@ -3117,6 +3422,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.114",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"

View File

@@ -11,7 +11,7 @@ serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
thiserror = "2.0.18" thiserror = "2.0.18"
thiserror-ext = "0.3.0" thiserror-ext = "0.3.0"
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.49.0", features = ["macros", "net", "rt-multi-thread", "signal"] }
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] } tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
tracing = "0.1.44" tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
@@ -22,3 +22,12 @@ url = "2.5.8"
reqwest = { version = "0.13.2", features = ["json"] } reqwest = { version = "0.13.2", features = ["json"] }
stunclient = "0.4.2" stunclient = "0.4.2"
ipnetwork = { version = "0.21.1", features = ["serde"] } ipnetwork = { version = "0.21.1", features = ["serde"] }
base64 = "0.22.1"
libc = "0.2"
# WireGuard netlink control
wireguard-control = "1.1"
# Network interface management
rtnetlink = "0.14"
netlink-packet-route = "0.19"

View File

@@ -1,22 +0,0 @@
use std::{ops::Deref, sync::Arc};
#[derive(Clone)]
pub struct AppState {
pub reqwest_client: reqwest::Client,
}
#[derive(Clone)]
pub struct Data<T>(Arc<T>);
impl<T> Deref for Data<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> Data<T> {
pub fn new(inner: T) -> Self {
Self(Arc::new(inner))
}
}

View File

@@ -4,19 +4,33 @@ use crate::error::{Error, Result};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use serde::Deserialize; use serde::Deserialize;
pub const DEFAULT_KEEPALIVE: u16 = 25;
pub const INTERFACE_NAME: &str = "mesh0";
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Config { pub struct Config {
pub interface: InterfaceConfig, pub interface: InterfaceConfig,
pub server: ServerConfig, pub server: ServerConfig,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct InterfaceConfig { pub struct InterfaceConfig {
pub private_key: String, pub private_key: String,
pub public_key: String, pub public_key: String,
#[serde(default = "default_listen_port")]
pub listen_port: u16, pub listen_port: u16,
pub address: String, #[serde(default = "default_keepalive")]
pub allowed_ips: Vec<IpNetwork>, pub persistent_keepalive: u16,
pub allowed_ips: Option<Vec<IpNetwork>>,
} }
fn default_listen_port() -> u16 {
51820
}
fn default_keepalive() -> u16 {
DEFAULT_KEEPALIVE
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ServerConfig { pub struct ServerConfig {
pub ws_url: String, pub ws_url: String,
@@ -36,7 +50,3 @@ pub fn base_path() -> PathBuf {
.unwrap_or_else(|| PathBuf::from(".")) .unwrap_or_else(|| PathBuf::from("."))
.join("wg-mesh") .join("wg-mesh")
} }
pub fn wg_config_path() -> PathBuf {
PathBuf::from("/etc/wireguard/mesh0.conf")
}

View File

@@ -29,12 +29,6 @@ pub enum ErrorKind {
Url(#[from] url::ParseError), Url(#[from] url::ParseError),
#[error("invalid url scheme: {url}")] #[error("invalid url scheme: {url}")]
UrlScheme { url: String }, UrlScheme { url: String },
#[error("error writing wireguard config to {path}")]
WriteConfig {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("STUN discovery failed: {message}")] #[error("STUN discovery failed: {message}")]
StunDiscovery { message: String }, StunDiscovery { message: String },
#[error("HTTP IP discovery failed")] #[error("HTTP IP discovery failed")]
@@ -45,6 +39,53 @@ pub enum ErrorKind {
DiscoveryFailed, DiscoveryFailed,
#[error("error with request")] #[error("error with request")]
Reqwest(#[from] reqwest::Error), Reqwest(#[from] reqwest::Error),
#[error("invalid base64 key: {context}")]
InvalidKey { context: String },
#[error("error creating WireGuard interface {interface}")]
CreateInterface {
interface: String,
#[source]
source: std::io::Error,
},
#[error("error configuring WireGuard device {interface}")]
ConfigureDevice {
interface: String,
#[source]
source: std::io::Error,
},
#[error("error with netlink operation: {context}")]
Netlink { context: String },
#[error("error setting interface address")]
SetAddress {
#[source]
source: rtnetlink::Error,
},
#[error("error bringing interface up")]
SetLinkUp {
#[source]
source: rtnetlink::Error,
},
#[error("error getting interface index for {interface}")]
GetInterface { interface: String },
#[error("registration failed")]
Registration {
#[source]
source: reqwest::Error,
},
#[error("error deregistering device {public_key}")]
Deregister {
public_key: String,
#[source]
source: reqwest::Error,
},
#[error("registration failed with status {status}: {body}")]
RegistrationStatus { status: u16, body: String },
#[error("error deleting interface")]
DeleteInterface {
name: String,
#[source]
source: rtnetlink::Error,
},
} }
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;

View File

@@ -1,27 +1,29 @@
mod app_state;
mod config; mod config;
mod discovery; mod discovery;
mod error; mod error;
mod netlink;
mod wireguard; mod wireguard;
use std::{fs::OpenOptions, io::Write, net::IpAddr, os::unix::fs::OpenOptionsExt}; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use console::style; use console::style;
use futures::StreamExt; use futures::{StreamExt, TryFutureExt, stream::SplitStream};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use registry::{Peer, PeerMessage}; use registry::{Peer, PeerMessage, RegisterResponse};
use serde::Serialize; use serde::Serialize;
use thiserror_ext::AsReport; use thiserror_ext::AsReport;
use tokio_tungstenite::tungstenite::Message; use tokio::{
net::TcpStream,
signal::{self, unix::SignalKind},
};
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite::Message};
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing_subscriber::{ use tracing_subscriber::{
EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt,
}; };
use url::Url;
use crate::{ use crate::{
app_state::{AppState, Data}, config::{Config, INTERFACE_NAME},
config::{Config, InterfaceConfig, wg_config_path},
discovery::{PublicEndpoint, discover_public_endpoint}, discovery::{PublicEndpoint, discover_public_endpoint},
error::{Error, Result}, error::{Error, Result},
}; };
@@ -34,105 +36,209 @@ pub struct RegisterRequest {
pub allowed_ips: Vec<IpNetwork>, pub allowed_ips: Vec<IpNetwork>,
} }
fn parse_ws_url(input: &Url) -> Result<String> {
let url = input.join("/ws/peers")?;
if url.scheme() != "ws" && url.scheme() != "wss" {
return Err(Error::url_scheme(url.to_string()));
}
Ok(url.to_string())
}
fn write_wg_config(interface: &InterfaceConfig, peers: &[Peer]) -> Result<()> {
let path = wg_config_path();
let config = wireguard::generate_config(interface, peers);
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&path)
.map_err(|e| Error::write_config(e, &path))?;
file.write_all(config.as_bytes())
.map_err(|e| Error::write_config(e, &path))?;
tracing::info!("wrote {} with {} peers", path.display(), peers.len());
Ok(())
}
async fn register_self( async fn register_self(
app_state: Data<AppState>, client: &reqwest::Client,
endpoint: &PublicEndpoint, endpoint: &PublicEndpoint,
config: &InterfaceConfig, config: &Config,
url: &str, ) -> Result<Ipv4Addr> {
) -> Result<()> { let url = format!("{}/register", &config.server.url);
app_state
.reqwest_client tracing::info!(
.post(url) public_ip = %endpoint.ip,
port = endpoint.port,
"registering with registry"
);
let response = client
.post(&url)
.json(&RegisterRequest { .json(&RegisterRequest {
public_key: config.public_key.clone(), public_key: config.interface.public_key.clone(),
public_ip: endpoint.ip, public_ip: endpoint.ip,
port: endpoint.port.to_string(), port: endpoint.port.to_string(),
allowed_ips: config.allowed_ips.clone(), allowed_ips: config.interface.allowed_ips.clone().unwrap_or(vec![]),
}) })
.send() .send()
.await
.map_err(Error::registration)?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
return Err(Error::registration_status(status, body));
}
let register_response: RegisterResponse = response.json().await.map_err(Error::registration)?;
tracing::info!(
mesh_ip = %register_response.mesh_ip,
"registration successful, assigned mesh IP"
);
Ok(register_response.mesh_ip)
}
async fn deregister_self(client: &reqwest::Client, config: &Config) -> Result<()> {
let url = format!(
"{}/deregister?public_key={}",
config.server.url, config.interface.public_key
);
client
.delete(&url)
.send()
.await? .await?
.error_for_status()?; .error_for_status()
.map_err(|e| Error::deregister(e, &config.interface.public_key))?;
Ok(()) Ok(())
} }
async fn run() -> crate::error::Result<()> { fn configure_peer(peer: &Peer, local_public_key: &str, keepalive: u16) -> Result<()> {
let config = Config::load()?; if peer.public_key == local_public_key {
let ws_url = &config.server.ws_url; tracing::debug!(peer_key = %peer.public_key, "skipping self");
let (ws_stream, response) = tokio_tungstenite::connect_async(ws_url) return Ok(());
.await }
.map_err(|e| Error::ws_connect(e, ws_url))?;
let (_, mut read) = ws_stream.split();
let app_state = Data::new(AppState {
reqwest_client: reqwest::Client::new(),
});
tracing::info!("connected, response: {:?}", response.status());
let endpoint = discover_public_endpoint(config.interface.listen_port).await?;
register_self(
app_state.clone(),
&endpoint,
&config.interface,
&format!("{}/register", &config.server.url),
)
.await?;
let mut peers: Vec<Peer> = Vec::new(); let peer_key = wireguard::parse_key(&peer.public_key)?;
let endpoint: SocketAddr = format!("{}:{}", peer.public_ip, peer.port)
.parse()
.map_err(|_| {
Error::netlink(format!(
"invalid endpoint: {}:{}",
peer.public_ip, peer.port
))
})?;
let mut allowed_ips: Vec<IpNetwork> =
vec![IpNetwork::new(std::net::IpAddr::V4(peer.mesh_ip), 32).expect("valid network")];
allowed_ips.extend(peer.allowed_ips.iter().cloned());
wireguard::configure_peer(&peer_key, Some(endpoint), &allowed_ips, Some(keepalive))?;
tracing::info!(
peer_key = %peer.public_key,
mesh_ip = %peer.mesh_ip,
endpoint = %endpoint,
"configured peer"
);
Ok(())
}
fn configure_all_peers(peers: &[Peer], local_public_key: &str, keepalive: u16) -> Result<()> {
for peer in peers {
if let Err(e) = configure_peer(peer, local_public_key, keepalive) {
tracing::warn!(peer_key = %peer.public_key, error = %e, "failed to configure peer");
}
}
Ok(())
}
async fn events(
read: &mut SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
config: &Config,
) -> Result<()> {
while let Some(msg) = read.next().await { while let Some(msg) = read.next().await {
match msg.map_err(|e| Error::ws_read(e))? { match msg.map_err(Error::ws_read)? {
Message::Text(text) => { Message::Text(text) => {
let server_msg: PeerMessage = let server_msg: PeerMessage =
serde_json::from_str(&text).map_err(|e| Error::deserialize_json(e))?; serde_json::from_str(&text).map_err(Error::deserialize_json)?;
match server_msg { match server_msg {
PeerMessage::HydratePeers { peers: new_peers } => { PeerMessage::HydratePeers { peers } => {
tracing::info!("received {} peers", new_peers.len()); tracing::info!(count = peers.len(), "Received initial peer list");
peers = new_peers; configure_all_peers(
write_wg_config(&config.interface, &peers)?; &peers,
&config.interface.public_key,
config.interface.persistent_keepalive,
)?;
} }
PeerMessage::PeerUpdate { peer } => { PeerMessage::PeerUpdate { peer } => {
tracing::info!("peer update: {}", peer.public_key); tracing::info!(
if let Some(existing) = peer_key = %peer.public_key,
peers.iter_mut().find(|p| p.public_key == peer.public_key) mesh_ip = %peer.mesh_ip,
{ "Received peer update"
*existing = peer; );
} else { configure_peer(
peers.push(peer); &peer,
} &config.interface.public_key,
write_wg_config(&config.interface, &peers)?; config.interface.persistent_keepalive,
)?;
} }
} }
} }
Message::Ping(_) => {}
Message::Pong(_) => {}
Message::Close(_) => {
tracing::warn!("connection closed by server");
break;
}
_ => {} _ => {}
} }
} }
Ok(()) Ok(())
} }
async fn run() -> Result<()> {
let config = Config::load()?;
let private_key = wireguard::parse_key(&config.interface.private_key)?;
let endpoint = discover_public_endpoint(config.interface.listen_port).await?;
tracing::info!(
public_ip = %endpoint.ip,
public_port = endpoint.port,
"public endpoint"
);
let http_client = reqwest::Client::new();
let mesh_ip = register_self(&http_client, &endpoint, &config).await?;
wireguard::create_interface()?;
wireguard::configure_interface(&private_key, config.interface.listen_port)?;
let (nl_handle, _nl_task) = netlink::connect().await?;
netlink::add_address(&nl_handle, mesh_ip, 32).await?;
netlink::set_link_up(&nl_handle).await?;
tracing::info!(
interface = INTERFACE_NAME,
mesh_ip = %mesh_ip,
"interface configured and up"
);
let ws_url = &config.server.ws_url;
let (ws_stream, response) = tokio_tungstenite::connect_async(ws_url)
.await
.map_err(|e| Error::ws_connect(e, ws_url))?;
tracing::info!(
status = ?response.status(),
"connected to registry WebSocket"
);
let (_, mut read) = ws_stream.split();
tokio::select! {
receiver = events(&mut read, &config) => receiver?,
_ = signal::ctrl_c() => {
},
_ = on_shutdown() => {}
};
tracing::debug!("gracefully shutting down");
netlink::delete_interface(&nl_handle, INTERFACE_NAME).await?;
deregister_self(&http_client, &config).await?;
tracing::info!("connection closed");
Ok(())
}
async fn on_shutdown() {
use tokio::signal::unix::signal;
let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM");
let mut sigint = signal(SignalKind::interrupt()).expect("failed to register SIGINT");
tokio::select! {
_ = sigterm.recv() => tracing::info!("received sigterm"),
_ = sigint.recv() => tracing::info!("received sigint")
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let tracing_env_filter = EnvFilter::builder() let tracing_env_filter = EnvFilter::builder()

84
client/src/netlink.rs Normal file
View File

@@ -0,0 +1,84 @@
use std::net::Ipv4Addr;
use futures::TryStreamExt;
use rtnetlink::Handle;
use crate::config::INTERFACE_NAME;
use crate::error::{Error, Result};
async fn get_interface_index(handle: &Handle, name: &str) -> Result<u32> {
let mut links = handle.link().get().match_name(name.to_string()).execute();
if let Some(link) = links.try_next().await.map_err(Error::set_link_up)? {
Ok(link.header.index)
} else {
Err(Error::get_interface(name.to_string()))
}
}
pub async fn add_address(handle: &Handle, addr: Ipv4Addr, prefix_len: u8) -> Result<()> {
let index = get_interface_index(handle, INTERFACE_NAME).await?;
tracing::debug!(
interface = INTERFACE_NAME,
address = %addr,
prefix_len,
"adding address to interface"
);
match handle
.address()
.add(index, std::net::IpAddr::V4(addr), prefix_len)
.execute()
.await
{
Ok(()) => Ok(()),
Err(rtnetlink::Error::NetlinkError(e)) if e.raw_code() == -libc::EEXIST => {
tracing::debug!(
interface = INTERFACE_NAME,
address = %addr,
"address already exists on interface"
);
Ok(())
}
Err(e) => Err(Error::set_address(e)),
}
}
pub async fn set_link_up(handle: &Handle) -> Result<()> {
let index = get_interface_index(handle, INTERFACE_NAME).await?;
tracing::info!(interface = INTERFACE_NAME, "set interface up");
handle
.link()
.set(index)
.up()
.execute()
.await
.map_err(Error::set_link_up)?;
Ok(())
}
pub async fn delete_interface(handle: &Handle, name: &str) -> Result<()> {
let index = get_interface_index(&handle, name).await?;
handle
.link()
.del(index)
.execute()
.await
.map_err(|e| Error::delete_interface(e, name))?;
Ok(())
}
pub async fn connect() -> Result<(Handle, tokio::task::JoinHandle<()>)> {
let (connection, handle, _) = rtnetlink::new_connection()
.map_err(|e| Error::netlink(format!("failed to create netlink connection: {}", e)))?;
let join_handle = tokio::spawn(connection);
Ok((handle, join_handle))
}

View File

@@ -1,24 +1,117 @@
use registry::Peer; use std::net::SocketAddr;
use crate::config::InterfaceConfig; use ipnetwork::IpNetwork;
use wireguard_control::{
AllowedIp, Backend, Device, DeviceUpdate, InterfaceName, InvalidKey, Key, PeerConfigBuilder,
};
pub fn generate_config(interface: &InterfaceConfig, peers: &[Peer]) -> String { use crate::config::INTERFACE_NAME;
let mut config = format!( use crate::error::{Error, Result};
"[Interface]\nPrivateKey = {}\nListenPort = {}\nAddress = {}\n",
interface.private_key, interface.listen_port, interface.address, pub fn parse_key(key_b64: &str) -> Result<Key> {
); Key::from_base64(key_b64).map_err(|e: InvalidKey| Error::invalid_key(e.to_string()))
for peer in peers { }
config.push_str(&format!(
"\n[Peer]\nPublicKey = {}\nEndpoint = {}:{}\nAllowedIPs = {}\n", pub fn interface_name() -> InterfaceName {
peer.public_key, INTERFACE_NAME.parse().expect("valid interface name")
peer.public_ip, }
peer.port,
peer.allowed_ips pub fn create_interface() -> Result<()> {
.iter() let name = interface_name();
.map(|ip| ip.to_string())
.collect::<Vec<_>>() if Device::get(&name, Backend::Kernel).is_ok() {
.join(", "), tracing::debug!(interface = INTERFACE_NAME, "interface already exists");
)); return Ok(());
} }
config
tracing::info!(interface = INTERFACE_NAME, "creating WireGuard interface");
DeviceUpdate::new()
.apply(&name, Backend::Kernel)
.map_err(|e| Error::create_interface(e, INTERFACE_NAME.to_string()))?;
Ok(())
}
pub fn configure_interface(private_key: &Key, listen_port: u16) -> Result<()> {
let name = interface_name();
tracing::debug!(
interface = INTERFACE_NAME,
listen_port,
"configuring WireGuard interface"
);
DeviceUpdate::new()
.set_private_key(private_key.clone())
.set_listen_port(listen_port)
.apply(&name, Backend::Kernel)
.map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string()))?;
Ok(())
}
pub fn configure_peer(
public_key: &Key,
endpoint: Option<SocketAddr>,
allowed_ips: &[IpNetwork],
persistent_keepalive: Option<u16>,
) -> Result<()> {
let name = interface_name();
let allowed: Vec<AllowedIp> = allowed_ips
.iter()
.map(|ip| AllowedIp {
address: ip.ip(),
cidr: ip.prefix(),
})
.collect();
let mut peer = PeerConfigBuilder::new(public_key)
.replace_allowed_ips()
.add_allowed_ips(&allowed);
if let Some(ep) = endpoint {
peer = peer.set_endpoint(ep);
}
if let Some(keepalive) = persistent_keepalive {
peer = peer.set_persistent_keepalive_interval(keepalive);
}
tracing::debug!(
peer_key = %public_key.to_base64(),
endpoint = ?endpoint,
allowed_ips = ?allowed_ips,
"configuring peer"
);
DeviceUpdate::new()
.add_peer(peer)
.apply(&name, Backend::Kernel)
.map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string()))?;
Ok(())
}
#[allow(dead_code)]
pub fn remove_peer(public_key: &Key) -> Result<()> {
let name = interface_name();
tracing::info!(
peer_key = %public_key.to_base64(),
"removing peer"
);
DeviceUpdate::new()
.remove_peer_by_key(public_key)
.apply(&name, Backend::Kernel)
.map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string()))?;
Ok(())
}
#[allow(dead_code)]
pub fn get_device() -> Result<Device> {
let name = interface_name();
Device::get(&name, Backend::Kernel)
.map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string()))
} }

View File

@@ -1,2 +1,7 @@
dev bin="registry": dev bin="registry":
bacon dev -- --bin {{bin}} bacon dev -- --bin {{bin}}
client target="debug":
cargo build -p client
sudo setcap cap_net_admin+ep ./target/{{target}}/client
./target/{{target}}/client

View File

@@ -21,6 +21,7 @@ tracing-actix-web = "0.7.21"
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "sync"] } tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "sync"] }
thiserror = "2.0.18" thiserror = "2.0.18"
thiserror-ext = "0.3.0" thiserror-ext = "0.3.0"
wireguard-control = "1.7.1"
[lib] [lib]
name = "registry" name = "registry"

View File

@@ -0,0 +1,27 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use wireguard_control::Key;
use crate::{
AppState,
error::{Error, Result},
storage::StorageImpl,
};
#[derive(Deserialize)]
pub struct DeregisterRequest {
public_key: String,
}
pub async fn deregister(
app_state: web::Data<AppState>,
query: web::Query<DeregisterRequest>,
) -> Result<HttpResponse> {
Key::from_base64(&query.public_key).map_err(|_| Error::invalid_key(&query.public_key))?;
app_state
.storage
.deregister_device(&query.public_key)
.await?;
Ok(HttpResponse::Ok().finish())
}

View File

@@ -1,3 +1,4 @@
pub mod deregister;
pub mod peers; pub mod peers;
pub mod register; pub mod register;
pub mod ws; pub mod ws;

View File

@@ -4,13 +4,14 @@ use crate::{
storage::{RegisterRequest, StorageImpl}, storage::{RegisterRequest, StorageImpl},
}; };
use actix_web::{HttpResponse, web}; use actix_web::{HttpResponse, web};
use registry::Peer; use registry::{Peer, RegisterResponse};
pub async fn register_peer( pub async fn register_peer(
app_state: web::Data<AppState>, app_state: web::Data<AppState>,
request: web::Json<RegisterRequest>, request: web::Json<RegisterRequest>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
app_state.storage.register_device(&request).await?; let mesh_ip = app_state.storage.register_device(&request).await?;
app_state app_state
.peer_updates .peer_updates
.send(PeerUpdate { .send(PeerUpdate {
@@ -18,10 +19,11 @@ pub async fn register_peer(
public_key: request.public_key.as_str().to_string(), public_key: request.public_key.as_str().to_string(),
public_ip: request.public_ip.to_string(), public_ip: request.public_ip.to_string(),
port: request.port.clone(), port: request.port.clone(),
mesh_ip,
allowed_ips: request.allowed_ips.clone(), allowed_ips: request.allowed_ips.clone(),
}, },
}) })
.unwrap(); .ok();
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().json(RegisterResponse { mesh_ip }))
} }

View File

@@ -1,4 +1,5 @@
use actix_web::{HttpResponse, ResponseError}; use actix_web::{HttpResponse, ResponseError};
use serde::Serialize;
use thiserror::Error; use thiserror::Error;
use thiserror_ext::{Box, Construct}; use thiserror_ext::{Box, Construct};
@@ -32,12 +33,30 @@ pub enum ErrorKind {
}, },
#[error("error handling ws")] #[error("error handling ws")]
Ws(#[source] actix_web::Error), Ws(#[source] actix_web::Error),
#[error("IP pool exhausted: no available addresses in {pool}")]
IpPoolExhausted { pool: String },
#[error("error deregistering device {public_key}")]
DeregisterDevice {
public_key: String,
#[source]
source: redis::RedisError,
},
#[error("error invalid key")]
InvalidKey(String),
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
} }
impl ResponseError for Error { impl ResponseError for Error {
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> { fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
match self.inner() { match self.inner() {
ErrorKind::Ws(e) => e.error_response(), ErrorKind::Ws(e) => e.error_response(),
ErrorKind::InvalidKey(key) => HttpResponse::BadRequest().json(ErrorResponse {
error: format!("error invalid key: {key}"),
}),
_ => HttpResponse::InternalServerError().finish(), _ => HttpResponse::InternalServerError().finish(),
} }
} }

View File

@@ -1,4 +1,4 @@
mod types; mod types;
mod utils; mod utils;
pub use types::peer_message::*; pub use types::peer_message::{Peer, PeerMessage, RegisterResponse};

View File

@@ -47,6 +47,10 @@ async fn run() -> crate::error::Result<()> {
"/register", "/register",
web::post().to(endpoints::register::register_peer), web::post().to(endpoints::register::register_peer),
) )
.route(
"/deregister",
web::delete().to(endpoints::deregister::deregister),
)
.route("/peers", web::get().to(endpoints::peers::get_peers)) .route("/peers", web::get().to(endpoints::peers::get_peers))
.route("/ws/peers", web::get().to(endpoints::ws::peers::peers)) .route("/ws/peers", web::get().to(endpoints::ws::peers::peers))
}) })

View File

@@ -1,3 +1,5 @@
use std::net::Ipv4Addr;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
mod valkey; mod valkey;
@@ -10,12 +12,13 @@ pub enum Storage {
} }
pub trait StorageImpl { pub trait StorageImpl {
async fn register_device(&self, request: &RegisterRequest) -> Result<()>; async fn register_device(&self, request: &RegisterRequest) -> Result<Ipv4Addr>;
async fn deregister_device(&self, public_key: &str) -> Result<()>;
async fn get_peers(&self) -> Result<Vec<Peer>>; async fn get_peers(&self) -> Result<Vec<Peer>>;
} }
impl StorageImpl for Storage { impl StorageImpl for Storage {
async fn register_device(&self, request: &RegisterRequest) -> Result<()> { async fn register_device(&self, request: &RegisterRequest) -> Result<Ipv4Addr> {
match self { match self {
Self::Valkey(storage) => storage.register_device(request).await, Self::Valkey(storage) => storage.register_device(request).await,
} }
@@ -26,6 +29,12 @@ impl StorageImpl for Storage {
Self::Valkey(storage) => storage.get_peers().await, Self::Valkey(storage) => storage.get_peers().await,
} }
} }
async fn deregister_device(&self, public_key: &str) -> Result<()> {
match self {
Self::Valkey(storage) => storage.deregister_device(public_key).await,
}
}
} }
pub fn get_storage_from_env() -> Result<Storage> { pub fn get_storage_from_env() -> Result<Storage> {

View File

@@ -1,6 +1,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::net::IpAddr; use std::net::{IpAddr, Ipv4Addr};
use futures::TryFutureExt;
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use redis::AsyncTypedCommands; use redis::AsyncTypedCommands;
use registry::Peer; use registry::Peer;
@@ -10,6 +11,10 @@ use crate::error::Result;
use crate::utils::WireguardPublicKey; use crate::utils::WireguardPublicKey;
use crate::{error::Error, storage::StorageImpl}; use crate::{error::Error, storage::StorageImpl};
const MESH_NETWORK_BASE: [u8; 4] = [10, 100, 0, 0];
const MESH_POOL_START: u8 = 1;
const MESH_POOL_END: u8 = 254;
pub struct ValkeyStorage { pub struct ValkeyStorage {
pub valkey_client: redis::Client, pub valkey_client: redis::Client,
} }
@@ -23,22 +28,38 @@ pub struct RegisterRequest {
} }
impl StorageImpl for ValkeyStorage { impl StorageImpl for ValkeyStorage {
async fn register_device(&self, request: &RegisterRequest) -> Result<()> { async fn register_device(&self, request: &RegisterRequest) -> Result<Ipv4Addr> {
let mut conn = self let mut conn = self
.valkey_client .valkey_client
.get_multiplexed_async_connection() .get_multiplexed_async_connection()
.await .await
.map_err(|e| Error::valkey_get_connection(e))?; .map_err(|e| Error::valkey_get_connection(e))?;
let peer_key = format!("peer:{}", request.public_key.as_str());
let existing_mesh_ip: Option<String> = conn
.hget(&peer_key, "mesh_ip")
.await
.map_err(|e| Error::get_peer(e))?;
let mesh_ip = if let Some(ip_str) = existing_mesh_ip {
ip_str.parse::<Ipv4Addr>().unwrap()
} else {
let allocated_ip = self.allocate_mesh_ip(&mut conn).await?;
allocated_ip
};
conn.hset_multiple::<_, _, _>( conn.hset_multiple::<_, _, _>(
format!("peer:{}", request.public_key.as_str()), &peer_key,
&[ &[
("public_ip", &request.public_ip.to_string()), ("public_ip", request.public_ip.to_string()),
( (
"allowed_ips", "allowed_ips",
&serde_json::to_string(&request.allowed_ips) serde_json::to_string(&request.allowed_ips)
.map_err(|e| Error::serialize_json(e, "serializing allowed_ips"))?, .map_err(|e| Error::serialize_json(e, "serializing allowed_ips"))?,
), ),
("port", &request.port), ("port", request.port.clone()),
("mesh_ip", mesh_ip.to_string()),
], ],
) )
.await .await
@@ -49,7 +70,8 @@ impl StorageImpl for ValkeyStorage {
request.public_ip.to_string(), request.public_ip.to_string(),
) )
})?; })?;
conn.sadd("peers", &request.public_key.as_str())
conn.sadd("peers", request.public_key.as_str())
.await .await
.map_err(|e| { .map_err(|e| {
Error::add_peer( Error::add_peer(
@@ -59,6 +81,25 @@ impl StorageImpl for ValkeyStorage {
) )
})?; })?;
Ok(mesh_ip)
}
async fn deregister_device(&self, public_key: &str) -> Result<()> {
let mut conn = self
.valkey_client
.get_multiplexed_async_connection()
.await
.map_err(|e| Error::valkey_get_connection(e))?;
let hash_key = format!("peer:{public_key}");
conn.srem("peers", public_key)
.map_err(|e| Error::deregister_device(e, public_key))
.await?;
let response = conn
.del(hash_key)
.await
.map_err(|e| Error::deregister_device(e, public_key))?;
tracing::debug!("deleted hash {keys} key(s) removed", keys = response);
Ok(()) Ok(())
} }
@@ -89,21 +130,72 @@ impl StorageImpl for ValkeyStorage {
.map_err(|e| Error::get_peer(e))? .map_err(|e| Error::get_peer(e))?
.into_iter() .into_iter()
.zip(keys.iter()) .zip(keys.iter())
.map(|(peer, key): (HashMap<String, String>, &String)| { .filter_map(|(peer, key): (HashMap<String, String>, &String)| {
let allowed_ips: Vec<IpNetwork> = peer let allowed_ips: Vec<IpNetwork> = peer
.get("allowed_ips") .get("allowed_ips")
.map(|s| serde_json::from_str(s).unwrap_or_default()) .map(|s| serde_json::from_str(s).unwrap_or_default())
.unwrap_or_default(); .unwrap_or_default();
Peer { let mesh_ip: Ipv4Addr = peer.get("mesh_ip")?.parse().ok()?;
Some(Peer {
public_key: key.clone(), public_key: key.clone(),
public_ip: peer.get("public_ip").unwrap().to_string(), public_ip: peer.get("public_ip")?.to_string(),
port: peer.get("port").unwrap().to_string(), port: peer.get("port")?.to_string(),
mesh_ip,
allowed_ips, allowed_ips,
} })
}) })
.collect(); .collect();
Ok(peers) Ok(peers)
} }
} }
impl ValkeyStorage {
async fn allocate_mesh_ip(
&self,
conn: &mut redis::aio::MultiplexedConnection,
) -> Result<Ipv4Addr> {
let keys: HashSet<String> = conn
.smembers("peers")
.await
.map_err(|e| Error::get_peer(e))?;
let mut assigned_ips: HashSet<Ipv4Addr> = HashSet::new();
if !keys.is_empty() {
let mut pipe = redis::pipe();
for key in keys.iter() {
pipe.hget(format!("peer:{key}"), "mesh_ip");
}
let ips: Vec<Option<String>> = pipe
.query_async(conn)
.await
.map_err(|e| Error::get_peer(e))?;
for ip_opt in ips {
if let Some(ip_str) = ip_opt {
if let Ok(ip) = ip_str.parse::<Ipv4Addr>() {
assigned_ips.insert(ip);
}
}
}
}
for last_octet in MESH_POOL_START..=MESH_POOL_END {
let candidate = Ipv4Addr::new(
MESH_NETWORK_BASE[0],
MESH_NETWORK_BASE[1],
MESH_NETWORK_BASE[2],
last_octet,
);
if !assigned_ips.contains(&candidate) {
return Ok(candidate);
}
}
Err(Error::ip_pool_exhausted("10.100.0.0/24".to_string()))
}
}

View File

@@ -1,3 +1,5 @@
use std::net::Ipv4Addr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
@@ -7,9 +9,15 @@ pub struct Peer {
pub public_key: String, pub public_key: String,
pub public_ip: String, pub public_ip: String,
pub port: String, pub port: String,
pub mesh_ip: Ipv4Addr,
pub allowed_ips: Vec<IpNetwork>, pub allowed_ips: Vec<IpNetwork>,
} }
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RegisterResponse {
pub mesh_ip: Ipv4Addr,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum PeerMessage { pub enum PeerMessage {

View File

@@ -1,5 +1,5 @@
use base64::Engine; use base64::Engine;
use serde::{Deserialize, de}; use serde::{de, Deserialize};
#[derive(Clone)] #[derive(Clone)]
pub struct WireguardPublicKey(String); pub struct WireguardPublicKey(String);