diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72c62f8..16cf149 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,13 @@ on: push: branches: - main + - dev paths-ignore: - "LICENSE" pull_request: branches: - main + - dev paths-ignore: - "LICENSE" diff --git a/Cargo.lock b/Cargo.lock index b1c396c..2410bec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,18 +14,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -53,22 +53,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake2" @@ -181,7 +181,7 @@ dependencies = [ "parking_lot", "ring", "socket2", - "thiserror", + "thiserror 2.0.17", "tracing", "uniffi", "untrusted", @@ -196,9 +196,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "camino" @@ -229,14 +229,14 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "cc" -version = "1.2.39" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "shlex", @@ -244,9 +244,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -291,9 +291,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.48" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstyle", "clap_lex", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -324,9 +324,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" @@ -345,9 +345,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core", @@ -387,6 +387,7 @@ dependencies = [ "base64", "boringtun", "env_logger", + "ipnet", "libc", "log", "netlink-packet-core", @@ -396,11 +397,14 @@ dependencies = [ "netlink-packet-wireguard", "netlink-sys", "nix", + "regex", "serde", "serde_test", - "thiserror", + "thiserror 2.0.17", "tracing", "tracing-subscriber", + "windows", + "wireguard-nt", "x25519-dalek", ] @@ -417,9 +421,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -451,7 +455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -468,9 +472,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fs-err" @@ -499,19 +503,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", ] [[package]] @@ -533,9 +537,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -560,12 +564,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", + "serde", + "serde_core", ] [[package]] @@ -599,11 +605,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" @@ -613,22 +625,22 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -643,9 +655,19 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "linux-raw-sys" @@ -655,19 +677,18 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -728,7 +749,7 @@ checksum = "3176f18d11a1ae46053e59ec89d46ba318ae1343615bd3f8c908bfc84edae35c" dependencies = [ "byteorder", "pastey", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -779,11 +800,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -794,9 +815,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -806,9 +827,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -816,15 +837,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -885,18 +906,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -918,18 +939,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -939,9 +960,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -950,9 +971,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "ring" @@ -993,7 +1014,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1081,6 +1102,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_test" version = "1.0.177" @@ -1125,12 +1155,12 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1153,9 +1183,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1169,10 +1199,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1184,13 +1214,33 @@ dependencies = [ "smawk", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1215,18 +1265,48 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "serde", + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1235,9 +1315,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1246,9 +1326,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -1267,9 +1347,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -1287,15 +1367,15 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "uniffi" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6d968cb62160c11f2573e6be724ef8b1b18a277aededd17033f8a912d73e2b4" +checksum = "c866f627c3f04c3df068b68bb2d725492caaa539dd313e2a9d26bb85b1a32f4e" dependencies = [ "anyhow", "camino", @@ -1310,9 +1390,9 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b39ef1acbe1467d5d210f274fae344cb6f8766339330cb4c9688752899bf6b" +checksum = "7c8ca600167641ebe7c8ba9254af40492dda3397c528cc3b2f511bd23e8541a5" dependencies = [ "anyhow", "askama", @@ -1336,9 +1416,9 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6683e6b665423cddeacd89a3f97312cf400b2fb245a26f197adaf65c45d505b2" +checksum = "3e55c05228f4858bb258f651d21d743fcc1fe5a2ec20d3c0f9daefddb105ee4d" dependencies = [ "anyhow", "camino", @@ -1347,9 +1427,9 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d990b553d6b9a7ee9c3ae71134674739913d52350b56152b0e613595bb5a6f" +checksum = "7e7a5a038ebffe8f4cf91416b154ef3c2468b18e828b7009e01b1b99938089f9" dependencies = [ "anyhow", "bytes", @@ -1359,9 +1439,9 @@ dependencies = [ [[package]] name = "uniffi_internal_macros" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f4f224becf14885c10e6e400b95cc4d1985738140cb194ccc2044563f8a56b" +checksum = "e3c2a6f93e7b73726e2015696ece25ca0ac5a5f1cf8d6a7ab5214dd0a01d2edf" dependencies = [ "anyhow", "indexmap", @@ -1372,9 +1452,9 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b481d385af334871d70904e6a5f129be7cd38c18fcf8dd8fd1f646b426a56d58" +checksum = "64c6309fc36c7992afc03bc0c5b059c656bccbef3f2a4bc362980017f8936141" dependencies = [ "camino", "fs-err", @@ -1389,9 +1469,9 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f817868a3b171bb7bf259e882138d104deafde65684689b4694c846d322491" +checksum = "0a138823392dba19b0aa494872689f97d0ee157de5852e2bec157ce6de9cdc22" dependencies = [ "anyhow", "siphasher", @@ -1401,9 +1481,9 @@ dependencies = [ [[package]] name = "uniffi_pipeline" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b147e133ad7824e32426b90bc41fda584363563f2ba747f590eca1fd6fd14e6" +checksum = "8c27c4b515d25f8e53cc918e238c39a79c3144a40eaf2e51c4a7958973422c29" dependencies = [ "anyhow", "heck", @@ -1414,9 +1494,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.29.4" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caed654fb73da5abbc7a7e9c741532284532ba4762d6fe5071372df22a41730a" +checksum = "d0adacdd848aeed7af4f5af7d2f621d5e82531325d405e29463482becfdeafca" dependencies = [ "anyhow", "textwrap", @@ -1464,15 +1544,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -1491,11 +1562,112 @@ dependencies = [ "nom", ] +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" @@ -1521,14 +1693,14 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] @@ -1551,19 +1723,28 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -1574,9 +1755,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -1586,9 +1767,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -1598,9 +1779,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -1610,9 +1791,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -1622,9 +1803,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -1634,9 +1815,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -1646,9 +1827,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -1658,19 +1839,35 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] +[[package]] +name = "wireguard-nt" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b4dbcc6c93786cf22e420ef96e8976bfb92a455070282302b74de5848191f4" +dependencies = [ + "bitflags", + "getrandom 0.2.16", + "ipnet", + "libloading", + "log", + "thiserror 1.0.69", + "widestring", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index d324180..e269e01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ keywords = ["wireguard", "network", "vpn"] categories = ["network-programming"] [dependencies] -boringtun = { path = "boringtun/boringtun", default-features = false, features = ["device"] } base64 = "0.22" log = "0.4" serde = { version = "1.0", features = ["derive"], optional = true } @@ -25,10 +24,23 @@ serde_test = "1.0" tracing = "0.1" tracing-subscriber = "0.3" -[target.'cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))'.dependencies] +[target.'cfg(unix)'.dependencies] +boringtun = { path = "boringtun/boringtun", default-features = false, features = [ + "device", +] } libc = { version = "0.2", default-features = false } nix = { version = "0.30", features = ["ioctl", "socket"] } +[target.'cfg(target_os = "windows")'.dependencies] +ipnet = "2.11" +windows = { version = "0.62", features = [ + "Win32_NetworkManagement_IpHelper", + "Win32_NetworkManagement_Ndis", + "Win32_Networking_WinSock", + "Win32_System_Com", +] } +wireguard-nt = "0.5.0" + [target.'cfg(target_os = "linux")'.dependencies] netlink-packet-core = "0.8" netlink-packet-generic = "0.4" @@ -37,6 +49,9 @@ netlink-packet-utils = "0.6" netlink-packet-wireguard = "0.2" netlink-sys = "0.8" +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))'.dependencies] +regex = "1.12" + [features] default = ["serde"] check_dependencies = [] diff --git a/README.md b/README.md index 9e24472..cddbb61 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,37 @@ It was developed as part of [defguard](https://github.com/defguard/defguard) sec ### Windows support -Please note that [WireGuard](https://www.wireguard.com/install/) needs to be installed on Windows with commands `wg` and `wireguard` available to be called from the command line. +Please note that [WireGuard-NT](https://git.zx2c4.com/wireguard-nt/about/) [dll file](https://download.wireguard.com/wireguard-nt/) has to be placed under `resources-windows/binaries/wireguard.dll` path relative to your binary. + +#### Windows development + +For Windows development you'll need: + +1. The `stable-x86_64-pc-windows-gnu` Rust toolchain. Use `rustup` to change the toolchain: + +``` +rustup install stable-x86_64-pc-windows-gnu +rustup default stable-x86_64-pc-windows-gnu +``` + +2. Install [MSYS2](https://www.msys2.org/) + +3. Then run this in the MSYS2 terminal: + +``` +pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain mingw-w64-ucrt-x86_64-nasm +``` + +4. Finally add msys to your PATH: + +``` +# cmd +set PATH=C:\msys64\ucrt64\bin;%PATH% +# power-shell +$env:PATH = "C:\msys64\ucrt64\bin;" + $env:PATH +``` + +More info can be found [here](https://stackoverflow.com/a/79640980). ## Examples diff --git a/boringtun b/boringtun index 69b2649..fd82bf9 160000 --- a/boringtun +++ b/boringtun @@ -1 +1 @@ -Subproject commit 69b2649515ca8c2c948172577bbdc5d183c5892b +Subproject commit fd82bf92f1a3901cd307923670ded2a244b564bc diff --git a/src/bsd/mod.rs b/src/bsd/mod.rs index 31c9512..d027dcb 100644 --- a/src/bsd/mod.rs +++ b/src/bsd/mod.rs @@ -115,7 +115,7 @@ impl IpAddrMask { .and_then(|ipv6| <[u8; 16]>::try_from(ipv6).ok().map(IpAddr::from)), } .map(|ip| Self { - ip, + address: ip, cidr: cidr as u8, }) }) @@ -129,7 +129,7 @@ impl<'a> IpAddrMask { nvlist.append_number(NV_CIDR, u64::from(self.cidr)); - match self.ip { + match self.address { IpAddr::V4(ipv4) => nvlist.append_bytes(NV_IPV4, ipv4.octets().into()), IpAddr::V6(ipv6) => nvlist.append_bytes(NV_IPV6, ipv6.octets().into()), } @@ -340,7 +340,7 @@ pub fn delete_interface(if_name: &str) -> Result<(), IoError> { } pub fn set_address(if_name: &str, address: &IpAddrMask) -> Result<(), IoError> { - match address.ip { + match address.address { IpAddr::V4(address) => { let ifreq = IfReq::new_with_address(if_name, address); ifreq.set_address() @@ -356,7 +356,7 @@ pub fn assign_address(if_name: &str, address: &IpAddrMask) -> Result<(), IoError let broadcast = address.broadcast(); let mask = address.mask(); - match (address.ip, broadcast, mask) { + match (address.address, broadcast, mask) { (IpAddr::V4(address), IpAddr::V4(broadcast), IpAddr::V4(mask)) => { let inaliasreq = InAliasReq::new(if_name, address, broadcast, mask); inaliasreq.add_address() @@ -370,7 +370,7 @@ pub fn assign_address(if_name: &str, address: &IpAddrMask) -> Result<(), IoError } pub fn remove_address(if_name: &str, address: &IpAddrMask) -> Result<(), IoError> { - match address.ip { + match address.address { IpAddr::V4(address) => { let ifreq = IfReq::new_with_address(if_name, address); ifreq.delete_address() @@ -459,7 +459,7 @@ pub fn get_gateway(ip_version: IpVersion) -> Result, IoError> { /// Add routing gateway. pub fn add_gateway(dest: &IpAddrMask, gateway: IpAddr, is_blackhole: bool) -> Result<(), IoError> { debug!("Adding gateway: destination {dest}, gateway {gateway}, is blackhole {is_blackhole}."); - match (dest.ip, dest.mask(), gateway) { + match (dest.address, dest.mask(), gateway) { (IpAddr::V4(ip), IpAddr::V4(mask), IpAddr::V4(gw)) => { let payload = DestAddrMask::::new(ip.into(), mask.into(), gw.into()); let rtmsg = RtMessage::new_for_add_gateway(payload, dest.is_host(), is_blackhole); @@ -480,7 +480,7 @@ pub fn add_gateway(dest: &IpAddrMask, gateway: IpAddr, is_blackhole: bool) -> Re /// Remove routing gateway. pub fn delete_gateway(dest: &IpAddrMask) -> Result<(), IoError> { debug!("Deleting gateway with destination {dest}."); - match (dest.ip, dest.mask()) { + match (dest.address, dest.mask()) { (IpAddr::V4(ip), IpAddr::V4(mask)) => { let payload = DestAddrMask::::new(ip.into(), mask.into(), SockAddrIn::default()); @@ -508,7 +508,7 @@ pub fn add_linked_route(dest: &IpAddrMask, if_name: &str) -> Result<(), IoError> if if_index == 0 { return Err(IoError::NetworkInterface); } - match (dest.ip, dest.mask()) { + match (dest.address, dest.mask()) { (IpAddr::V4(ip), IpAddr::V4(mask)) => { let link = SockAddrDl::new(if_index); let payload = GatewayLink::::new(ip.into(), mask.into(), link); @@ -535,7 +535,7 @@ pub fn add_route(dest: &IpAddrMask, if_name: &str) -> Result<(), IoError> { if if_index == 0 { return Err(IoError::NetworkInterface); } - match (dest.ip, dest.mask()) { + match (dest.address, dest.mask()) { (IpAddr::V4(ip), IpAddr::V4(mask)) => { let payload = DestAddrMask::::new_for_interface(ip.into(), mask.into(), if_name); @@ -561,7 +561,7 @@ pub fn delete_route(dest: &IpAddrMask, if_name: &str) -> Result<(), IoError> { if if_index == 0 { return Err(IoError::NetworkInterface); } - match (dest.ip, dest.mask()) { + match (dest.address, dest.mask()) { (IpAddr::V4(ip), IpAddr::V4(mask)) => { let payload = DestAddrMask::::new_for_interface(ip.into(), mask.into(), if_name); diff --git a/src/dependencies.rs b/src/dependencies.rs index 9e63789..0dbf577 100644 --- a/src/dependencies.rs +++ b/src/dependencies.rs @@ -1,12 +1,12 @@ use std::env; -use crate::error::WireguardInterfaceError; +use crate::{error::WireguardInterfaceError, utils::get_command_path}; #[cfg(target_os = "linux")] const COMMANDS: [&str; 2] = ["resolvconf", "ip"]; #[cfg(target_os = "windows")] -const COMMANDS: [&str; 1] = [("wireguard.exe")]; +const COMMANDS: [&str; 0] = []; #[cfg(target_os = "macos")] const COMMANDS: [&str; 1] = ["networksetup"]; @@ -16,37 +16,19 @@ const COMMANDS: [&str; 1] = ["resolvconf"]; pub(crate) fn check_external_dependencies() -> Result<(), WireguardInterfaceError> { debug!("Checking if all commands required by wireguard-rs are available"); - let paths = env::var_os("PATH").ok_or(WireguardInterfaceError::MissingDependency( - "Environment variable `PATH` not found".into(), - ))?; - - // Find the missing command to provide a more informative error message later. - let missing = COMMANDS.iter().find(|cmd| { - !env::split_paths(&paths).any(|dir| { - trace!("Trying to find {cmd} in {dir:?}"); - match dir.join(cmd).try_exists() { - Ok(true) => { - debug!("{cmd} found in {dir:?}"); - true - } - Ok(false) => { - trace!("{cmd} not found in {dir:?}"); - false - } - Err(err) => { - warn!("Error while checking for {cmd} in {dir:?}: {err}"); - false - } - } - }) + let paths = env::var_os("PATH").ok_or_else(|| { + WireguardInterfaceError::MissingDependency("Environment variable `PATH` not found".into()) }); - if let Some(cmd) = missing { - Err(WireguardInterfaceError::MissingDependency(format!( - "Command `{cmd}` required by wireguard-rs couldn't be found. The following directories were checked: {paths:?}" - ))) - } else { - debug!("All commands required by wireguard-rs are available"); - Ok(()) - } + // Find the missing command to provide a more informative error message later. + let missing_command = COMMANDS + .iter() + .find(|cmd| get_command_path(cmd).map_or(true, |path_opt| path_opt.is_none())); + + missing_command.map_or_else(|| { + debug!("All commands required by wireguard-rs are available"); + Ok(()) + }, |cmd| Err(WireguardInterfaceError::MissingDependency(format!( + "Command `{cmd}` required by wireguard-rs couldn't be found. The following directories were checked: {paths:?}" + )))) } diff --git a/src/error.rs b/src/error.rs index ac8f4dc..7442023 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,9 @@ use thiserror::Error; +#[cfg(target_os = "windows")] +use crate::wgapi_windows::WindowsError; + #[derive(Debug, Error)] #[non_exhaustive] pub enum WireguardInterfaceError { @@ -41,6 +44,10 @@ pub enum WireguardInterfaceError { ServiceRemovalFailed(String), #[error("Socket is closed: {0}")] SocketClosed(String), + #[cfg(target_os = "windows")] + #[error(transparent)] + WindowsError(#[from] WindowsError), + #[cfg(unix)] #[error("BoringTun {0}")] BoringTun(#[from] boringtun::device::Error), } diff --git a/src/host.rs b/src/host.rs index 0dccdac..ae3c16d 100644 --- a/src/host.rs +++ b/src/host.rs @@ -1,4 +1,7 @@ //! Host interface configuration +//! +//! Reference: +//! * WireGuard [Cross-platform Userspace Implementation](https://www.wireguard.com/xplatform/) use std::{ collections::HashMap, @@ -355,11 +358,10 @@ impl Host { WgDeviceAttrs::Peers(nlas) => { for nla in nlas { let peer = Peer::from_nlas(nla); - // On some systems, a dual-stack (IPv4 + IPv6) configuration creates separate peer entries - // for each address family. These entries share the same public key, so inserting a new - // peer would overwrite the existing one. To avoid this, we first check if the peer - // already exists and, if it does, update its statistics instead of replacing it. + // On tunnels with a lot of routes, the peer info may be split across multiple + // entries. We need to merge them here. // https://github.com/DefGuard/client/issues/617 + // https://github.com/DefGuard/wireguard-rs/issues/101 if let Some(existing_peer) = self.peers.get_mut(&peer.public_key) { existing_peer.rx_bytes += peer.rx_bytes; existing_peer.tx_bytes += peer.tx_bytes; @@ -370,6 +372,20 @@ impl Host { (None, Some(y)) => Some(y), (None, None) => None, }; + if peer.preshared_key.is_some() { + existing_peer.preshared_key = peer.preshared_key; + } + if peer.protocol_version.is_some() { + existing_peer.protocol_version = peer.protocol_version; + } + if peer.endpoint.is_some() { + existing_peer.endpoint = peer.endpoint; + } + if peer.persistent_keepalive_interval.is_some() { + existing_peer.persistent_keepalive_interval = + peer.persistent_keepalive_interval; + } + existing_peer.allowed_ips.extend(peer.allowed_ips); } else { self.peers.insert(peer.public_key.clone(), peer); } diff --git a/src/lib.rs b/src/lib.rs index 6750b1c..5d22214 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,7 @@ mod dependencies; mod wgapi_freebsd; #[cfg(target_os = "linux")] mod wgapi_linux; -#[cfg(target_family = "unix")] +#[cfg(unix)] mod wgapi_userspace; #[cfg(target_family = "windows")] mod wgapi_windows; @@ -105,7 +105,7 @@ pub struct InterfaceConfiguration { pub name: String, pub prvkey: String, pub addresses: Vec, - pub port: u32, + pub port: u16, pub peers: Vec, /// Maximum transfer unit. `None` means do not set MTU, but keep the system default. pub mtu: Option, @@ -129,7 +129,7 @@ impl TryFrom<&InterfaceConfiguration> for Host { fn try_from(config: &InterfaceConfiguration) -> Result { let key = config.prvkey.as_str().try_into()?; - let mut host = Host::new(config.port as u16, key); + let mut host = Host::new(config.port, key); for peercfg in &config.peers { let peer = peercfg.clone(); let key: Key = peer.public_key.clone(); diff --git a/src/net.rs b/src/net.rs index e4ac646..c0ba321 100644 --- a/src/net.rs +++ b/src/net.rs @@ -19,31 +19,31 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct IpAddrMask { // IP v4 or v6 - pub ip: IpAddr, + pub address: IpAddr, // Classless Inter-Domain Routing pub cidr: u8, } impl IpAddrMask { #[must_use] - pub fn new(ip: IpAddr, cidr: u8) -> Self { - Self { ip, cidr } + pub fn new(address: IpAddr, cidr: u8) -> Self { + Self { address, cidr } } #[must_use] - pub fn host(ip: IpAddr) -> Self { - let cidr = match ip { + pub fn host(address: IpAddr) -> Self { + let cidr = match address { IpAddr::V4(_) => 32, IpAddr::V6(_) => 128, }; - Self { ip, cidr } + Self { address, cidr } } /// Returns broadcast address as `IpAddr`. /// Note: IPv6 does not really use broadcast. #[must_use] pub fn broadcast(&self) -> IpAddr { - match self.ip { + match self.address { IpAddr::V4(ip) => { let addr = u32::from(ip); let bits = if self.cidr >= 32 { @@ -68,7 +68,7 @@ impl IpAddrMask { /// Returns network mask as `IpAddr`. #[must_use] pub fn mask(&self) -> IpAddr { - match self.ip { + match self.address { IpAddr::V4(_) => { let mask = if self.cidr == 0 { 0 @@ -91,7 +91,7 @@ impl IpAddrMask { /// Returns `true` if the address defines a host, `false` if it is a network. #[must_use] pub fn is_host(&self) -> bool { - if self.ip.is_ipv4() { + if self.address.is_ipv4() { self.cidr == 32 } else { self.cidr == 128 @@ -102,12 +102,12 @@ impl IpAddrMask { #[must_use] pub fn to_nlas_allowed_ip(&self) -> WgAllowedIp { let mut attrs = Vec::new(); - attrs.push(WgAllowedIpAttrs::Family(if self.ip.is_ipv4() { + attrs.push(WgAllowedIpAttrs::Family(if self.address.is_ipv4() { AF_INET } else { AF_INET6 })); - attrs.push(WgAllowedIpAttrs::IpAddr(self.ip)); + attrs.push(WgAllowedIpAttrs::IpAddr(self.address)); attrs.push(WgAllowedIpAttrs::Cidr(self.cidr)); WgAllowedIp(attrs) } @@ -115,7 +115,7 @@ impl IpAddrMask { impl fmt::Display for IpAddrMask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.ip, self.cidr) + write!(f, "{}/{}", self.address, self.cidr) } } @@ -144,11 +144,11 @@ impl FromStr for IpAddrMask { if cidr > max_cidr { return Err(IpAddrParseError); } - Ok(IpAddrMask { ip, cidr }) + Ok(IpAddrMask { address: ip, cidr }) } else { let ip = ip_str.parse().map_err(|_| IpAddrParseError)?; Ok(IpAddrMask { - ip, + address: ip, cidr: if ip.is_ipv4() { 32 } else { 128 }, }) } diff --git a/src/netlink.rs b/src/netlink.rs index c68365a..2b416a1 100644 --- a/src/netlink.rs +++ b/src/netlink.rs @@ -102,7 +102,7 @@ impl Key { impl IpAddrMask { #[must_use] fn address_family(&self) -> AddressFamily { - match self.ip { + match self.address { IpAddr::V4(_) => AddressFamily::Inet, IpAddr::V6(_) => AddressFamily::Inet6, } @@ -260,18 +260,20 @@ fn set_address(index: u32, address: &IpAddrMask) -> NetlinkResult<()> { message.header.index = index; message.header.family = address.address_family(); - if address.ip.is_multicast() { - if let IpAddr::V6(addr) = address.ip { + if address.address.is_multicast() { + if let IpAddr::V6(addr) = address.address { message.attributes.push(AddressAttribute::Multicast(addr)); } } else { message .attributes - .push(AddressAttribute::Address(address.ip)); + .push(AddressAttribute::Address(address.address)); // For IPv4 the Local address can be set to the same value as // Address. - message.attributes.push(AddressAttribute::Local(address.ip)); + message + .attributes + .push(AddressAttribute::Local(address.address)); // Set the broadcast address as well (IPv6 does not support // broadcast). @@ -527,7 +529,7 @@ pub(crate) fn add_route( }; header.address_family = address.address_family(); header.destination_prefix_length = address.cidr; - let route_address = match address.ip { + let route_address = match address.address { IpAddr::V4(ipv4) => RouteAddress::Inet(ipv4), IpAddr::V6(ipv6) => RouteAddress::Inet6(ipv6), }; diff --git a/src/utils.rs b/src/utils.rs index 52c96da..6826af8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,17 @@ #[cfg(target_os = "macos")] -use std::io::{BufRead, BufReader, Cursor, Error as IoError}; +use std::io::{Cursor, Error as IoError}; #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] use std::net::{Ipv4Addr, Ipv6Addr}; -use std::net::{SocketAddr, ToSocketAddrs}; +#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] +use std::path::Path; #[cfg(target_os = "linux")] use std::{collections::HashSet, fs::OpenOptions}; #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] use std::{io::Write, process::Stdio}; +use std::{ + io::{BufRead, BufReader}, + net::{SocketAddr, ToSocketAddrs}, +}; #[cfg(not(target_os = "windows"))] use std::{net::IpAddr, process::Command}; @@ -24,6 +29,55 @@ use crate::{ #[cfg(target_os = "linux")] use crate::{IpVersion, check_command_output_status, netlink}; +#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] +const IFACE_ORDER_PATH: &str = "/etc/resolvconf/interface-order"; + +/// Constructs the resolvconf interface name for manipulating DNS settings. +/// Resolvconf may be symlinked to resolvectl on some systems that use systemd-resolved. +/// In such cases there is no need to prefix the interface name and this function just returns +/// the base interface name. +/// +/// On other systems, especially those that don't use systemd-resolved (Debian 13) +/// resolvconf may be a binary from the "resolvconf" package. In such cases, this function +/// reads the interface-order file to find a highest priority interface prefix and constructs +/// the full interface name prefixing the base interface name with the found prefix. +#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] +fn construct_resolvconf_ifname(base_ifname: &str) -> String { + let iface_order = Path::new(IFACE_ORDER_PATH); + if iface_order.exists() { + // Check if resolvconf command is a symlink or a binary + if let Ok(Some(resolvconf_path)) = get_command_path("resolvconf") { + if let Ok(metadata) = std::fs::symlink_metadata(&resolvconf_path) { + if !metadata.file_type().is_symlink() { + // It's a binary, proceed to read interface_order file + let iface_regex = regex::Regex::new(r"^([A-Za-z0-9-]+)\*$").unwrap(); + if let Ok(file) = std::fs::File::open(iface_order) { + let reader = BufReader::new(file); + if let Some(constructed_ifname) = reader.lines().map_while(Result::ok).find_map(|line| { + let iface = line.trim(); + iface_regex.captures(iface).and_then(|captures| { + captures.get(1).map(|matched_iface| { + // Output format: . + let constructed_ifname = + format!("{}.{base_ifname}", matched_iface.as_str()); + debug!( + "Constructed interface name from interface_order: {constructed_ifname}" + ); + constructed_ifname + }) + }) + }) { + return constructed_ifname; + } + } + } + } + } + } + + base_ifname.into() +} + #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] pub(crate) fn configure_dns( ifname: &str, @@ -36,7 +90,8 @@ pub(crate) fn configure_dns( domains: {search_domains:?}" ); let mut cmd = Command::new("resolvconf"); - let mut args = vec!["-a", ifname, "-m", "0"]; + let ifname = construct_resolvconf_ifname(ifname); + let mut args = vec!["-a", &ifname, "-m", "0"]; // Set the exclusive flag if no search domains are provided, // making the DNS servers a preferred route for any domain if search_domains.is_empty() { @@ -170,7 +225,8 @@ pub(crate) fn configure_dns( #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] pub(crate) fn clear_dns(ifname: &str) -> Result<(), WireguardInterfaceError> { debug!("Removing DNS configuration for interface {ifname}"); - let args = ["-d", ifname, "-f"]; + let ifname = construct_resolvconf_ifname(ifname); + let args = ["-d", &ifname, "-f"]; debug!("Executing resolvconf with args: {args:?}"); let mut cmd = Command::new("resolvconf"); let output = cmd.args(args).output()?; @@ -188,7 +244,7 @@ fn setup_default_route( addr: &crate::IpAddrMask, ) -> Result<(), WireguardInterfaceError> { debug!("Found default route in AllowedIPs: {addr:?}"); - let is_ipv6 = addr.ip.is_ipv6(); + let is_ipv6 = addr.address.is_ipv6(); let proto = if is_ipv6 { "-6" } else { "-4" }; debug!("Using the following IP version: {proto}"); @@ -271,9 +327,9 @@ pub(crate) fn add_peer_routing( // Gather allowed IPs and default routes for peer in peers { for addr in &peer.allowed_ips { - if addr.ip.is_unspecified() { + if addr.address.is_unspecified() { // Default route - store for later - if addr.ip.is_ipv4() { + if addr.address.is_ipv4() { default_routes.0 = Some(addr); } else { default_routes.1 = Some(addr); @@ -281,7 +337,7 @@ pub(crate) fn add_peer_routing( continue; } // Regular route - add to set - if addr.ip.is_ipv4() { + if addr.address.is_ipv4() { allowed_ips.0.insert(addr); } else { allowed_ips.1.insert(addr); @@ -339,14 +395,14 @@ pub(crate) fn add_peer_routing( debug!("Processing route for allowed IP: {addr}, interface: {ifname}"); // FIXME: currently it is impossible to add another default route, so use the hack from // wg-quick for Darwin. - if addr.ip.is_unspecified() && addr.cidr == 0 { + if addr.address.is_unspecified() && addr.cidr == 0 { debug!( "Found following default route in the allowed IPs: {addr}, interface: \ {ifname}, proceeding with default route initial setup." ); let default1; let default2; - if addr.ip.is_ipv4() { + if addr.address.is_ipv4() { // 0.0.0.0/1 default1 = IpAddrMask::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 1); // 128.0.0.0/1 @@ -537,3 +593,33 @@ pub(crate) fn resolve(addr: &str) -> Result .next() .ok_or_else(error) } + +pub(crate) fn get_command_path( + command: &str, +) -> Result, WireguardInterfaceError> { + use std::env; + + debug!("Searching for command {command} in PATH"); + let paths = env::var_os("PATH").ok_or_else(|| { + WireguardInterfaceError::MissingDependency("Environment variable `PATH` not found".into()) + })?; + debug!("PATH variable: {paths:?}"); + + Ok(env::split_paths(&paths).find_map(|dir| { + let full_path = dir.join(command); + match full_path.try_exists() { + Ok(true) => { + debug!("Command {command} found in {dir:?}"); + Some(full_path) + } + Ok(false) => { + debug!("Command {command} not found in {dir:?}"); + None + } + Err(err) => { + warn!("Error while checking for {command} in {dir:?}: {err}"); + None + } + } + })) +} diff --git a/src/wgapi.rs b/src/wgapi.rs index b433ff4..561b1f6 100644 --- a/src/wgapi.rs +++ b/src/wgapi.rs @@ -1,7 +1,10 @@ //! Shared multi-platform management API abstraction use std::marker::PhantomData; +#[cfg(unix)] use boringtun::device::DeviceHandle; +#[cfg(target_os = "windows")] +use wireguard_nt::Adapter; #[cfg(feature = "check_dependencies")] use crate::dependencies::check_external_dependencies; @@ -16,7 +19,10 @@ pub struct Userspace; /// to detect the correct API implementation for most common platforms. pub struct WGApi { pub(super) ifname: String, + #[cfg(unix)] pub(super) device_handle: Option, + #[cfg(target_os = "windows")] + pub(super) adapter: Option, pub(super) _api: PhantomData, } @@ -27,7 +33,10 @@ impl WGApi { check_external_dependencies()?; Ok(WGApi { ifname: ifname.into(), + #[cfg(unix)] device_handle: None, + #[cfg(target_os = "windows")] + adapter: None, _api: PhantomData, }) } diff --git a/src/wgapi_freebsd.rs b/src/wgapi_freebsd.rs index 290c28f..add25ab 100644 --- a/src/wgapi_freebsd.rs +++ b/src/wgapi_freebsd.rs @@ -12,7 +12,7 @@ use crate::{ /// Requires FreeBSD version 13+. impl WireguardInterfaceApi for WGApi { /// Creates a WireGuard network interface. - fn create_interface(&self) -> Result<(), WireguardInterfaceError> { + fn create_interface(&mut self) -> Result<(), WireguardInterfaceError> { let _ = bsd::load_wireguard_kernel_module(); debug!("Creating interface {}", &self.ifname); bsd::create_interface(&self.ifname)?; diff --git a/src/wgapi_windows.rs b/src/wgapi_windows.rs index ccd67af..0f2efa1 100644 --- a/src/wgapi_windows.rs +++ b/src/wgapi_windows.rs @@ -1,14 +1,34 @@ use std::{ - env, - fs::File, - io::{BufRead, BufReader, Cursor, Write}, - net::{IpAddr, SocketAddr}, - process::Command, + collections::HashMap, + ffi::OsStr, + net::IpAddr, + os::windows::ffi::OsStrExt, str::FromStr, - thread::sleep, - time::{Duration, SystemTime}, + sync::{LazyLock, Mutex}, }; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; +use thiserror::Error; +use windows::{ + Win32::{ + Foundation::{ERROR_BUFFER_OVERFLOW, NO_ERROR}, + NetworkManagement::{ + IpHelper::{ + ConvertInterfaceGuidToLuid, DNS_INTERFACE_SETTINGS, + DNS_INTERFACE_SETTINGS_VERSION1, DNS_SETTING_IPV6, DNS_SETTING_NAMESERVER, + GAA_FLAG_INCLUDE_PREFIX, GetAdaptersAddresses, GetIpInterfaceEntry, + IP_ADAPTER_ADDRESSES_LH, InitializeIpInterfaceEntry, MIB_IPINTERFACE_ROW, + SetInterfaceDnsSettings, SetIpInterfaceEntry, + }, + Ndis::NET_LUID_LH, + }, + Networking::WinSock::{ADDRESS_FAMILY, AF_INET, AF_INET6, AF_UNSPEC}, + System::Com::CLSIDFromString, + }, + core::{GUID, PCSTR, PCWSTR, PSTR}, +}; +use wireguard_nt::Wireguard; + use crate::{ InterfaceConfiguration, WireguardInterfaceApi, error::WireguardInterfaceError, @@ -18,228 +38,311 @@ use crate::{ wgapi::{Kernel, WGApi}, }; -/// Manages interfaces created with Windows kernel using https://git.zx2c4.com/wireguard-nt. -impl WireguardInterfaceApi for WGApi { - fn create_interface(&self) -> Result<(), WireguardInterfaceError> { - info!("Opening/creating interface {}", self.ifname); - Ok(()) - } +static WIREGUARD_DLL_PATH: &str = "resources-windows/binaries/wireguard.dll"; +// Load wireguard.dll. Unsafe because we are loading an arbitrary dll file. +static WIREGUARD_DLL: LazyLock> = LazyLock::new(|| { + Mutex::new( + unsafe { wireguard_nt::load_from_path(WIREGUARD_DLL_PATH) } + .expect("Failed to load wireguard.dll"), + ) +}); + +#[derive(Debug, Error)] +pub enum WindowsError { + #[error("Empty interface array")] + EmptyInterfaceArrayError, + #[error("Invalid adapter id: {0}")] + InvalidAdapterId(String), + #[error("Non-zero return value: {0}")] + NonZeroReturnValue(u32), + #[error("Adapter not found: {0}")] + AdapterNotFound(String), + #[error(transparent)] + WireguardNtError(#[from] wireguard_nt::Error), + #[error(transparent)] + FromUtf16Error(#[from] std::string::FromUtf16Error), + #[error(transparent)] + FromUtf8Error(#[from] std::string::FromUtf8Error), + #[error(transparent)] + WindowsCoreError(#[from] windows::core::Error), + #[error("Missing peer endpoint for peer {0}")] + MissingPeerEndpoint(String), +} - fn assign_address(&self, address: &IpAddrMask) -> Result<(), WireguardInterfaceError> { - debug!("Assigning address {address} to interface {}", self.ifname); - Ok(()) - } +/// Converts a string representation of a GUID into a `windows::core::GUID`. +/// Example guid string: "{6B29FC40-CA47-1067-B31D-00DD010662DA}". +fn guid_from_str(s: &str) -> Result { + let wide = str_to_wide_null_terminated(s); + let guid = unsafe { CLSIDFromString(PCWSTR(wide.as_ptr())).map_err(WindowsError::from)? }; + Ok(guid) +} - fn configure_interface( - &self, - config: &InterfaceConfiguration, - dns: &[IpAddr], - search_domains: &[&str], - ) -> Result<(), WireguardInterfaceError> { - debug!( - "Configuring interface {} with config: {config:?}", - self.ifname - ); +/// Returns the GUID of a network adapter given its name. +/// Example adapter name: "Ethernet", "WireGuard". +fn get_adapter_guid(adapter_name: &str) -> Result { + debug!("Finding adapter {adapter_name}"); + // We have to call `GetAdaptersAddresses` twice - first call to just get the `buffer_size` to hold the adapters. + // Before the second call we allocate the buffer with `buffer_size` capacity so that the call can actually + // store the adapters in the buffer. + let mut buffer_size: u32 = 0; + let mut result = unsafe { + // Sets the `buffer_size` + GetAdaptersAddresses( + AF_UNSPEC.0 as u32, + GAA_FLAG_INCLUDE_PREFIX, + None, + None, + &mut buffer_size, + ) + }; + + // We expect the overflow here, since `buffer_size = 0`. No overflow means no adapters. + if result != ERROR_BUFFER_OVERFLOW.0 { + return Err(WindowsError::EmptyInterfaceArrayError); + } - // Interface is created here so that there is no need to pass private key only for Windows - let file_name = format!("{}.conf", &self.ifname); - let path = env::current_dir()?; - let file_path_buf = path.join(&file_name); - let file_path = file_path_buf.to_str().unwrap_or_default(); + // Allocate the buffer and actually get the adapters + let mut buffer = vec![0u8; buffer_size as usize]; + let addresses = buffer.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH; + result = unsafe { + GetAdaptersAddresses( + AF_UNSPEC.0 as u32, + GAA_FLAG_INCLUDE_PREFIX, + None, + Some(addresses), + &mut buffer_size, + ) + }; + if result != NO_ERROR.0 { + return Err(WindowsError::NonZeroReturnValue(result)); + } - debug!("Creating WireGuard configuration file {file_name} in: {file_path}"); + // Find our adapter + let mut current = buffer.as_ptr() as *const IP_ADAPTER_ADDRESSES_LH; + let mut guid: Option = None; + while !current.is_null() { + // SAFETY: + // `current` comes from the linked list allocated and initialized by + // `GetAdaptersAddresses`. The pointer is valid, properly aligned, + // non-null (checked above), and the backing `buffer` lives for the + // duration of this loop. No concurrent mutation occurs, so aliasing + // rules are respected. + let adapter = unsafe { &*current }; + + let friendly_name = unsafe { PCWSTR(adapter.FriendlyName.0).to_string()? }; + if friendly_name == adapter_name { + let adapter_name_str = unsafe { PCSTR(PSTR(adapter.AdapterName.0).0).to_string()? }; + guid = Some(guid_from_str(&adapter_name_str)?); + info!("Found adapter {adapter_name}, GUID: {guid:?}"); + break; + } - let mut file = File::create(&file_name)?; + current = adapter.Next; + } - debug!( - "WireGuard configuration file {file_name} created in {file_path}. Preparing configuration..." - ); + guid.ok_or_else(|| WindowsError::AdapterNotFound(adapter_name.to_string())) +} - let address = config - .addresses - .iter() - .map(|addr| addr.to_string()) - .collect::>() - .join(","); - let mut wireguard_configuration = format!( - "[Interface]\nPrivateKey = {}\nAddress = {address}\n", - config.prvkey +/// Sets both IPv4 and IPv6 MTU on specified interface. +fn set_interface_mtu(interface_name: &str, mtu: u32) -> Result<(), WindowsError> { + debug!("Setting interface {interface_name} MTU to {mtu}"); + let guid = get_adapter_guid(interface_name)?; + + // Convert interface GUID to LUID. + let mut luid = NET_LUID_LH::default(); + let res = unsafe { ConvertInterfaceGuidToLuid(&guid, &mut luid) }; + if res.0 != 0 { + error!( + "ConvertInterfaceGuidToLuid call failed, error value: {}", + res.0 ); + return Err(WindowsError::NonZeroReturnValue(res.0)); + } - if !dns.is_empty() { - // Format: - // DNS = , - // If search domains are present: - // DNS = ,,, - let dns_addresses = format!( - "\nDNS = {}{}", - // DNS addresses part - dns.iter() - .map(|v| v.to_string()) - .collect::>() - .join(","), - // Search domains part, optional - if !search_domains.is_empty() { - format!( - ",{}", - search_domains - .iter() - .map(|v| v.to_string()) - .collect::>() - .join(",") - ) - } else { - "".to_string() - } - ); - wireguard_configuration.push_str(dns_addresses.as_str()); + // Helper function, sets MTU for given IP family. + fn set_mtu_for_family(luid: NET_LUID_LH, family: u16, mtu: u32) -> Result<(), WindowsError> { + // InitializeIpInterfaceEntry has to be called before get/set operations. + let mut row = MIB_IPINTERFACE_ROW::default(); + unsafe { InitializeIpInterfaceEntry(&mut row) }; + + // Load current configuration. + row.InterfaceLuid = luid; + row.Family = ADDRESS_FAMILY(family); + let res = unsafe { GetIpInterfaceEntry(&mut row) }; + if res.0 != 0 { + error!("GetIpInterfaceEntry call failed, error value: {}", res.0); + return Err(WindowsError::NonZeroReturnValue(res.0)); } - for peer in &config.peers { - wireguard_configuration - .push_str(format!("\n[Peer]\nPublicKey = {}", peer.public_key).as_str()); + // Modify the configuration and apply. + row.NlMtu = mtu; + let res = unsafe { SetIpInterfaceEntry(&mut row) }; + if res.0 != 0 { + error!("SetIpInterfaceEntry call failed, error value: {}", res.0); + return Err(WindowsError::NonZeroReturnValue(res.0)); + } + Ok(()) + } - if let Some(preshared_key) = &peer.preshared_key { - wireguard_configuration - .push_str(format!("\nPresharedKey = {}", preshared_key).as_str()); - } + // Set MTU for both IP addr families. + set_mtu_for_family(luid, AF_INET.0, mtu)?; + set_mtu_for_family(luid, AF_INET6.0, mtu)?; - if let Some(keep_alive) = peer.persistent_keepalive_interval { - wireguard_configuration - .push_str(format!("\nPersistentKeepalive = {}", keep_alive).as_str()); - } + info!("Set interface {interface_name} MTU to {mtu}"); + Ok(()) +} - if let Some(endpoint) = peer.endpoint { - wireguard_configuration.push_str(format!("\nEndpoint = {}", endpoint).as_str()); - } +impl From for Peer { + fn from(peer: wireguard_nt::WireguardPeer) -> Self { + Self { + public_key: Key::new(peer.public_key), + preshared_key: Some(Key::new(peer.preshared_key)), + protocol_version: None, + endpoint: Some(peer.endpoint), + tx_bytes: peer.tx_bytes, + rx_bytes: peer.rx_bytes, + last_handshake: peer.last_handshake, + persistent_keepalive_interval: Some(peer.persistent_keepalive), + allowed_ips: peer + .allowed_ips + .iter() + .map(|ip| IpAddrMask::new(ip.addr(), ip.prefix_len())) + .collect(), + } + } +} - if !peer.allowed_ips.is_empty() { - let allowed_ips = format!( - "\nAllowedIPs = {}", - peer.allowed_ips - .iter() - .map(|v| v.to_string()) - .collect::>() - .join(",") - ); - wireguard_configuration.push_str(allowed_ips.as_str()); - } +impl From for Host { + fn from(iface: wireguard_nt::WireguardInterface) -> Self { + let mut peers = HashMap::new(); + for peer in iface.peers { + peers.insert(Key::new(peer.public_key), peer.into()); } + Self { + listen_port: iface.listen_port, + private_key: Some(Key::new(iface.private_key)), + fwmark: None, + peers, + } + } +} - debug!( - "WireGuard configuration prepared: {wireguard_configuration}, writing to the file at {file_path}..." - ); - file.write_all(wireguard_configuration.as_bytes())?; - info!("WireGuard configuration written to file: {file_path}",); +/// Converts an str to wide (u16), null-terminated +fn str_to_wide_null_terminated(s: &str) -> Vec { + OsStr::new(s).encode_wide().chain(Some(0)).collect() +} - // Check for existing service and remove it - debug!( - "Checking for existing wireguard service for interface {}", - self.ifname - ); - let output = Command::new("wg") - .arg("show") - .arg(&self.ifname) - .output() - .map_err(|err| { - error!("Failed to read interface data. Error: {err}"); - WireguardInterfaceError::ReadInterfaceError(err.to_string()) - })?; - debug!("WireGuard service check output: {output:?}",); - - // Service already exists - if output.status.success() { - debug!("Service already exists, removing it first"); - Command::new("wireguard") - .arg("/uninstalltunnelservice") - .arg(&self.ifname) - .output()?; - - debug!("Waiting for service to be removed"); - let mut counter = 1; - loop { - // Occasionally the tunnel is still available even though wg show cannot find it, causing /installtunnelservice to fail - // This might be excessive as closing the application closes the WireGuard tunnel. - sleep(Duration::from_secs(1)); - - let output = Command::new("wg") - .arg("show") - .arg(&self.ifname) - .output() - .map_err(|err| { - error!("Failed to read interface data. Error: {err}"); - WireguardInterfaceError::ReadInterfaceError(err.to_string()) - })?; - - // Service has been removed - if !output.status.success() || counter == 5 { - break; - } - - counter += 1; +/// Manages interfaces created with Windows kernel using https://git.zx2c4.com/wireguard-nt. +impl WireguardInterfaceApi for WGApi { + fn create_interface(&mut self) -> Result<(), WireguardInterfaceError> { + debug!("Opening/creating interface {}", self.ifname); + + // Try to open the adapter. If it's not present create it. + let wireguard = WIREGUARD_DLL.lock().expect("Failed to lock WIREGUARD_DLL"); + let adapter = match wireguard_nt::Adapter::open(&wireguard, &self.ifname) { + Ok(adapter) => { + debug!("Found existing adapter {}", self.ifname); + adapter } - debug!( - "Finished waiting for service to be removed, the service is considered to be removed, proceeding further" - ); - } - - debug!("Installing the new service for interface {}", self.ifname); - let service_installation_output = Command::new("wireguard") - .arg("/installtunnelservice") - .arg(file_path) - .output() - .map_err(|err| { - error!("Failed to create interface. Error: {err}"); - WireguardInterfaceError::ServiceInstallationFailed(err.to_string()) - })?; + Err(_) => { + debug!("Adapter {} does not exist, creating", self.ifname); + wireguard_nt::Adapter::create(&wireguard, &self.ifname, &self.ifname, None) + .map_err(WindowsError::from)? + } + }; + self.adapter = Some(adapter); - debug!( - "Done installing the new service. Service installation output: {service_installation_output:?}", - ); + info!("Opened/created interface {}", self.ifname); + Ok(()) + } - if !service_installation_output.status.success() { - let message = format!( - "Failed to install WireGuard tunnel as a Windows service: {:?}", - service_installation_output.stdout - ); - return Err(WireguardInterfaceError::ServiceInstallationFailed(message)); - } + fn assign_address(&self, address: &IpAddrMask) -> Result<(), WireguardInterfaceError> { + debug!("Assigning address {address} to interface {}", self.ifname); + Ok(()) + } + fn configure_interface( + &self, + config: &InterfaceConfiguration, + ) -> Result<(), WireguardInterfaceError> { debug!( - "Disabling automatic restart for interface {} tunnel service", + "Configuring interface {} with config: {config:?}", self.ifname ); - let service_update_output = Command::new("sc") - .arg("config") - .arg(format!("WireGuardTunnel${}", self.ifname)) - .arg("start=demand") - .output() - .map_err(|err| { - error!("Failed to configure tunnel service. Error: {err}"); - WireguardInterfaceError::ServiceInstallationFailed(err.to_string()) - })?; + // Retrieve the adapter - should be created by calling `Self::create_interface` first. + let Some(ref adapter) = self.adapter else { + Err(WindowsError::AdapterNotFound(self.ifname.clone()))? + }; + + // Prepare peers + debug!("Preparing peers for adapter {}", self.ifname); + let peers: Result, WindowsError> = config + .peers + .iter() + .map(|peer| { + Ok(wireguard_nt::SetPeer { + public_key: Some(peer.public_key.as_array()), + preshared_key: peer.preshared_key.as_ref().map(|key| key.as_array()), + keep_alive: peer.persistent_keepalive_interval, + allowed_ips: peer + .allowed_ips + .iter() + .filter_map(|ip| match ip.address { + IpAddr::V4(addr) => Some(IpNet::V4(Ipv4Net::new(addr, ip.cidr).ok()?)), + IpAddr::V6(addr) => Some(IpNet::V6(Ipv6Net::new(addr, ip.cidr).ok()?)), + }) + .collect(), + endpoint: peer.endpoint.ok_or_else(|| { + WindowsError::MissingPeerEndpoint(peer.public_key.to_string()) + })?, + }) + }) + .collect(); + let peers = peers?; + + // Configure the interface + debug!("Applying configuration for adapter {}", self.ifname); + let interface = wireguard_nt::SetInterface { + listen_port: Some(config.port), + public_key: None, // derived from private key + private_key: Some(Key::from_str(&config.prvkey)?.as_array()), + peers, + }; + adapter.set_config(&interface).map_err(WindowsError::from)?; + + // Set adapter addresses debug!( - "Done disabling automatic restart for the new service. Service update output: {service_update_output:?}", + "Assigning addresses to adapter {}: {:?}", + self.ifname, config.addresses ); - if !service_update_output.status.success() { - let message = format!( - "Failed to configure WireGuard tunnel service: {:?}", - service_update_output.stdout - ); - return Err(WireguardInterfaceError::ServiceInstallationFailed(message)); + let addresses: Vec<_> = config + .addresses + .iter() + .filter_map(|ip| match ip.address { + IpAddr::V4(addr) => Some(IpNet::V4(Ipv4Net::new(addr, ip.cidr).ok()?)), + IpAddr::V6(addr) => Some(IpNet::V6(Ipv6Net::new(addr, ip.cidr).ok()?)), + }) + .collect(); + adapter + .set_default_route(&addresses, &interface) + .map_err(WindowsError::from)?; + + // Set MTU + if let Some(mtu) = config.mtu { + set_interface_mtu(&self.ifname, mtu)?; + // Turn it off and on again. + adapter.down().map_err(WindowsError::from)?; } - // TODO: set maximum transfer unit (MTU) + // Bring the adapter up. + debug!("Bringing up adapter {}", self.ifname); + adapter.up().map_err(WindowsError::from)?; info!( "Interface {} has been successfully configured.", self.ifname ); - debug!( - "Interface {} configured with config: {config:?}", - self.ifname - ); Ok(()) } @@ -247,26 +350,9 @@ impl WireguardInterfaceApi for WGApi { Ok(()) } - fn remove_interface(&self) -> Result<(), WireguardInterfaceError> { + fn remove_interface(&mut self) -> Result<(), WireguardInterfaceError> { debug!("Removing interface {}", self.ifname); - - let command_output = Command::new("wireguard") - .arg("/uninstalltunnelservice") - .arg(&self.ifname) - .output() - .map_err(|err| { - error!("Failed to remove interface. Error: {err}"); - WireguardInterfaceError::CommandExecutionFailed(err) - })?; - - if !command_output.status.success() { - let message = format!( - "Failed to remove WireGuard tunnel service: {:?}", - command_output.stdout - ); - return Err(WireguardInterfaceError::ServiceRemovalFailed(message)); - } - + self.adapter = None; info!("Interface {} removed successfully", self.ifname); Ok(()) } @@ -287,67 +373,11 @@ impl WireguardInterfaceApi for WGApi { fn read_interface_data(&self) -> Result { debug!("Reading host info for interface {}", self.ifname); - let output = Command::new("wg") - .arg("show") - .arg(&self.ifname) - .arg("dump") - .output() - .map_err(|err| { - error!("Failed to read interface. Error: {err}"); - WireguardInterfaceError::CommandExecutionFailed(err) - })?; - - let reader = BufReader::new(Cursor::new(output.stdout)); - let mut host = Host::default(); - let lines = reader.lines(); - - for (index, line_result) in lines.enumerate() { - let line = match &line_result { - Ok(line) => line, - Err(_err) => { - continue; - } - }; - - let data: Vec<&str> = line.split("\t").collect(); - - // First line contains [Interface] section data, every other line is a separate [Peer] - if index == 0 { - // Interface data: private key, public key, listen port, fwmark - host.private_key = Key::from_str(data[0]).ok(); - host.listen_port = data[2].parse().unwrap_or_default(); - - if data[3] != "off" { - host.fwmark = Some(data[3].parse().unwrap()); - } - } else { - // Peer data: public key, preshared key, endpoint, allowed ips, latest handshake, transfer-rx, transfer-tx, persistent-keepalive - if let Ok(public_key) = Key::from_str(data[0]) { - let mut peer = Peer::new(public_key.clone()); - - if data[1] != "(none)" { - peer.preshared_key = Key::from_str(data[0]).ok(); - } - - peer.endpoint = SocketAddr::from_str(data[2]).ok(); - - for allowed_ip in data[3].split(",") { - let addr = IpAddrMask::from_str(allowed_ip.trim())?; - peer.allowed_ips.push(addr); - } - - let handshake = peer.last_handshake.get_or_insert(SystemTime::UNIX_EPOCH); - *handshake += Duration::from_secs(data[4].parse().unwrap_or_default()); - - peer.rx_bytes = data[5].parse().unwrap_or_default(); - peer.tx_bytes = data[6].parse().unwrap_or_default(); - peer.persistent_keepalive_interval = data[7].parse().ok(); - - host.peers.insert(public_key.clone(), peer); - } - } - } - + // Retrieve the adapter - should be created by calling `Self::create_interface` first. + let Some(ref adapter) = self.adapter else { + Err(WindowsError::AdapterNotFound(self.ifname.clone()))? + }; + let host = adapter.get_config().into(); debug!("Read interface data: {host:?}"); Ok(host) } @@ -355,12 +385,63 @@ impl WireguardInterfaceApi for WGApi { fn configure_dns( &self, dns: &[IpAddr], - _search_domains: &[&str], + search_domains: &[&str], ) -> Result<(), WireguardInterfaceError> { debug!( "Configuring DNS for interface {}, using address: {dns:?}", self.ifname ); + let guid = get_adapter_guid(&self.ifname)?; + let (ipv4_dns_ips, ipv6_dns_ips): (Vec<&IpAddr>, Vec<&IpAddr>) = + dns.iter().partition(|ip| ip.is_ipv4()); + let ipv4_dns_servers: Vec = ipv4_dns_ips.iter().map(|ip| ip.to_string()).collect(); + let ipv6_dns_servers: Vec = ipv6_dns_ips.iter().map(|ip| ip.to_string()).collect(); + + let mut search_domains_vec: Vec = + str_to_wide_null_terminated(&search_domains.join(",")); + let search_domains_wide = windows::core::PWSTR(search_domains_vec.as_mut_ptr()); + + if !ipv4_dns_servers.is_empty() { + let dns_str = ipv4_dns_servers.join(","); + let mut wide: Vec = str_to_wide_null_terminated(&dns_str); + let name_server = windows::core::PWSTR(wide.as_mut_ptr()); + + let settings = DNS_INTERFACE_SETTINGS { + Version: DNS_INTERFACE_SETTINGS_VERSION1, + Flags: DNS_SETTING_NAMESERVER as u64, + NameServer: name_server, + SearchList: search_domains_wide, + ..Default::default() + }; + + let status = unsafe { SetInterfaceDnsSettings(guid, &settings) }; + if status != NO_ERROR { + Err(WindowsError::NonZeroReturnValue(status.0))?; + } + } + if !ipv6_dns_servers.is_empty() { + let dns_str = ipv6_dns_servers.join(","); + let mut wide: Vec = str_to_wide_null_terminated(&dns_str); + let name_server = windows::core::PWSTR(wide.as_mut_ptr()); + + let settings = DNS_INTERFACE_SETTINGS { + Version: DNS_INTERFACE_SETTINGS_VERSION1, + Flags: (DNS_SETTING_NAMESERVER | DNS_SETTING_IPV6) as u64, + NameServer: name_server, + SearchList: search_domains_wide, + ..Default::default() + }; + + let status = unsafe { SetInterfaceDnsSettings(guid, &settings) }; + if status != NO_ERROR { + Err(WindowsError::NonZeroReturnValue(status.0))?; + } + } + + info!( + "Configured DNS for interface {}, using address: {dns:?}", + self.ifname + ); Ok(()) } } diff --git a/src/wireguard_interface.rs b/src/wireguard_interface.rs index ace1999..bd6a37f 100644 --- a/src/wireguard_interface.rs +++ b/src/wireguard_interface.rs @@ -24,25 +24,20 @@ pub trait WireguardInterfaceApi { } /// Updates configuration of an existing WireGuard interface. - #[cfg(not(target_os = "windows"))] - fn configure_interface( - &self, - config: &InterfaceConfiguration, - ) -> Result<(), WireguardInterfaceError>; - - #[cfg(target_os = "windows")] fn configure_interface( &self, config: &InterfaceConfiguration, - dns: &[IpAddr], - search_domains: &[&str], ) -> Result<(), WireguardInterfaceError>; /// Removes the WireGuard interface being managed. /// /// Meant to be used in `drop` method for a given API struct. + #[cfg(not(target_os = "windows"))] fn remove_interface(&self) -> Result<(), WireguardInterfaceError>; + #[cfg(target_os = "windows")] + fn remove_interface(&mut self) -> Result<(), WireguardInterfaceError>; + /// Adds a peer or updates peer configuration. fn configure_peer(&self, peer: &Peer) -> Result<(), WireguardInterfaceError>;