├── .github ├── release.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── deny.toml ├── docs └── header.png ├── examples ├── client.rs ├── server.rs └── userspace.rs ├── flake.lock ├── flake.nix └── src ├── bsd ├── ifconfig.rs ├── mod.rs ├── nvlist.rs ├── route.rs ├── sockaddr.rs ├── timespec.rs └── wgio.rs ├── dependencies.rs ├── error.rs ├── host.rs ├── key.rs ├── lib.rs ├── net.rs ├── netlink.rs ├── utils.rs ├── wgapi.rs ├── wgapi_freebsd.rs ├── wgapi_linux.rs ├── wgapi_userspace.rs ├── wgapi_windows.rs └── wireguard_interface.rs /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - Semver-Major 9 | - breaking-change 10 | - title: Exciting New Features 🎉 11 | labels: 12 | - Semver-Minor 13 | - enhancement 14 | - title: Other Changes 15 | labels: 16 | - "*" 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "LICENSE" 9 | pull_request: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - "LICENSE" 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | test: 20 | runs-on: [self-hosted, Linux] 21 | container: rust:1.80 22 | 23 | steps: 24 | - name: Debug 25 | run: echo ${{ github.ref_name }} 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | with: 29 | submodules: recursive 30 | - name: Cache 31 | uses: Swatinem/rust-cache@v2 32 | with: 33 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 34 | - name: Check format 35 | run: | 36 | rustup component add rustfmt 37 | cargo fmt -- --check 38 | - name: Run cargo deny 39 | uses: EmbarkStudios/cargo-deny-action@v2 40 | - name: Run tests 41 | run: cargo test --locked --no-fail-fast 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create a new GitHub release 2 | on: 3 | push: 4 | tags: 5 | - v*.*.* 6 | 7 | jobs: 8 | create-release: 9 | runs-on: [self-hosted, Linux] 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Create release 14 | uses: softprops/action-gh-release@v1 15 | if: startsWith(github.ref, 'refs/tags/') 16 | with: 17 | draft: true 18 | generate_release_notes: true 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea/ 3 | .vscode/ 4 | .direnv 5 | .envrc 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.8" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "anyhow" 66 | version = "1.0.98" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 69 | 70 | [[package]] 71 | name = "autocfg" 72 | version = "1.4.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 75 | 76 | [[package]] 77 | name = "base64" 78 | version = "0.22.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 81 | 82 | [[package]] 83 | name = "bitflags" 84 | version = "2.9.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 87 | 88 | [[package]] 89 | name = "byteorder" 90 | version = "1.5.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 93 | 94 | [[package]] 95 | name = "bytes" 96 | version = "1.10.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 99 | 100 | [[package]] 101 | name = "cfg-if" 102 | version = "1.0.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 105 | 106 | [[package]] 107 | name = "cfg_aliases" 108 | version = "0.2.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 111 | 112 | [[package]] 113 | name = "colorchoice" 114 | version = "1.0.3" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 117 | 118 | [[package]] 119 | name = "cpufeatures" 120 | version = "0.2.17" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 123 | dependencies = [ 124 | "libc", 125 | ] 126 | 127 | [[package]] 128 | name = "curve25519-dalek" 129 | version = "4.1.3" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 132 | dependencies = [ 133 | "cfg-if", 134 | "cpufeatures", 135 | "curve25519-dalek-derive", 136 | "fiat-crypto", 137 | "rustc_version", 138 | "subtle", 139 | "zeroize", 140 | ] 141 | 142 | [[package]] 143 | name = "curve25519-dalek-derive" 144 | version = "0.1.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 147 | dependencies = [ 148 | "proc-macro2", 149 | "quote", 150 | "syn", 151 | ] 152 | 153 | [[package]] 154 | name = "defguard_wireguard_rs" 155 | version = "0.7.3" 156 | dependencies = [ 157 | "base64", 158 | "env_logger", 159 | "libc", 160 | "log", 161 | "netlink-packet-core", 162 | "netlink-packet-generic", 163 | "netlink-packet-route", 164 | "netlink-packet-utils", 165 | "netlink-packet-wireguard", 166 | "netlink-sys", 167 | "nix", 168 | "serde", 169 | "serde_test", 170 | "thiserror 2.0.12", 171 | "x25519-dalek", 172 | ] 173 | 174 | [[package]] 175 | name = "env_filter" 176 | version = "0.1.3" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 179 | dependencies = [ 180 | "log", 181 | "regex", 182 | ] 183 | 184 | [[package]] 185 | name = "env_logger" 186 | version = "0.11.8" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 189 | dependencies = [ 190 | "anstream", 191 | "anstyle", 192 | "env_filter", 193 | "jiff", 194 | "log", 195 | ] 196 | 197 | [[package]] 198 | name = "fiat-crypto" 199 | version = "0.2.9" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 202 | 203 | [[package]] 204 | name = "getrandom" 205 | version = "0.2.16" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 208 | dependencies = [ 209 | "cfg-if", 210 | "libc", 211 | "wasi", 212 | ] 213 | 214 | [[package]] 215 | name = "is_terminal_polyfill" 216 | version = "1.70.1" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 219 | 220 | [[package]] 221 | name = "jiff" 222 | version = "0.2.14" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 225 | dependencies = [ 226 | "jiff-static", 227 | "log", 228 | "portable-atomic", 229 | "portable-atomic-util", 230 | "serde", 231 | ] 232 | 233 | [[package]] 234 | name = "jiff-static" 235 | version = "0.2.14" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 238 | dependencies = [ 239 | "proc-macro2", 240 | "quote", 241 | "syn", 242 | ] 243 | 244 | [[package]] 245 | name = "libc" 246 | version = "0.2.172" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 249 | 250 | [[package]] 251 | name = "log" 252 | version = "0.4.27" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 255 | 256 | [[package]] 257 | name = "memchr" 258 | version = "2.7.4" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 261 | 262 | [[package]] 263 | name = "memoffset" 264 | version = "0.9.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 267 | dependencies = [ 268 | "autocfg", 269 | ] 270 | 271 | [[package]] 272 | name = "netlink-packet-core" 273 | version = "0.7.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" 276 | dependencies = [ 277 | "anyhow", 278 | "byteorder", 279 | "netlink-packet-utils", 280 | ] 281 | 282 | [[package]] 283 | name = "netlink-packet-generic" 284 | version = "0.3.3" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b" 287 | dependencies = [ 288 | "anyhow", 289 | "byteorder", 290 | "netlink-packet-core", 291 | "netlink-packet-utils", 292 | ] 293 | 294 | [[package]] 295 | name = "netlink-packet-route" 296 | version = "0.22.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" 299 | dependencies = [ 300 | "anyhow", 301 | "bitflags", 302 | "byteorder", 303 | "libc", 304 | "log", 305 | "netlink-packet-core", 306 | "netlink-packet-utils", 307 | ] 308 | 309 | [[package]] 310 | name = "netlink-packet-utils" 311 | version = "0.5.2" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" 314 | dependencies = [ 315 | "anyhow", 316 | "byteorder", 317 | "paste", 318 | "thiserror 1.0.69", 319 | ] 320 | 321 | [[package]] 322 | name = "netlink-packet-wireguard" 323 | version = "0.2.3" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "60b25b050ff1f6a1e23c6777b72db22790fe5b6b5ccfd3858672587a79876c8f" 326 | dependencies = [ 327 | "anyhow", 328 | "byteorder", 329 | "libc", 330 | "log", 331 | "netlink-packet-generic", 332 | "netlink-packet-utils", 333 | ] 334 | 335 | [[package]] 336 | name = "netlink-sys" 337 | version = "0.8.7" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" 340 | dependencies = [ 341 | "bytes", 342 | "libc", 343 | "log", 344 | ] 345 | 346 | [[package]] 347 | name = "nix" 348 | version = "0.30.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 351 | dependencies = [ 352 | "bitflags", 353 | "cfg-if", 354 | "cfg_aliases", 355 | "libc", 356 | "memoffset", 357 | ] 358 | 359 | [[package]] 360 | name = "once_cell_polyfill" 361 | version = "1.70.1" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 364 | 365 | [[package]] 366 | name = "paste" 367 | version = "1.0.15" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 370 | 371 | [[package]] 372 | name = "portable-atomic" 373 | version = "1.11.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 376 | 377 | [[package]] 378 | name = "portable-atomic-util" 379 | version = "0.2.4" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 382 | dependencies = [ 383 | "portable-atomic", 384 | ] 385 | 386 | [[package]] 387 | name = "proc-macro2" 388 | version = "1.0.95" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 391 | dependencies = [ 392 | "unicode-ident", 393 | ] 394 | 395 | [[package]] 396 | name = "quote" 397 | version = "1.0.40" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 400 | dependencies = [ 401 | "proc-macro2", 402 | ] 403 | 404 | [[package]] 405 | name = "rand_core" 406 | version = "0.6.4" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 409 | dependencies = [ 410 | "getrandom", 411 | ] 412 | 413 | [[package]] 414 | name = "regex" 415 | version = "1.11.1" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 418 | dependencies = [ 419 | "aho-corasick", 420 | "memchr", 421 | "regex-automata", 422 | "regex-syntax", 423 | ] 424 | 425 | [[package]] 426 | name = "regex-automata" 427 | version = "0.4.9" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 430 | dependencies = [ 431 | "aho-corasick", 432 | "memchr", 433 | "regex-syntax", 434 | ] 435 | 436 | [[package]] 437 | name = "regex-syntax" 438 | version = "0.8.5" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 441 | 442 | [[package]] 443 | name = "rustc_version" 444 | version = "0.4.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 447 | dependencies = [ 448 | "semver", 449 | ] 450 | 451 | [[package]] 452 | name = "semver" 453 | version = "1.0.26" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 456 | 457 | [[package]] 458 | name = "serde" 459 | version = "1.0.219" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 462 | dependencies = [ 463 | "serde_derive", 464 | ] 465 | 466 | [[package]] 467 | name = "serde_derive" 468 | version = "1.0.219" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 471 | dependencies = [ 472 | "proc-macro2", 473 | "quote", 474 | "syn", 475 | ] 476 | 477 | [[package]] 478 | name = "serde_test" 479 | version = "1.0.177" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" 482 | dependencies = [ 483 | "serde", 484 | ] 485 | 486 | [[package]] 487 | name = "subtle" 488 | version = "2.6.1" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 491 | 492 | [[package]] 493 | name = "syn" 494 | version = "2.0.101" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 497 | dependencies = [ 498 | "proc-macro2", 499 | "quote", 500 | "unicode-ident", 501 | ] 502 | 503 | [[package]] 504 | name = "thiserror" 505 | version = "1.0.69" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 508 | dependencies = [ 509 | "thiserror-impl 1.0.69", 510 | ] 511 | 512 | [[package]] 513 | name = "thiserror" 514 | version = "2.0.12" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 517 | dependencies = [ 518 | "thiserror-impl 2.0.12", 519 | ] 520 | 521 | [[package]] 522 | name = "thiserror-impl" 523 | version = "1.0.69" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 526 | dependencies = [ 527 | "proc-macro2", 528 | "quote", 529 | "syn", 530 | ] 531 | 532 | [[package]] 533 | name = "thiserror-impl" 534 | version = "2.0.12" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 537 | dependencies = [ 538 | "proc-macro2", 539 | "quote", 540 | "syn", 541 | ] 542 | 543 | [[package]] 544 | name = "unicode-ident" 545 | version = "1.0.18" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 548 | 549 | [[package]] 550 | name = "utf8parse" 551 | version = "0.2.2" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 554 | 555 | [[package]] 556 | name = "wasi" 557 | version = "0.11.0+wasi-snapshot-preview1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 560 | 561 | [[package]] 562 | name = "windows-sys" 563 | version = "0.59.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 566 | dependencies = [ 567 | "windows-targets", 568 | ] 569 | 570 | [[package]] 571 | name = "windows-targets" 572 | version = "0.52.6" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 575 | dependencies = [ 576 | "windows_aarch64_gnullvm", 577 | "windows_aarch64_msvc", 578 | "windows_i686_gnu", 579 | "windows_i686_gnullvm", 580 | "windows_i686_msvc", 581 | "windows_x86_64_gnu", 582 | "windows_x86_64_gnullvm", 583 | "windows_x86_64_msvc", 584 | ] 585 | 586 | [[package]] 587 | name = "windows_aarch64_gnullvm" 588 | version = "0.52.6" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 591 | 592 | [[package]] 593 | name = "windows_aarch64_msvc" 594 | version = "0.52.6" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 597 | 598 | [[package]] 599 | name = "windows_i686_gnu" 600 | version = "0.52.6" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 603 | 604 | [[package]] 605 | name = "windows_i686_gnullvm" 606 | version = "0.52.6" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 609 | 610 | [[package]] 611 | name = "windows_i686_msvc" 612 | version = "0.52.6" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 615 | 616 | [[package]] 617 | name = "windows_x86_64_gnu" 618 | version = "0.52.6" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 621 | 622 | [[package]] 623 | name = "windows_x86_64_gnullvm" 624 | version = "0.52.6" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 627 | 628 | [[package]] 629 | name = "windows_x86_64_msvc" 630 | version = "0.52.6" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 633 | 634 | [[package]] 635 | name = "x25519-dalek" 636 | version = "2.0.1" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" 639 | dependencies = [ 640 | "curve25519-dalek", 641 | "rand_core", 642 | "serde", 643 | "zeroize", 644 | ] 645 | 646 | [[package]] 647 | name = "zeroize" 648 | version = "1.8.1" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 651 | dependencies = [ 652 | "zeroize_derive", 653 | ] 654 | 655 | [[package]] 656 | name = "zeroize_derive" 657 | version = "1.4.2" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 660 | dependencies = [ 661 | "proc-macro2", 662 | "quote", 663 | "syn", 664 | ] 665 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "defguard_wireguard_rs" 3 | version = "0.7.3" 4 | edition = "2021" 5 | rust-version = "1.80" 6 | description = "A unified multi-platform high-level API for managing WireGuard interfaces" 7 | license = "Apache-2.0" 8 | readme = "README.md" 9 | homepage = "https://github.com/DefGuard/wireguard-rs" 10 | repository = "https://github.com/DefGuard/wireguard-rs" 11 | keywords = ["wireguard", "network", "vpn"] 12 | categories = ["network-programming"] 13 | 14 | [dependencies] 15 | base64 = "0.22" 16 | log = "0.4" 17 | serde = { version = "1.0", features = ["derive"], optional = true } 18 | thiserror = "2.0" 19 | x25519-dalek = { version = "2.0", features = ["getrandom", "static_secrets"] } 20 | 21 | [dev-dependencies] 22 | env_logger = "0.11" 23 | serde_test = "1.0" 24 | 25 | [target.'cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))'.dependencies] 26 | libc = { version = "0.2", default-features = false } 27 | nix = { version = "0.30", features = ["ioctl", "socket"] } 28 | 29 | [target.'cfg(target_os = "linux")'.dependencies] 30 | netlink-packet-core = "0.7" 31 | netlink-packet-generic = "0.3" 32 | netlink-packet-route = "0.22" 33 | netlink-packet-utils = "0.5" 34 | netlink-packet-wireguard = "0.2" 35 | netlink-sys = "0.8" 36 | 37 | [features] 38 | default = ["serde"] 39 | check_dependencies = [] 40 | serde = ["dep:serde"] 41 | 42 | [profile.release] 43 | codegen-units = 1 44 | panic = "abort" 45 | lto = "thin" 46 | strip = "symbols" 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 teonite ventures sp. z o.o. (teonite) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | defguard 3 |

4 | 5 | **defguard_wireguard_rs** is a multi-platform Rust library providing a unified high-level API for managing WireGuard interfaces using native OS kernel and userspace WireGuard protocol implementations. 6 | It can be used to create your own [WireGuard:tm:](https://www.wireguard.com/) VPN servers or clients for secure and private networking. 7 | 8 | It was developed as part of [defguard](https://github.com/defguard/defguard) security platform and used in the [gateway/server](https://github.com/defguard/gateway) as well as [desktop client](https://github.com/defguard/client). 9 | 10 | ## Supported platforms 11 | 12 | * **Native OS Kernel**: Linux, FreeBSD (and pfSense/OPNSense), Windows 13 | * Userspace using [wireguard-go](https://github.com/WireGuard/wireguard-go) - Linux, **macOS**, FreeBSD 14 | 15 | ### Unique features 16 | 17 | * **Peer routing** - see [WGApi](https://docs.rs/defguard_wireguard_rs/latest/defguard_wireguard_rs/struct.WGApi.html) docs. 18 | * Configuring **DNS resolver** - see [WGApi](https://docs.rs/defguard_wireguard_rs/latest/defguard_wireguard_rs/struct.WGApi.html) docs. 19 | * On FreeBSD network interfaces are managed using **ioctl**. 20 | * On Linux, handle network routing using **netlink**. 21 | * **fwmark** handling 22 | 23 | ### Windows support 24 | 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. 25 | 26 | ### Note on `wireguard-go` 27 | If you intend to use the userspace WireGuard implementation you should note that currently the library assumes 28 | that the `wireguard-go` binary will be available at runtime. There are some sanity checks when instantiating the API, 29 | but installing it is outside the scope of this project. 30 | 31 | ## Examples 32 | 33 | * Client: https://github.com/DefGuard/wireguard-rs/blob/main/examples/client.rs 34 | * Server: https://github.com/DefGuard/wireguard-rs/blob/main/examples/server.rs 35 | 36 | ## Documentation 37 | 38 | See the [documentation](https://defguard.gitbook.io) for more information. 39 | 40 | ## Community and Support 41 | 42 | Find us on Matrix: [#defguard:teonite.com](https://matrix.to/#/#defguard:teonite.com) 43 | 44 | ## Contribution 45 | 46 | Please review the [Contributing guide](https://defguard.gitbook.io/defguard/for-developers/contributing) for information on how to get started contributing to the project. You might also find our [environment setup guide](https://defguard.gitbook.io/defguard/for-developers/dev-env-setup) handy. 47 | 48 | # Built and sponsored by 49 | 50 |

51 | build by teonite 52 |

53 | 54 | # Legal 55 | WireGuard® is [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld. 56 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # Root options 13 | 14 | # The graph table configures how the dependency graph is constructed and thus 15 | # which crates the checks are performed against 16 | [graph] 17 | # If 1 or more target triples (and optionally, target_features) are specified, 18 | # only the specified targets will be checked when running `cargo deny check`. 19 | # This means, if a particular package is only ever used as a target specific 20 | # dependency, such as, for example, the `nix` crate only being used via the 21 | # `target_family = "unix"` configuration, that only having windows targets in 22 | # this list would mean the nix crate, as well as any of its exclusive 23 | # dependencies not shared by any other crates, would be ignored, as the target 24 | # list here is effectively saying which targets you are building for. 25 | targets = [ 26 | # The triple can be any string, but only the target triples built in to 27 | # rustc (as of 1.40) can be checked against actual config expressions 28 | #"x86_64-unknown-linux-musl", 29 | # You can also specify which target_features you promise are enabled for a 30 | # particular target. target_features are currently not validated against 31 | # the actual valid features supported by the target architecture. 32 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 33 | ] 34 | # When creating the dependency graph used as the source of truth when checks are 35 | # executed, this field can be used to prune crates from the graph, removing them 36 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 37 | # is pruned from the graph, all of its dependencies will also be pruned unless 38 | # they are connected to another crate in the graph that hasn't been pruned, 39 | # so it should be used with care. The identifiers are [Package ID Specifications] 40 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 41 | #exclude = [] 42 | # If true, metadata will be collected with `--all-features`. Note that this can't 43 | # be toggled off if true, if you want to conditionally enable `--all-features` it 44 | # is recommended to pass `--all-features` on the cmd line instead 45 | all-features = false 46 | # If true, metadata will be collected with `--no-default-features`. The same 47 | # caveat with `all-features` applies 48 | no-default-features = false 49 | # If set, these feature will be enabled when collecting metadata. If `--features` 50 | # is specified on the cmd line they will take precedence over this option. 51 | #features = [] 52 | 53 | # The output table provides options for how/if diagnostics are outputted 54 | [output] 55 | # When outputting inclusion graphs in diagnostics that include features, this 56 | # option can be used to specify the depth at which feature edges will be added. 57 | # This option is included since the graphs can be quite large and the addition 58 | # of features from the crate(s) to all of the graph roots can be far too verbose. 59 | # This option can be overridden via `--feature-depth` on the cmd line 60 | feature-depth = 1 61 | 62 | # This section is considered when running `cargo deny check advisories` 63 | # More documentation for the advisories section can be found here: 64 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 65 | [advisories] 66 | # The path where the advisory databases are cloned/fetched into 67 | #db-path = "$CARGO_HOME/advisory-dbs" 68 | # The url(s) of the advisory databases to use 69 | #db-urls = ["https://github.com/rustsec/advisory-db"] 70 | # A list of advisory IDs to ignore. Note that ignored advisories will still 71 | # output a note when they are encountered. 72 | ignore = [ 73 | { id = "RUSTSEC-2024-0436", reason = "Unmaintained" }, 74 | ] 75 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 76 | # If this is false, then it uses a built-in git library. 77 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 78 | # See Git Authentication for more information about setting up git authentication. 79 | #git-fetch-with-cli = true 80 | 81 | # This section is considered when running `cargo deny check licenses` 82 | # More documentation for the licenses section can be found here: 83 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 84 | [licenses] 85 | # List of explicitly allowed licenses 86 | # See https://spdx.org/licenses/ for list of possible licenses 87 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 88 | allow = [ 89 | "MIT", 90 | "Apache-2.0", 91 | "Apache-2.0 WITH LLVM-exception", 92 | "MPL-2.0", 93 | "BSD-3-Clause", 94 | "Unicode-3.0", 95 | "Unicode-DFS-2016", # unicode-ident 96 | "Zlib", 97 | "ISC", 98 | "BSL-1.0", 99 | "0BSD", 100 | "CC0-1.0", 101 | "OpenSSL", 102 | "CDLA-Permissive-2.0", 103 | ] 104 | # The confidence threshold for detecting a license from license text. 105 | # The higher the value, the more closely the license text must be to the 106 | # canonical license text of a valid SPDX license file. 107 | # [possible values: any between 0.0 and 1.0]. 108 | confidence-threshold = 0.8 109 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 110 | # aren't accepted for every possible crate as with the normal allow list 111 | exceptions = [ 112 | # Each entry is the crate and version constraint, and its specific allow 113 | # list 114 | #{ allow = ["Zlib"], crate = "adler32" }, 115 | ] 116 | 117 | # Some crates don't have (easily) machine readable licensing information, 118 | # adding a clarification entry for it allows you to manually specify the 119 | # licensing information 120 | #[[licenses.clarify]] 121 | # The package spec the clarification applies to 122 | #crate = "ring" 123 | # The SPDX expression for the license requirements of the crate 124 | #expression = "MIT AND ISC AND OpenSSL" 125 | # One or more files in the crate's source used as the "source of truth" for 126 | # the license expression. If the contents match, the clarification will be used 127 | # when running the license check, otherwise the clarification will be ignored 128 | # and the crate will be checked normally, which may produce warnings or errors 129 | # depending on the rest of your configuration 130 | #license-files = [ 131 | # Each entry is a crate relative path, and the (opaque) hash of its contents 132 | #{ path = "LICENSE", hash = 0xbd0eed23 } 133 | #] 134 | 135 | [licenses.private] 136 | # If true, ignores workspace crates that aren't published, or are only 137 | # published to private registries. 138 | # To see how to mark a crate as unpublished (to the official registry), 139 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 140 | ignore = false 141 | # One or more private registries that you might publish crates to, if a crate 142 | # is only published to private registries, and ignore is true, the crate will 143 | # not have its license(s) checked 144 | registries = [ 145 | #"https://sekretz.com/registry 146 | ] 147 | 148 | # This section is considered when running `cargo deny check bans`. 149 | # More documentation about the 'bans' section can be found here: 150 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 151 | [bans] 152 | # Lint level for when multiple versions of the same crate are detected 153 | multiple-versions = "warn" 154 | # Lint level for when a crate version requirement is `*` 155 | wildcards = "allow" 156 | # The graph highlighting used when creating dotgraphs for crates 157 | # with multiple versions 158 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 159 | # * simplest-path - The path to the version with the fewest edges is highlighted 160 | # * all - Both lowest-version and simplest-path are used 161 | highlight = "all" 162 | # The default lint level for `default` features for crates that are members of 163 | # the workspace that is being checked. This can be overridden by allowing/denying 164 | # `default` on a crate-by-crate basis if desired. 165 | workspace-default-features = "allow" 166 | # The default lint level for `default` features for external crates that are not 167 | # members of the workspace. This can be overridden by allowing/denying `default` 168 | # on a crate-by-crate basis if desired. 169 | external-default-features = "allow" 170 | # List of crates that are allowed. Use with care! 171 | allow = [ 172 | #"ansi_term@0.11.0", 173 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, 174 | ] 175 | # List of crates to deny 176 | deny = [ 177 | #"ansi_term@0.11.0", 178 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, 179 | # Wrapper crates can optionally be specified to allow the crate when it 180 | # is a direct dependency of the otherwise banned crate 181 | #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, 182 | ] 183 | 184 | # List of features to allow/deny 185 | # Each entry the name of a crate and a version range. If version is 186 | # not specified, all versions will be matched. 187 | #[[bans.features]] 188 | #crate = "reqwest" 189 | # Features to not allow 190 | #deny = ["json"] 191 | # Features to allow 192 | #allow = [ 193 | # "rustls", 194 | # "__rustls", 195 | # "__tls", 196 | # "hyper-rustls", 197 | # "rustls", 198 | # "rustls-pemfile", 199 | # "rustls-tls-webpki-roots", 200 | # "tokio-rustls", 201 | # "webpki-roots", 202 | #] 203 | # If true, the allowed features must exactly match the enabled feature set. If 204 | # this is set there is no point setting `deny` 205 | #exact = true 206 | 207 | # Certain crates/versions that will be skipped when doing duplicate detection. 208 | skip = [ 209 | #"ansi_term@0.11.0", 210 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, 211 | ] 212 | # Similarly to `skip` allows you to skip certain crates during duplicate 213 | # detection. Unlike skip, it also includes the entire tree of transitive 214 | # dependencies starting at the specified crate, up to a certain depth, which is 215 | # by default infinite. 216 | skip-tree = [ 217 | #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies 218 | #{ crate = "ansi_term@0.11.0", depth = 20 }, 219 | ] 220 | 221 | # This section is considered when running `cargo deny check sources`. 222 | # More documentation about the 'sources' section can be found here: 223 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 224 | [sources] 225 | # Lint level for what to happen when a crate from a crate registry that is not 226 | # in the allow list is encountered 227 | unknown-registry = "warn" 228 | # Lint level for what to happen when a crate from a git repository that is not 229 | # in the allow list is encountered 230 | unknown-git = "warn" 231 | # List of URLs for allowed crate registries. Defaults to the crates.io index 232 | # if not specified. If it is specified but empty, no registries are allowed. 233 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 234 | # List of URLs for allowed Git repositories 235 | allow-git = [] 236 | 237 | [sources.allow-org] 238 | # github.com organizations to allow git sources for 239 | github = [] 240 | # gitlab.com organizations to allow git sources for 241 | gitlab = [] 242 | # bitbucket.org organizations to allow git sources for 243 | bitbucket = [] 244 | -------------------------------------------------------------------------------- /docs/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefGuard/wireguard-rs/69f1ff7064240f5f2ddbab242db0e8271733bf46/docs/header.png -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, str::FromStr}; 2 | 3 | use defguard_wireguard_rs::{ 4 | host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, Kernel, WGApi, 5 | WireguardInterfaceApi, 6 | }; 7 | use x25519_dalek::{EphemeralSecret, PublicKey}; 8 | 9 | fn main() -> Result<(), Box> { 10 | // Create new API object for interface 11 | let ifname: String = if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { 12 | "wg0".into() 13 | } else { 14 | "utun3".into() 15 | }; 16 | 17 | #[cfg(not(target_os = "macos"))] 18 | let wgapi = WGApi::::new(ifname.clone())?; 19 | #[cfg(target_os = "macos")] 20 | let wgapi = WGApi::::new(ifname.clone())?; 21 | 22 | // create interface 23 | wgapi.create_interface()?; 24 | 25 | // Peer configuration 26 | let secret = EphemeralSecret::random(); 27 | let key = PublicKey::from(&secret); 28 | // Peer secret key 29 | let peer_key: Key = key.as_ref().try_into().unwrap(); 30 | let mut peer = Peer::new(peer_key.clone()); 31 | 32 | log::info!("endpoint"); 33 | // Your WireGuard server endpoint which client connects to 34 | let endpoint: SocketAddr = "10.10.10.10:55001".parse().unwrap(); 35 | // Peer endpoint and interval 36 | peer.endpoint = Some(endpoint); 37 | peer.persistent_keepalive_interval = Some(25); 38 | peer.allowed_ips.push(IpAddrMask::from_str("10.6.0.0/24")?); 39 | peer.allowed_ips 40 | .push(IpAddrMask::from_str("192.168.22.0/24")?); 41 | 42 | // interface configuration 43 | let interface_config = InterfaceConfiguration { 44 | name: ifname.clone(), 45 | prvkey: "AAECAwQFBgcICQoLDA0OD/Dh0sO0pZaHeGlaSzwtHg8=".to_string(), 46 | addresses: vec!["10.6.0.30".parse().unwrap()], 47 | port: 12345, 48 | peers: vec![peer], 49 | mtu: None, 50 | }; 51 | 52 | #[cfg(not(windows))] 53 | wgapi.configure_interface(&interface_config)?; 54 | #[cfg(windows)] 55 | wgapi.configure_interface(&interface_config, &[], &[])?; 56 | wgapi.configure_peer_routing(&interface_config.peers)?; 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /examples/server.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use defguard_wireguard_rs::{ 4 | host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, Kernel, WGApi, 5 | WireguardInterfaceApi, 6 | }; 7 | use x25519_dalek::{EphemeralSecret, PublicKey}; 8 | 9 | fn main() -> Result<(), Box> { 10 | // Create new api object for interface management 11 | let ifname: String = if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { 12 | "wg0".into() 13 | } else { 14 | "utun3".into() 15 | }; 16 | 17 | #[cfg(not(target_os = "macos"))] 18 | let wgapi = WGApi::::new(ifname.clone())?; 19 | #[cfg(target_os = "macos")] 20 | let wgapi = WGApi::::new(ifname.clone())?; 21 | 22 | // create host interface 23 | wgapi.create_interface()?; 24 | 25 | // read current interface status 26 | let host = wgapi.read_interface_data()?; 27 | println!("WireGuard interface before configuration: {host:#?}"); 28 | 29 | // store peer keys to remove peers later 30 | let mut peer_keys = Vec::new(); 31 | 32 | // prepare initial WireGuard interface configuration with one client 33 | let secret = EphemeralSecret::random(); 34 | let key = PublicKey::from(&secret); 35 | let peer_key: Key = key.as_ref().try_into().unwrap(); 36 | peer_keys.push(peer_key.clone()); 37 | let mut peer = Peer::new(peer_key); 38 | let addr = IpAddrMask::from_str("10.20.30.2/32").unwrap(); 39 | peer.allowed_ips.push(addr); 40 | 41 | let interface_config = InterfaceConfiguration { 42 | name: ifname.clone(), 43 | prvkey: "AAECAwQFBgcICQoLDA0OD/Dh0sO0pZaHeGlaSzwtHg8=".to_string(), 44 | addresses: vec!["10.6.0.30".parse().unwrap()], 45 | port: 12345, 46 | peers: vec![peer], 47 | mtu: None, 48 | }; 49 | println!("Prepared interface configuration: {interface_config:?}"); 50 | 51 | // apply initial interface configuration 52 | #[cfg(not(windows))] 53 | wgapi.configure_interface(&interface_config)?; 54 | #[cfg(windows)] 55 | wgapi.configure_interface(&interface_config, &[])?; 56 | 57 | // read current interface status 58 | let host = wgapi.read_interface_data()?; 59 | println!("WireGuard interface after configuration: {host:#?}"); 60 | 61 | // add more WireGuard clients 62 | for peer_id in 3..13 { 63 | let secret = EphemeralSecret::random(); 64 | let key = PublicKey::from(&secret); 65 | let peer_key: Key = key.as_ref().try_into().unwrap(); 66 | peer_keys.push(peer_key.clone()); 67 | let mut peer = Peer::new(peer_key); 68 | let addr = IpAddrMask::from_str(&format!("10.20.30.{peer_id}/32")).unwrap(); 69 | peer.allowed_ips.push(addr); 70 | // add peer to WireGuard interface 71 | wgapi.configure_peer(&peer)?; 72 | } 73 | 74 | // read current interface status 75 | let host = wgapi.read_interface_data()?; 76 | println!("WireGuard interface with peers: {host:#?}"); 77 | 78 | // remove all peers 79 | for peer_key in peer_keys { 80 | wgapi.remove_peer(&peer_key)?; 81 | } 82 | 83 | // read current interface status 84 | let host = wgapi.read_interface_data()?; 85 | println!("WireGuard interface without peers: {host:#?}"); 86 | 87 | // remove interface 88 | wgapi.remove_interface()?; 89 | 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /examples/userspace.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | use std::io::{stdin, stdout, Read, Write}; 3 | 4 | #[cfg(target_os = "macos")] 5 | use defguard_wireguard_rs::{Userspace, WGApi, WireguardInterfaceApi}; 6 | 7 | #[cfg(target_os = "macos")] 8 | fn pause() { 9 | let mut stdout = stdout(); 10 | stdout.write_all(b"Press Enter to continue...").unwrap(); 11 | stdout.flush().unwrap(); 12 | stdin().read(&mut [0]).unwrap(); 13 | } 14 | 15 | #[cfg(target_os = "macos")] 16 | fn main() -> Result<(), Box> { 17 | // Setup API struct for interface management 18 | let ifname: String = if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { 19 | "wg0".into() 20 | } else { 21 | "utun5".into() 22 | }; 23 | let api = WGApi::::new(ifname.clone())?; 24 | 25 | // create interface 26 | api.create_interface()?; 27 | 28 | // Peer configuration 29 | let secret = EphemeralSecret::random(); 30 | let key = PublicKey::from(&secret); 31 | // Peer secret key 32 | let peer_key: Key = key.as_ref().try_into().unwrap(); 33 | let mut peer = Peer::new(peer_key.clone()); 34 | 35 | println!("endpoint"); 36 | // Your WireGuard server endpoint which peer connects too 37 | let endpoint: SocketAddr = "10.20.30.40:55001".parse().unwrap(); 38 | // Peer endpoint and interval 39 | peer.endpoint = Some(endpoint); 40 | peer.persistent_keepalive_interval = Some(25); 41 | 42 | // Peer allowed ips 43 | let allowed_ips = vec!["10.6.0.0/24", "192.168.2.0/24"]; 44 | for allowed_ip in allowed_ips { 45 | let addr = IpAddrMask::from_str(allowed_ip)?; 46 | peer.allowed_ips.push(addr); 47 | } 48 | 49 | // interface configuration 50 | let interface_config = InterfaceConfiguration { 51 | name: ifname.clone(), 52 | prvkey: "AAECAwQFBgcICQoLDA0OD/Dh0sO0pZaHeGlaSzwtHg8=".to_string(), 53 | addresses: vec![ 54 | "10.6.0.30".parse().unwrap(), 55 | "fc00:def9::0a1d".parse().unwrap(), 56 | ], 57 | port: 12345, 58 | peers: vec![peer], 59 | mtu: None, 60 | }; 61 | 62 | #[cfg(not(windows))] 63 | api.configure_interface(&interface_config)?; 64 | #[cfg(windows)] 65 | api.configure_interface(&interface_config, &[])?; 66 | 67 | println!("Interface {ifname} configured."); 68 | pause(); 69 | 70 | api.remove_interface()?; 71 | 72 | println!("Interface {ifname} removed."); 73 | 74 | Ok(()) 75 | } 76 | 77 | #[cfg(not(target_os = "macos"))] 78 | fn main() {} 79 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1744463964, 24 | "narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1744943606, 52 | "narHash": "sha256-VL4swGy4uBcHvX+UR5pMeNE9uQzXfA7B37lkwet1EmA=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "ec22cd63500f4832d1f3432d2425e0b31b0361b1", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Rust development flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | rust-overlay = { 8 | url = "github:oxalica/rust-overlay"; 9 | inputs = { 10 | nixpkgs.follows = "nixpkgs"; 11 | }; 12 | }; 13 | }; 14 | 15 | outputs = { 16 | nixpkgs, 17 | flake-utils, 18 | rust-overlay, 19 | ... 20 | }: 21 | flake-utils.lib.eachDefaultSystem (system: let 22 | overlays = [(import rust-overlay)]; 23 | pkgs = import nixpkgs { 24 | inherit system overlays; 25 | }; 26 | rustToolchain = pkgs.rust-bin.stable.latest.default.override { 27 | extensions = ["rust-analyzer" "rust-src" "rustfmt" "clippy"]; 28 | }; 29 | # define shared build inputs 30 | nativeBuildInputs = with pkgs; [rustToolchain pkg-config]; 31 | in { 32 | devShells.default = pkgs.mkShell { 33 | inherit nativeBuildInputs; 34 | 35 | # Specify the rust-src path (many editors rely on this) 36 | RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; 37 | }; 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/bsd/ifconfig.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{Ipv4Addr, Ipv6Addr}, 3 | os::fd::AsRawFd, 4 | }; 5 | 6 | use libc::{IFF_UP, IF_NAMESIZE}; 7 | use nix::{ioctl_readwrite, ioctl_write_ptr, sys::socket::AddressFamily}; 8 | 9 | use super::{ 10 | create_socket, 11 | sockaddr::{SockAddrIn, SockAddrIn6}, 12 | IoError, 13 | }; 14 | 15 | // From `netinet6/in6.h`. 16 | const ND6_INFINITE_LIFETIME: u32 = u32::MAX; 17 | 18 | // SIOCIFDESTROY 19 | ioctl_write_ptr!(destroy_clone_if, b'i', 121, IfReq); 20 | 21 | // SIOCIFCREATE2 22 | // FIXME: not on NetBSD 23 | ioctl_readwrite!(create_clone_if, b'i', 124, IfReq); 24 | 25 | // SIOCGIFMTU 26 | #[cfg(any(target_os = "freebsd", target_os = "macos"))] 27 | ioctl_readwrite!(get_if_mtu, b'i', 51, IfMtu); 28 | #[cfg(target_os = "netbsd")] 29 | ioctl_readwrite!(get_if_mtu, b'i', 126, IfMtu); 30 | 31 | // SIOCSIFMTU 32 | #[cfg(any(target_os = "freebsd", target_os = "macos"))] 33 | ioctl_write_ptr!(set_if_mtu, b'i', 52, IfMtu); 34 | #[cfg(target_os = "netbsd")] 35 | ioctl_write_ptr!(set_if_mtu, b'i', 127, IfMtu); 36 | 37 | // SIOCSIFADDR 38 | ioctl_write_ptr!(set_addr_if, b'i', 12, IfReq); 39 | 40 | // SIOCAIFADDR 41 | #[cfg(target_os = "freebsd")] 42 | ioctl_write_ptr!(add_addr_if, b'i', 43, InAliasReq); 43 | #[cfg(any(target_os = "macos", target_os = "netbsd"))] 44 | ioctl_write_ptr!(add_addr_if, b'i', 26, InAliasReq); 45 | 46 | // SIOCDIFADDR 47 | ioctl_write_ptr!(del_addr_if, b'i', 25, IfReq); 48 | 49 | // SIOCSIFADDR_IN6 50 | ioctl_write_ptr!(set_addr_if_in6, b'i', 12, IfReq6); 51 | 52 | // SIOCAIFADDR_IN6 53 | #[cfg(target_os = "freebsd")] 54 | ioctl_write_ptr!(add_addr_if_in6, b'i', 27, In6AliasReq); 55 | #[cfg(target_os = "macos")] 56 | ioctl_write_ptr!(add_addr_if_in6, b'i', 26, In6AliasReq); 57 | #[cfg(target_os = "netbsd")] 58 | ioctl_write_ptr!(add_addr_if_in6, b'i', 107, In6AliasReq); 59 | 60 | // SIOCDIFADDR_IN6 61 | ioctl_write_ptr!(del_addr_if_in6, b'i', 25, IfReq6); 62 | 63 | // SIOCSIFFLAGS 64 | ioctl_write_ptr!(set_if_flags, b'i', 16, IfReqFlags); 65 | 66 | // SIOCGIFFLAGS 67 | ioctl_readwrite!(get_if_flags, b'i', 17, IfReqFlags); 68 | 69 | type IfName = [u8; IF_NAMESIZE]; 70 | 71 | fn make_ifr_name(if_name: &str) -> IfName { 72 | let mut ifr_name = [0u8; IF_NAMESIZE]; 73 | let len = if_name.len().min(IF_NAMESIZE - 1); 74 | ifr_name[..len].copy_from_slice(&if_name.as_bytes()[..len]); 75 | ifr_name 76 | } 77 | 78 | /// Represent `struct ifreq` as defined in `net/if.h`. 79 | #[repr(C)] 80 | pub struct IfReq { 81 | ifr_name: IfName, 82 | ifr_ifru: SockAddrIn, 83 | } 84 | 85 | impl IfReq { 86 | #[must_use] 87 | pub(super) fn new_with_address(if_name: &str, address: Ipv4Addr) -> Self { 88 | Self { 89 | ifr_name: make_ifr_name(if_name), 90 | ifr_ifru: address.into(), 91 | } 92 | } 93 | 94 | #[must_use] 95 | pub(super) fn new(if_name: &str) -> Self { 96 | Self { 97 | ifr_name: make_ifr_name(if_name), 98 | ifr_ifru: SockAddrIn::default(), 99 | } 100 | } 101 | 102 | pub(super) fn create(&mut self) -> Result<(), IoError> { 103 | let socket = create_socket(AddressFamily::Unix).map_err(IoError::WriteIo)?; 104 | 105 | unsafe { 106 | create_clone_if(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | pub(super) fn destroy(&self) -> Result<(), IoError> { 113 | let socket = create_socket(AddressFamily::Unix).map_err(IoError::WriteIo)?; 114 | 115 | unsafe { 116 | destroy_clone_if(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | pub(super) fn set_address(&self) -> Result<(), IoError> { 123 | let socket = create_socket(AddressFamily::Inet).map_err(IoError::WriteIo)?; 124 | unsafe { 125 | set_addr_if(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 126 | } 127 | 128 | Ok(()) 129 | } 130 | 131 | pub(super) fn delete_address(&self) -> Result<(), IoError> { 132 | let socket = create_socket(AddressFamily::Inet).map_err(IoError::WriteIo)?; 133 | unsafe { 134 | del_addr_if(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 135 | } 136 | 137 | Ok(()) 138 | } 139 | } 140 | 141 | /// Represent `struct ifreq` as defined in `net/if.h` - ifr_mtu variant. 142 | #[repr(C)] 143 | pub struct IfMtu { 144 | ifr_name: IfName, 145 | ifru_mtu: u32, 146 | _padding: [u8; 12], 147 | } 148 | 149 | impl IfMtu { 150 | #[must_use] 151 | pub(super) fn new(if_name: &str) -> Self { 152 | Self { 153 | ifr_name: make_ifr_name(if_name), 154 | ifru_mtu: 0, 155 | _padding: [0u8; 12], 156 | } 157 | } 158 | 159 | pub(super) fn get_mtu(&mut self) -> Result { 160 | let socket = create_socket(AddressFamily::Unix).map_err(IoError::WriteIo)?; 161 | 162 | unsafe { 163 | get_if_mtu(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 164 | } 165 | 166 | Ok(self.ifru_mtu) 167 | } 168 | 169 | pub(super) fn set_mtu(&mut self, mtu: u32) -> Result<(), IoError> { 170 | self.ifru_mtu = mtu; 171 | let socket = create_socket(AddressFamily::Unix).map_err(IoError::WriteIo)?; 172 | 173 | unsafe { 174 | set_if_mtu(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 175 | } 176 | 177 | Ok(()) 178 | } 179 | } 180 | 181 | /// Represent `struct in6_ifreq` as defined in `netinet6/in6_var.h`. 182 | #[repr(C)] 183 | pub struct IfReq6 { 184 | ifr_name: IfName, 185 | ifr_ifru: SockAddrIn6, 186 | _padding: [u8; 244], 187 | } 188 | 189 | impl IfReq6 { 190 | #[must_use] 191 | pub(super) fn new_with_address(if_name: &str, address: Ipv6Addr) -> Self { 192 | Self { 193 | ifr_name: make_ifr_name(if_name), 194 | ifr_ifru: address.into(), 195 | _padding: [0u8; 244], 196 | } 197 | } 198 | 199 | pub(super) fn set_address(&self) -> Result<(), IoError> { 200 | let socket = create_socket(AddressFamily::Inet6).map_err(IoError::WriteIo)?; 201 | 202 | unsafe { 203 | set_addr_if_in6(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 204 | } 205 | 206 | Ok(()) 207 | } 208 | 209 | pub(super) fn delete_address(&self) -> Result<(), IoError> { 210 | let socket = create_socket(AddressFamily::Inet6).map_err(IoError::WriteIo)?; 211 | unsafe { 212 | del_addr_if_in6(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 213 | } 214 | 215 | Ok(()) 216 | } 217 | } 218 | 219 | /// Respresent `in_aliasreq` as defined in . 220 | #[repr(C)] 221 | pub struct InAliasReq { 222 | ifr_name: IfName, 223 | ifra_addr: SockAddrIn, 224 | ifra_broadaddr: SockAddrIn, 225 | ifra_mask: SockAddrIn, 226 | #[cfg(target_os = "freebsd")] 227 | ifra_vhid: u32, 228 | } 229 | 230 | impl InAliasReq { 231 | #[must_use] 232 | pub(super) fn new( 233 | if_name: &str, 234 | address: Ipv4Addr, 235 | broadcast: Ipv4Addr, 236 | mask: Ipv4Addr, 237 | ) -> Self { 238 | Self { 239 | ifr_name: make_ifr_name(if_name), 240 | ifra_addr: address.into(), 241 | ifra_broadaddr: broadcast.into(), 242 | ifra_mask: mask.into(), 243 | #[cfg(target_os = "freebsd")] 244 | ifra_vhid: 0, 245 | } 246 | } 247 | 248 | pub(super) fn add_address(&self) -> Result<(), IoError> { 249 | let socket = create_socket(AddressFamily::Inet).map_err(IoError::WriteIo)?; 250 | 251 | unsafe { 252 | add_addr_if(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 253 | } 254 | 255 | Ok(()) 256 | } 257 | } 258 | 259 | /// Respresent `in6_aliasreq` as defined in . 260 | #[repr(C)] 261 | pub struct In6AliasReq { 262 | ifr_name: IfName, 263 | ifra_addr: SockAddrIn6, 264 | ifra_dstaddr: SockAddrIn6, 265 | ifra_prefixmask: SockAddrIn6, 266 | ifra_flags: u32, 267 | // ifra_lifetime: 268 | ia6t_expire: u64, 269 | ia6t_preferred: u64, 270 | ia6t_vltime: u32, 271 | ia6t_pltime: u32, 272 | #[cfg(target_os = "freebsd")] 273 | ifra_vhid: u32, 274 | } 275 | 276 | impl In6AliasReq { 277 | #[must_use] 278 | pub(super) fn new( 279 | if_name: &str, 280 | address: Ipv6Addr, 281 | // FIXME: currenlty unused: dstaddr: Ipv6Addr, 282 | prefixmask: Ipv6Addr, 283 | ) -> Self { 284 | Self { 285 | ifr_name: make_ifr_name(if_name), 286 | ifra_addr: address.into(), 287 | ifra_dstaddr: SockAddrIn6::zeroed(), 288 | ifra_prefixmask: prefixmask.into(), 289 | ifra_flags: 0, 290 | ia6t_expire: 0, 291 | ia6t_preferred: 0, 292 | ia6t_vltime: ND6_INFINITE_LIFETIME, 293 | ia6t_pltime: ND6_INFINITE_LIFETIME, 294 | #[cfg(target_os = "freebsd")] 295 | ifra_vhid: 0, 296 | } 297 | } 298 | 299 | pub(super) fn add_address(&self) -> Result<(), IoError> { 300 | let socket = create_socket(AddressFamily::Inet6).map_err(IoError::WriteIo)?; 301 | 302 | unsafe { 303 | add_addr_if_in6(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 304 | } 305 | 306 | Ok(()) 307 | } 308 | } 309 | 310 | /// Represent `struct ifreq` as defined in `net/if.h`. 311 | #[repr(C)] 312 | pub struct IfReqFlags { 313 | ifr_name: IfName, 314 | ifr_flags: u64, 315 | ifr_zero: u64, // fill in for size of SockAddrIn 316 | } 317 | 318 | impl IfReqFlags { 319 | #[must_use] 320 | pub(super) fn new(if_name: &str) -> Self { 321 | Self { 322 | ifr_name: make_ifr_name(if_name), 323 | ifr_flags: 0, 324 | ifr_zero: 0, 325 | } 326 | } 327 | 328 | pub(super) fn up(&mut self) -> Result<(), IoError> { 329 | let socket = create_socket(AddressFamily::Unix).map_err(IoError::WriteIo)?; 330 | 331 | // Get current interface flags. 332 | unsafe { 333 | get_if_flags(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 334 | } 335 | 336 | // Set interface up flag. 337 | self.ifr_flags |= IFF_UP as u64; 338 | unsafe { 339 | set_if_flags(socket.as_raw_fd(), self).map_err(IoError::WriteIo)?; 340 | } 341 | 342 | Ok(()) 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/bsd/mod.rs: -------------------------------------------------------------------------------- 1 | mod ifconfig; 2 | mod nvlist; 3 | mod route; 4 | mod sockaddr; 5 | mod timespec; 6 | mod wgio; 7 | 8 | use std::{ 9 | collections::HashMap, ffi::CString, mem::size_of, net::IpAddr, os::fd::OwnedFd, ptr::from_ref, 10 | slice::from_raw_parts, 11 | }; 12 | 13 | use nix::{ 14 | errno::Errno, 15 | sys::socket::{socket, AddressFamily, SockFlag, SockType}, 16 | }; 17 | use route::{DestAddrMask, GatewayLink}; 18 | use sockaddr::{SockAddrDl, SockAddrIn, SockAddrIn6}; 19 | use thiserror::Error; 20 | 21 | use self::{ 22 | ifconfig::{IfMtu, IfReq, IfReq6, IfReqFlags, In6AliasReq, InAliasReq}, 23 | nvlist::NvList, 24 | route::{GatewayAddr, RtMessage}, 25 | sockaddr::{pack_sockaddr, unpack_sockaddr}, 26 | timespec::{pack_timespec, unpack_timespec}, 27 | wgio::{WgReadIo, WgWriteIo}, 28 | }; 29 | use crate::{ 30 | host::{Host, Peer}, 31 | net::IpAddrMask, 32 | IpVersion, Key, WireguardInterfaceError, 33 | }; 34 | 35 | // nvlist key names 36 | static NV_LISTEN_PORT: &str = "listen-port"; 37 | static NV_FWMARK: &str = "user-cookie"; 38 | static NV_PUBLIC_KEY: &str = "public-key"; 39 | static NV_PRIVATE_KEY: &str = "private-key"; 40 | static NV_PEERS: &str = "peers"; 41 | static NV_REPLACE_PEERS: &str = "replace-peers"; 42 | 43 | static NV_PRESHARED_KEY: &str = "preshared-key"; 44 | static NV_KEEPALIVE_INTERVAL: &str = "persistent-keepalive-interval"; 45 | static NV_ENDPOINT: &str = "endpoint"; 46 | static NV_RX_BYTES: &str = "rx-bytes"; 47 | static NV_TX_BYTES: &str = "tx-bytes"; 48 | static NV_LAST_HANDSHAKE: &str = "last-handshake-time"; 49 | static NV_ALLOWED_IPS: &str = "allowed-ips"; 50 | static NV_REPLACE_ALLOWED_IPS: &str = "replace-allowed-ips"; 51 | static NV_REMOVE: &str = "remove"; 52 | 53 | static NV_CIDR: &str = "cidr"; 54 | static NV_IPV4: &str = "ipv4"; 55 | static NV_IPV6: &str = "ipv6"; 56 | 57 | /// Cast bytes to `T`. 58 | unsafe fn cast_ref(bytes: &[u8]) -> &T { 59 | bytes.as_ptr().cast::().as_ref().unwrap() 60 | } 61 | 62 | /// Cast `T' to bytes. 63 | unsafe fn cast_bytes(p: &T) -> &[u8] { 64 | from_raw_parts(from_ref::(p).cast::(), size_of::()) 65 | } 66 | 67 | /// Create socket for ioctl communication. 68 | fn create_socket(address_family: AddressFamily) -> Result { 69 | socket(address_family, SockType::Datagram, SockFlag::empty(), None) 70 | } 71 | 72 | #[derive(Debug, Error)] 73 | pub enum IoError { 74 | #[error("Memory allocation error")] 75 | MemAlloc, 76 | #[error("Read error {0}")] 77 | ReadIo(Errno), 78 | #[error("Write error {0}")] 79 | WriteIo(Errno), 80 | #[error("Network interface does not exist")] 81 | NetworkInterface, 82 | #[error("Not enough bytes to unpack")] 83 | Unpack, 84 | #[error("Failed to load kernel module")] 85 | KernelModule, 86 | } 87 | 88 | impl From for WireguardInterfaceError { 89 | fn from(error: IoError) -> Self { 90 | WireguardInterfaceError::BsdError(error.to_string()) 91 | } 92 | } 93 | 94 | impl IpAddrMask { 95 | #[must_use] 96 | fn try_from_nvlist(nvlist: &NvList) -> Option { 97 | // cidr is mendatory 98 | nvlist.get_number(NV_CIDR).and_then(|cidr| { 99 | match nvlist.get_binary(NV_IPV4) { 100 | Some(ipv4) => <[u8; 4]>::try_from(ipv4).ok().map(IpAddr::from), 101 | None => nvlist 102 | .get_binary(NV_IPV6) 103 | .and_then(|ipv6| <[u8; 16]>::try_from(ipv6).ok().map(IpAddr::from)), 104 | } 105 | .map(|ip| Self { 106 | ip, 107 | cidr: cidr as u8, 108 | }) 109 | }) 110 | } 111 | } 112 | 113 | impl<'a> IpAddrMask { 114 | #[must_use] 115 | fn as_nvlist(&'a self) -> NvList<'a> { 116 | let mut nvlist = NvList::new(); 117 | 118 | nvlist.append_number(NV_CIDR, u64::from(self.cidr)); 119 | 120 | match self.ip { 121 | IpAddr::V4(ipv4) => nvlist.append_bytes(NV_IPV4, ipv4.octets().into()), 122 | IpAddr::V6(ipv6) => nvlist.append_bytes(NV_IPV6, ipv6.octets().into()), 123 | } 124 | 125 | nvlist.append_nvlist_array_next(); 126 | nvlist 127 | } 128 | } 129 | 130 | impl Host { 131 | #[must_use] 132 | fn from_nvlist(nvlist: &NvList) -> Self { 133 | let listen_port = nvlist.get_number(NV_LISTEN_PORT).unwrap_or_default(); 134 | let private_key = nvlist 135 | .get_binary(NV_PRIVATE_KEY) 136 | .and_then(|value| (*value).try_into().ok()); 137 | 138 | let mut peers = HashMap::new(); 139 | if let Some(peer_array) = nvlist.get_nvlist_array(NV_PEERS) { 140 | for peer_list in peer_array { 141 | if let Some(peer) = Peer::try_from_nvlist(peer_list) { 142 | peers.insert(peer.public_key.clone(), peer); 143 | } 144 | } 145 | } 146 | 147 | Self { 148 | listen_port: listen_port as u16, 149 | private_key, 150 | fwmark: nvlist.get_number(NV_FWMARK).map(|num| num as u32), 151 | peers, 152 | } 153 | } 154 | } 155 | 156 | impl<'a> Host { 157 | #[must_use] 158 | fn as_nvlist(&'a self) -> NvList<'a> { 159 | let mut nvlist = NvList::new(); 160 | 161 | nvlist.append_number(NV_LISTEN_PORT, u64::from(self.listen_port)); 162 | if let Some(private_key) = self.private_key.as_ref() { 163 | nvlist.append_binary(NV_PRIVATE_KEY, private_key.as_slice()); 164 | } 165 | if let Some(fwmark) = self.fwmark { 166 | nvlist.append_number(NV_FWMARK, u64::from(fwmark)); 167 | } 168 | 169 | nvlist.append_bool(NV_REPLACE_PEERS, true); 170 | if !self.peers.is_empty() { 171 | let peers = self.peers.values().map(Peer::as_nvlist).collect(); 172 | nvlist.append_nvlist_array(NV_PEERS, peers); 173 | } 174 | 175 | nvlist 176 | } 177 | } 178 | 179 | impl Peer { 180 | #[must_use] 181 | fn try_from_nvlist(nvlist: &NvList) -> Option { 182 | if let Some(public_key) = nvlist 183 | .get_binary(NV_PUBLIC_KEY) 184 | .and_then(|value| (*value).try_into().ok()) 185 | { 186 | let preshared_key = nvlist 187 | .get_binary(NV_PRESHARED_KEY) 188 | .and_then(|value| (*value).try_into().ok()); 189 | let mut allowed_ips = Vec::new(); 190 | if let Some(ip_array) = nvlist.get_nvlist_array(NV_ALLOWED_IPS) { 191 | for ip_list in ip_array { 192 | if let Some(ip) = IpAddrMask::try_from_nvlist(ip_list) { 193 | allowed_ips.push(ip); 194 | } 195 | } 196 | } 197 | 198 | Some(Self { 199 | public_key, 200 | preshared_key, 201 | protocol_version: None, 202 | endpoint: nvlist.get_binary(NV_ENDPOINT).and_then(unpack_sockaddr), 203 | last_handshake: nvlist 204 | .get_binary(NV_LAST_HANDSHAKE) 205 | .and_then(unpack_timespec), 206 | tx_bytes: nvlist.get_number(NV_TX_BYTES).unwrap_or_default(), 207 | rx_bytes: nvlist.get_number(NV_RX_BYTES).unwrap_or_default(), 208 | persistent_keepalive_interval: nvlist 209 | .get_number(NV_KEEPALIVE_INTERVAL) 210 | .map(|value| value as u16), 211 | allowed_ips, 212 | }) 213 | } else { 214 | None 215 | } 216 | } 217 | } 218 | 219 | impl<'a> Peer { 220 | #[must_use] 221 | fn as_nvlist(&'a self) -> NvList<'a> { 222 | let mut nvlist = NvList::new(); 223 | 224 | nvlist.append_binary(NV_PUBLIC_KEY, self.public_key.as_slice()); 225 | if let Some(preshared_key) = self.preshared_key.as_ref() { 226 | nvlist.append_binary(NV_PRESHARED_KEY, preshared_key.as_slice()); 227 | } 228 | if let Some(endpoint) = self.endpoint.as_ref() { 229 | nvlist.append_bytes(NV_ENDPOINT, pack_sockaddr(endpoint)); 230 | } 231 | if let Some(last_handshake) = self.last_handshake.as_ref() { 232 | nvlist.append_bytes(NV_LAST_HANDSHAKE, pack_timespec(last_handshake)); 233 | } 234 | nvlist.append_number(NV_TX_BYTES, self.tx_bytes); 235 | nvlist.append_number(NV_RX_BYTES, self.rx_bytes); 236 | if let Some(keepalive_interval) = self.persistent_keepalive_interval { 237 | nvlist.append_number(NV_KEEPALIVE_INTERVAL, u64::from(keepalive_interval)); 238 | } 239 | 240 | nvlist.append_bool(NV_REPLACE_ALLOWED_IPS, true); 241 | if !self.allowed_ips.is_empty() { 242 | let allowed_ips = self.allowed_ips.iter().map(IpAddrMask::as_nvlist).collect(); 243 | nvlist.append_nvlist_array(NV_ALLOWED_IPS, allowed_ips); 244 | } 245 | 246 | nvlist.append_nvlist_array_next(); 247 | nvlist 248 | } 249 | } 250 | 251 | impl<'a> Key { 252 | #[must_use] 253 | fn as_nvlist_for_removal(&'a self) -> NvList<'a> { 254 | let mut nvlist = NvList::new(); 255 | 256 | nvlist.append_binary(NV_PUBLIC_KEY, self.as_slice()); 257 | nvlist.append_bool(NV_REMOVE, true); 258 | 259 | nvlist.append_nvlist_array_next(); 260 | nvlist 261 | } 262 | } 263 | 264 | pub fn get_host(if_name: &str) -> Result { 265 | let mut wg_data = WgReadIo::new(if_name); 266 | wg_data.read_data()?; 267 | 268 | let mut nvlist = NvList::new(); 269 | // FIXME: use proper error, here and above 270 | nvlist 271 | .unpack(wg_data.as_slice()) 272 | .map_err(|_| IoError::MemAlloc)?; 273 | 274 | Ok(Host::from_nvlist(&nvlist)) 275 | } 276 | 277 | pub fn set_host(if_name: &str, host: &Host) -> Result<(), IoError> { 278 | let nvlist = host.as_nvlist(); 279 | // FIXME: use proper error, here and above 280 | let mut buf = nvlist.pack().map_err(|_| IoError::MemAlloc)?; 281 | 282 | let mut wg_data = WgWriteIo::new(if_name, &mut buf); 283 | wg_data.write_data() 284 | } 285 | 286 | pub fn set_peer(if_name: &str, peer: &Peer) -> Result<(), IoError> { 287 | let mut nvlist = NvList::new(); 288 | nvlist.append_nvlist_array(NV_PEERS, vec![peer.as_nvlist()]); 289 | // FIXME: use proper error, here and above 290 | let mut buf = nvlist.pack().map_err(|_| IoError::MemAlloc)?; 291 | 292 | let mut wg_data = WgWriteIo::new(if_name, &mut buf); 293 | wg_data.write_data() 294 | } 295 | 296 | pub fn delete_peer(if_name: &str, public_key: &Key) -> Result<(), IoError> { 297 | let mut nvlist = NvList::new(); 298 | nvlist.append_nvlist_array(NV_PEERS, vec![public_key.as_nvlist_for_removal()]); 299 | // FIXME: use proper error, here and above 300 | let mut buf = nvlist.pack().map_err(|_| IoError::MemAlloc)?; 301 | 302 | let mut wg_data = WgWriteIo::new(if_name, &mut buf); 303 | wg_data.write_data() 304 | } 305 | 306 | #[cfg(target_os = "freebsd")] 307 | pub fn load_wireguard_kernel_module() -> Result<(), IoError> { 308 | // Ignore the return value for the time being. 309 | let retval = unsafe { libc::kld_load(c"if_wg".as_ptr()) }; 310 | if retval == 0 { 311 | Ok(()) 312 | } else { 313 | Err(IoError::KernelModule) 314 | } 315 | } 316 | 317 | pub fn create_interface(if_name: &str) -> Result<(), IoError> { 318 | let mut ifreq = IfReq::new(if_name); 319 | ifreq.create()?; 320 | // Put the interface up here as it is done on Linux. 321 | let mut ifreq = IfReqFlags::new(if_name); 322 | ifreq.up() 323 | } 324 | 325 | pub fn delete_interface(if_name: &str) -> Result<(), IoError> { 326 | let ifreq = IfReq::new(if_name); 327 | ifreq.destroy() 328 | } 329 | 330 | pub fn set_address(if_name: &str, address: &IpAddrMask) -> Result<(), IoError> { 331 | match address.ip { 332 | IpAddr::V4(address) => { 333 | let ifreq = IfReq::new_with_address(if_name, address); 334 | ifreq.set_address() 335 | } 336 | IpAddr::V6(address) => { 337 | let ifreq6 = IfReq6::new_with_address(if_name, address); 338 | ifreq6.set_address() 339 | } 340 | } 341 | } 342 | 343 | pub fn assign_address(if_name: &str, address: &IpAddrMask) -> Result<(), IoError> { 344 | let broadcast = address.broadcast(); 345 | let mask = address.mask(); 346 | 347 | match (address.ip, broadcast, mask) { 348 | (IpAddr::V4(address), IpAddr::V4(broadcast), IpAddr::V4(mask)) => { 349 | let inaliasreq = InAliasReq::new(if_name, address, broadcast, mask); 350 | inaliasreq.add_address() 351 | } 352 | (IpAddr::V6(address), IpAddr::V6(_broadcast), IpAddr::V6(mask)) => { 353 | let inaliasreq = In6AliasReq::new(if_name, address, mask); 354 | inaliasreq.add_address() 355 | } 356 | _ => unreachable!(), 357 | } 358 | } 359 | 360 | pub fn remove_address(if_name: &str, address: &IpAddrMask) -> Result<(), IoError> { 361 | match address.ip { 362 | IpAddr::V4(address) => { 363 | let ifreq = IfReq::new_with_address(if_name, address); 364 | ifreq.delete_address() 365 | } 366 | IpAddr::V6(address) => { 367 | let ifreq6 = IfReq6::new_with_address(if_name, address); 368 | ifreq6.delete_address() 369 | } 370 | } 371 | } 372 | 373 | pub fn get_mtu(if_name: &str) -> Result { 374 | let mut ifmtu = IfMtu::new(if_name); 375 | ifmtu.get_mtu() 376 | } 377 | 378 | pub fn set_mtu(if_name: &str, mtu: u32) -> Result<(), IoError> { 379 | let mut ifmtu = IfMtu::new(if_name); 380 | ifmtu.set_mtu(mtu) 381 | } 382 | 383 | /// Get (default) gateway for a given IP address version. 384 | pub fn get_gateway(ip_version: IpVersion) -> Result, IoError> { 385 | match ip_version { 386 | IpVersion::IPv4 => RtMessage::>::new_for_gateway().get_gateway(), 387 | IpVersion::IPv6 => RtMessage::>::new_for_gateway().get_gateway(), 388 | } 389 | } 390 | 391 | /// Add routing gateway. 392 | pub fn add_gateway(dest: &IpAddrMask, gateway: IpAddr, is_blackhole: bool) -> Result<(), IoError> { 393 | debug!( 394 | "Adding gateway, destination: {dest}, gateway: {gateway}, is blackhole: {is_blackhole}..." 395 | ); 396 | match (dest.ip, dest.mask(), gateway) { 397 | (IpAddr::V4(ip), IpAddr::V4(mask), IpAddr::V4(gw)) => { 398 | let payload = DestAddrMask::::new(ip.into(), mask.into(), gw.into()); 399 | let rtmsg = RtMessage::new_for_add_gateway(payload, dest.is_host(), is_blackhole); 400 | return rtmsg.execute(); 401 | } 402 | (IpAddr::V6(ip), IpAddr::V6(mask), IpAddr::V6(gw)) => { 403 | let payload = DestAddrMask::::new(ip.into(), mask.into(), gw.into()); 404 | let rtmsg = RtMessage::new_for_add_gateway(payload, dest.is_host(), is_blackhole); 405 | return rtmsg.execute(); 406 | } 407 | _ => error!("Unsupported address for add route"), 408 | } 409 | 410 | debug!("Gateway added"); 411 | Ok(()) 412 | } 413 | 414 | /// Remove routing gateway. 415 | pub fn delete_gateway(dest: &IpAddrMask) -> Result<(), IoError> { 416 | debug!("Deleting gateway with destination {dest}..."); 417 | match (dest.ip, dest.mask()) { 418 | (IpAddr::V4(ip), IpAddr::V4(mask)) => { 419 | let payload = 420 | DestAddrMask::::new(ip.into(), mask.into(), SockAddrIn::default()); 421 | let rtmsg = RtMessage::new_for_delete_gateway(payload, dest.is_host()); 422 | return rtmsg.execute(); 423 | } 424 | (IpAddr::V6(ip), IpAddr::V6(mask)) => { 425 | let payload = 426 | DestAddrMask::::new(ip.into(), mask.into(), SockAddrIn6::default()); 427 | let rtmsg = RtMessage::new_for_delete_gateway(payload, dest.is_host()); 428 | return rtmsg.execute(); 429 | } 430 | _ => error!("Unsupported address for add route"), 431 | } 432 | 433 | debug!("Gateway {dest} deleted."); 434 | Ok(()) 435 | } 436 | 437 | /// Add link layer address gateway. 438 | pub fn add_linked_route(dest: &IpAddrMask, if_name: &str) -> Result<(), IoError> { 439 | debug!("Adding link layer gateway, destination: {dest}, interface: {if_name}"); 440 | let name = CString::new(if_name).unwrap(); 441 | let if_index = unsafe { libc::if_nametoindex(name.as_ptr()) as u16 }; 442 | if if_index == 0 { 443 | return Err(IoError::NetworkInterface); 444 | } 445 | match (dest.ip, dest.mask()) { 446 | (IpAddr::V4(ip), IpAddr::V4(mask)) => { 447 | let link = SockAddrDl::new(if_index); 448 | let payload = GatewayLink::::new(ip.into(), mask.into(), link); 449 | let rtmsg = RtMessage::new_for_add(if_index, payload); 450 | return rtmsg.execute(); 451 | } 452 | (IpAddr::V6(ip), IpAddr::V6(mask)) => { 453 | let link = SockAddrDl::new(if_index); 454 | let payload = GatewayLink::::new(ip.into(), mask.into(), link); 455 | let rtmsg = RtMessage::new_for_add(if_index, payload); 456 | return rtmsg.execute(); 457 | } 458 | _ => error!("Unsupported address for add route"), 459 | } 460 | 461 | debug!("Link layer gateway with destination: {dest} (interface: {if_name}) has been added."); 462 | Ok(()) 463 | } 464 | 465 | /// Add a route to the routing table for a named network interface. 466 | pub fn add_route(dest: &IpAddrMask, if_name: &str) -> Result<(), IoError> { 467 | let name = CString::new(if_name).unwrap(); 468 | let if_index = unsafe { libc::if_nametoindex(name.as_ptr()) as u16 }; 469 | if if_index == 0 { 470 | return Err(IoError::NetworkInterface); 471 | } 472 | match (dest.ip, dest.mask()) { 473 | (IpAddr::V4(ip), IpAddr::V4(mask)) => { 474 | let payload = 475 | DestAddrMask::::new_for_interface(ip.into(), mask.into(), if_name); 476 | let rtmsg = RtMessage::new_for_add(if_index, payload); 477 | return rtmsg.execute(); 478 | } 479 | (IpAddr::V6(ip), IpAddr::V6(mask)) => { 480 | let payload = 481 | DestAddrMask::::new_for_interface(ip.into(), mask.into(), if_name); 482 | let rtmsg = RtMessage::new_for_add(if_index, payload); 483 | return rtmsg.execute(); 484 | } 485 | _ => error!("Unsupported address for add route"), 486 | } 487 | 488 | Ok(()) 489 | } 490 | 491 | /// Add a route from the routing table for a named network interface. 492 | pub fn delete_route(dest: &IpAddrMask, if_name: &str) -> Result<(), IoError> { 493 | let name = CString::new(if_name).unwrap(); 494 | let if_index = unsafe { libc::if_nametoindex(name.as_ptr()) as u16 }; 495 | if if_index == 0 { 496 | return Err(IoError::NetworkInterface); 497 | } 498 | match (dest.ip, dest.mask()) { 499 | (IpAddr::V4(ip), IpAddr::V4(mask)) => { 500 | let payload = 501 | DestAddrMask::::new_for_interface(ip.into(), mask.into(), if_name); 502 | let rtmsg = RtMessage::new_for_delete(if_index, payload); 503 | return rtmsg.execute(); 504 | } 505 | (IpAddr::V6(ip), IpAddr::V6(mask)) => { 506 | let payload = 507 | DestAddrMask::::new_for_interface(ip.into(), mask.into(), if_name); 508 | let rtmsg = RtMessage::new_for_delete(if_index, payload); 509 | return rtmsg.execute(); 510 | } 511 | _ => error!("Unsupported address for add route"), 512 | } 513 | 514 | Ok(()) 515 | } 516 | -------------------------------------------------------------------------------- /src/bsd/route.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem::{size_of, MaybeUninit}, 3 | net::IpAddr, 4 | os::fd::{AsFd, AsRawFd}, 5 | }; 6 | 7 | use nix::{ 8 | errno::Errno, 9 | sys::socket::{shutdown, socket, AddressFamily, Shutdown, SockFlag, SockType}, 10 | unistd::{read, write}, 11 | }; 12 | 13 | use super::{ 14 | cast_bytes, cast_ref, 15 | sockaddr::{unpack_sockaddr, SockAddrDl, SocketFromRaw}, 16 | IoError, 17 | }; 18 | 19 | // Routing data types are not defined in libc crate, so define then here. 20 | 21 | #[allow(dead_code)] 22 | /// Message types for use with `RtMsgHdr`. 23 | #[non_exhaustive] 24 | #[repr(u8)] 25 | enum MessageType { 26 | Add = 1, 27 | Delete, 28 | Change, 29 | Get, 30 | // Losing, 31 | // Redirect, 32 | // Miss, 33 | // Lock, 34 | // OldAdd, // not defined on FreeBSD 35 | // OldDel, // not defined on FreeBSD 36 | // Resolve, // commented out on NetBSD 37 | } 38 | 39 | #[cfg(any(target_os = "freebsd", target_os = "macos"))] 40 | const RTM_VERSION: u8 = 5; 41 | #[cfg(target_os = "netbsd")] 42 | const RTM_VERSION: u8 = 4; 43 | 44 | /// Route message flags. 45 | const RTF_UP: i32 = 0x1; 46 | const RTF_GATEWAY: i32 = 0x2; 47 | const RTF_HOST: i32 = 0x4; 48 | // const RTF_REJECT: i32 = 0x8; 49 | // const RTF_DYNAMIC: i32 = 0x10; 50 | // const RTF_MODIFIED: i32 = 0x20; 51 | // const RTF_DONE: i32 = 0x40; 52 | // const RTF_DELCLONE: i32 = 0x80; // RTF_MASK on NetBSD 53 | const RTF_CLONING: i32 = 0x100; 54 | // const RTF_XRESOLVE: i32 = 0x200; 55 | // const RTF_LLINFO: i32 = 0x400; 56 | // const RTF_LLDATA: i32 = 0x400; 57 | const RTF_STATIC: i32 = 0x800; 58 | const RTF_BLACKHOLE: i32 = 0x1000; 59 | // const RTF_LOCAL: i32 = 0x200000; 60 | // const RTF_BROADCAST: i32 = 0x400000; 61 | // const RTF_MULTICAST: i32 = 0x800000; 62 | // const RTF_IFSCOPE: i32 = 0x1000000; 63 | // const RTF_CONDEMNED: i32 = 0x2000000; 64 | // const RTF_IFREF: i32 = 0x4000000; 65 | // const RTF_PROXY: i32 = 0x8000000; 66 | // const RTF_ROUTER: i32 = 0x10000000; 67 | // const RTF_DEAD: i32 = 0x20000000; 68 | // const RTF_GLOBAL: i32 = 0x40000000; 69 | 70 | /// Bitmask values for rtm_addrs. 71 | const RTA_DST: i32 = 0x1; 72 | const RTA_GATEWAY: i32 = 0x2; 73 | const RTA_NETMASK: i32 = 0x4; 74 | const RTA_GENMASK: i32 = 0x8; 75 | const RTA_IFP: i32 = 0x10; 76 | const RTA_IFA: i32 = 0x20; 77 | // const RTA_AUTHOR: i32 = 0x40; 78 | // const RTA_BRD: i32 = 0x80; 79 | 80 | /// FreeBSD version of `struct rt_metrics` from 81 | #[cfg(target_os = "freebsd")] 82 | #[derive(Default)] 83 | #[repr(C)] 84 | struct RtMetrics { 85 | rmx_locks: u64, 86 | rmx_mtu: u64, 87 | rmx_hopcount: u64, 88 | rmx_expire: u64, 89 | rmx_recvpipe: u64, 90 | rmx_sendpipe: u64, 91 | rmx_ssthresh: u64, 92 | rmx_rtt: u64, 93 | rmx_rttvar: u64, 94 | rmx_pksent: u64, 95 | rmx_weight: u64, 96 | rmx_nhidx: u64, 97 | rmx_filler: [u64; 2], 98 | } 99 | 100 | /// macOS version of `struct rt_metrics` from 101 | #[cfg(target_os = "macos")] 102 | #[derive(Default)] 103 | #[repr(C)] 104 | struct RtMetrics { 105 | rmx_locks: u32, 106 | rmx_mtu: u32, 107 | rmx_hopcount: u32, 108 | rmx_expire: i32, 109 | rmx_recvpipe: u32, 110 | rmx_sendpipe: u32, 111 | rmx_ssthresh: u32, 112 | rmx_rtt: u32, 113 | rmx_rttvar: u32, 114 | rmx_pksent: u32, 115 | rmx_filler: [u32; 4], 116 | } 117 | 118 | /// NetBSD version of `struct rt_metrics` from 119 | #[cfg(target_os = "netbsd")] 120 | #[derive(Default)] 121 | #[repr(C)] 122 | struct RtMetrics { 123 | rmx_locks: u64, 124 | rmx_mtu: u64, 125 | rmx_hopcount: u64, 126 | rmx_recvpipe: u64, 127 | rmx_sendpipe: u64, 128 | rmx_ssthresh: u64, 129 | rmx_rtt: u64, 130 | rmx_rttvar: u64, 131 | rmx_expire: i64, 132 | rmx_pksent: i64, 133 | } 134 | 135 | /// `struct rt_msghdr` from 136 | #[repr(C)] 137 | struct RtMsgHdr { 138 | rtm_msglen: u16, 139 | rtm_version: u8, 140 | rtm_type: u8, 141 | rtm_index: u16, 142 | #[cfg(target_os = "freebsd")] 143 | _rtm_spare1: i16, 144 | rtm_flags: i32, 145 | rtm_addrs: i32, 146 | rtm_pid: i32, 147 | rtm_seq: i32, 148 | rtm_errno: i32, 149 | #[cfg(target_os = "freebsd")] 150 | rtm_fmask: i32, 151 | #[cfg(any(target_os = "macos", target_os = "netbsd"))] 152 | rtm_use: i32, 153 | rtm_inits: u32, 154 | rtm_rmx: RtMetrics, 155 | } 156 | 157 | impl RtMsgHdr { 158 | #[must_use] 159 | fn new( 160 | message_length: u16, 161 | message_type: MessageType, 162 | if_index: u16, 163 | flags: i32, 164 | addrs: i32, 165 | ) -> Self { 166 | Self { 167 | rtm_msglen: message_length, 168 | rtm_version: RTM_VERSION, 169 | rtm_type: message_type as u8, 170 | rtm_index: if_index, 171 | #[cfg(target_os = "freebsd")] 172 | _rtm_spare1: 0, 173 | rtm_flags: flags, 174 | rtm_addrs: addrs, 175 | rtm_pid: unsafe { libc::getpid() }, 176 | rtm_seq: 1, 177 | rtm_errno: 0, 178 | #[cfg(target_os = "freebsd")] 179 | rtm_fmask: 0, 180 | #[cfg(any(target_os = "macos", target_os = "netbsd"))] 181 | rtm_use: 0, 182 | rtm_inits: 0, 183 | rtm_rmx: RtMetrics::default(), 184 | } 185 | } 186 | } 187 | 188 | #[repr(C)] 189 | pub(super) struct RtMessage { 190 | header: RtMsgHdr, 191 | payload: Payload, 192 | } 193 | 194 | #[derive(Default)] 195 | #[repr(C)] 196 | pub(super) struct GatewayAddr { 197 | dest: S, 198 | ifa: S, // default gateway links will have IP address here 199 | } 200 | 201 | #[repr(C)] 202 | pub(super) struct DestAddrMask { 203 | dest: S, 204 | gateway: S, // mendatory on FreeBSD - if missing, EINVAL is returned 205 | netmask: S, 206 | } 207 | 208 | #[repr(C)] 209 | pub(super) struct GatewayLink { 210 | dest: S, 211 | gateway: SockAddrDl, // mendatory on FreeBSD - if missing, EINVAL is returned 212 | netmask: S, 213 | } 214 | 215 | /// Get an address for a given interface. First address is returned. 216 | fn if_addr(if_name: &str) -> Option { 217 | let mut addrs = MaybeUninit::<*mut libc::ifaddrs>::uninit(); 218 | let errno = unsafe { libc::getifaddrs(addrs.as_mut_ptr()) }; 219 | if errno == 0 { 220 | let addrs = unsafe { addrs.assume_init() }; 221 | let mut addr = addrs; 222 | while !addr.is_null() { 223 | let name = unsafe { std::ffi::CStr::from_ptr((*addr).ifa_name) }; 224 | if name.to_str().unwrap() == if_name { 225 | if let Some(sockaddr) = unsafe { S::from_raw((*addr).ifa_addr) } { 226 | return Some(sockaddr); 227 | } 228 | } 229 | addr = unsafe { (*addr).ifa_next }; 230 | } 231 | unsafe { libc::freeifaddrs(addrs) }; 232 | } else { 233 | debug!("getifaddrs returned {errno}"); 234 | } 235 | 236 | None 237 | } 238 | 239 | impl GatewayLink { 240 | #[must_use] 241 | pub(super) fn new(dest: S, netmask: S, link: SockAddrDl) -> Self { 242 | Self { 243 | dest, 244 | gateway: link, 245 | netmask, 246 | } 247 | } 248 | } 249 | 250 | impl DestAddrMask { 251 | #[must_use] 252 | pub(super) fn new(dest: S, netmask: S, gateway: S) -> Self { 253 | Self { 254 | dest, 255 | gateway, 256 | netmask, 257 | } 258 | } 259 | 260 | #[must_use] 261 | pub(super) fn new_for_interface(dest: S, netmask: S, if_name: &str) -> Self { 262 | Self { 263 | dest, 264 | gateway: if_addr(if_name).unwrap_or_default(), 265 | netmask, 266 | } 267 | } 268 | } 269 | 270 | impl RtMessage { 271 | #[must_use] 272 | pub(super) fn new_for_gateway() -> Self { 273 | let header = RtMsgHdr::new( 274 | size_of::() as u16, 275 | MessageType::Get, 276 | 0, 277 | RTF_UP | RTF_GATEWAY | RTF_STATIC, 278 | RTA_DST | RTA_IFA, 279 | ); 280 | 281 | Self { 282 | header, 283 | payload: Payload::default(), 284 | } 285 | } 286 | } 287 | 288 | impl RtMessage { 289 | #[must_use] 290 | pub(super) fn new_for_add_gateway(payload: Payload, is_host: bool, is_blackhole: bool) -> Self { 291 | let mut flags = RTF_UP | RTF_STATIC | RTF_GATEWAY; 292 | if is_host { 293 | flags |= RTF_HOST; 294 | } 295 | if is_blackhole { 296 | flags |= RTF_BLACKHOLE; 297 | } 298 | let header = RtMsgHdr::new( 299 | size_of::() as u16, 300 | MessageType::Add, 301 | 0, 302 | flags, 303 | RTA_DST | RTA_GATEWAY | RTA_NETMASK, 304 | ); 305 | 306 | Self { header, payload } 307 | } 308 | 309 | // TODO: check if gateway field and flags are needed. 310 | #[must_use] 311 | pub(super) fn new_for_delete_gateway(payload: Payload, is_host: bool) -> Self { 312 | let mut flags = RTF_UP | RTF_STATIC | RTF_GATEWAY; 313 | if is_host { 314 | flags |= RTF_HOST; 315 | } 316 | let header = RtMsgHdr::new( 317 | size_of::() as u16, 318 | MessageType::Delete, 319 | 0, 320 | flags, 321 | RTA_DST | RTA_GATEWAY | RTA_NETMASK, 322 | ); 323 | 324 | Self { header, payload } 325 | } 326 | 327 | #[must_use] 328 | pub(super) fn new_for_add(if_index: u16, payload: Payload) -> Self { 329 | let header = RtMsgHdr::new( 330 | size_of::() as u16, 331 | MessageType::Add, 332 | if_index, 333 | RTF_UP | RTF_STATIC | RTF_CLONING, 334 | RTA_DST | RTA_GATEWAY | RTA_NETMASK, 335 | ); 336 | 337 | Self { header, payload } 338 | } 339 | 340 | #[must_use] 341 | pub(super) fn new_for_delete(if_index: u16, payload: Payload) -> Self { 342 | let header = RtMsgHdr::new( 343 | size_of::() as u16, 344 | MessageType::Delete, 345 | if_index, 346 | RTF_UP | RTF_STATIC | RTF_CLONING, 347 | RTA_DST | RTA_GATEWAY | RTA_NETMASK, 348 | ); 349 | 350 | Self { header, payload } 351 | } 352 | 353 | pub(super) fn execute(&self) -> Result<(), IoError> { 354 | let socket = socket(AddressFamily::Route, SockType::Raw, SockFlag::empty(), None) 355 | .map_err(IoError::WriteIo)?; 356 | // Don't want to read back our messages. 357 | shutdown(socket.as_raw_fd(), Shutdown::Read).map_err(IoError::WriteIo)?; 358 | let buf = unsafe { cast_bytes(self) }; 359 | match write(socket.as_fd(), buf) { 360 | Ok(_) | Err(Errno::ESRCH) => Ok(()), // not in table 361 | Err(err) => Err(IoError::WriteIo(err)), 362 | } 363 | } 364 | 365 | pub(super) fn get_gateway(&self) -> Result, IoError> { 366 | let socket = socket(AddressFamily::Route, SockType::Raw, SockFlag::empty(), None) 367 | .map_err(IoError::WriteIo)?; 368 | let buf = unsafe { cast_bytes(self) }; 369 | match write(socket.as_fd(), buf) { 370 | Ok(_) => (), 371 | Err(Errno::ESRCH) => return Ok(None), // not in table 372 | Err(err) => return Err(IoError::WriteIo(err)), 373 | } 374 | 375 | let mut buf = [0u8; 256]; // FIXME: fixed buffer size 376 | let len = read(socket.as_fd(), &mut buf).map_err(IoError::ReadIo)?; 377 | if len < size_of::() { 378 | return Err(IoError::Unpack); 379 | } 380 | 381 | let header = unsafe { cast_ref::(&buf) }; 382 | 383 | let mut offset = size_of::(); 384 | if header.rtm_addrs & RTA_DST != 0 { 385 | let len = (&buf[offset..])[0] as usize; 386 | offset += if len > 0 { len } else { 4 }; 387 | } 388 | if header.rtm_addrs & RTA_GATEWAY != 0 { 389 | let addr = unpack_sockaddr(&buf[offset..]); 390 | if let Some(addr) = addr { 391 | return Ok(Some(addr.ip())); 392 | } 393 | let len = (&buf[offset..])[0] as usize; 394 | offset += if len > 0 { len } else { 4 }; 395 | } 396 | if header.rtm_addrs & RTA_NETMASK != 0 { 397 | let len = (&buf[offset..])[0] as usize; 398 | offset += if len > 0 { len } else { 4 }; 399 | } 400 | if header.rtm_addrs & RTA_GENMASK != 0 { 401 | let len = (&buf[offset..])[0] as usize; 402 | offset += if len > 0 { len } else { 4 }; 403 | } 404 | if header.rtm_addrs & RTA_IFP != 0 { 405 | let len = (&buf[offset..])[0] as usize; 406 | offset += if len > 0 { len } else { 4 }; 407 | } 408 | if header.rtm_addrs & RTA_IFA != 0 { 409 | return Ok(unpack_sockaddr(&buf[offset..]).map(|addr| addr.ip())); 410 | } 411 | 412 | Ok(None) 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/bsd/sockaddr.rs: -------------------------------------------------------------------------------- 1 | //! Convert binary `sockaddr_in` or `sockaddr_in6` (see netinet/in.h) to `SocketAddr`. 2 | use std::{ 3 | mem::{size_of, zeroed}, 4 | net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, 5 | ptr::{copy, from_mut}, 6 | }; 7 | 8 | use super::{cast_bytes, cast_ref}; 9 | 10 | // Note: these values differ across different platforms. 11 | const AF_INET: u8 = libc::AF_INET as u8; 12 | const AF_INET6: u8 = libc::AF_INET6 as u8; 13 | const AF_LINK: u8 = libc::AF_LINK as u8; 14 | const SA_IN_SIZE: u8 = size_of::() as u8; 15 | const SA_IN6_SIZE: u8 = size_of::() as u8; 16 | 17 | pub(super) trait SocketFromRaw { 18 | unsafe fn from_raw(addr: *const libc::sockaddr) -> Option 19 | where 20 | Self: Sized; 21 | } 22 | 23 | /// `struct sockaddr_in` from `netinet/in.h` 24 | #[repr(C)] 25 | pub(super) struct SockAddrIn { 26 | len: u8, 27 | family: u8, 28 | port: u16, 29 | addr: [u8; 4], 30 | zero: [u8; 8], 31 | } 32 | 33 | impl SocketFromRaw for SockAddrIn { 34 | /// Construct `SockAddrIn` from `libc::sockaddr`. 35 | unsafe fn from_raw(addr: *const libc::sockaddr) -> Option { 36 | if addr.is_null() || (*addr).sa_family != AF_INET { 37 | None 38 | } else { 39 | let mut sockaddr: Self = zeroed(); 40 | copy( 41 | addr.cast::(), 42 | from_mut::(&mut sockaddr).cast::(), 43 | (*addr).sa_len as usize, 44 | ); 45 | Some(sockaddr) 46 | } 47 | } 48 | } 49 | 50 | impl Default for SockAddrIn { 51 | fn default() -> Self { 52 | Self { 53 | len: SA_IN_SIZE, 54 | family: AF_INET, 55 | port: 0, 56 | addr: [0u8; 4], 57 | zero: [0u8; 8], 58 | } 59 | } 60 | } 61 | 62 | impl From<&SockAddrIn> for SocketAddr { 63 | fn from(sa: &SockAddrIn) -> Self { 64 | Self::V4(SocketAddrV4::new( 65 | Ipv4Addr::from(sa.addr), 66 | u16::from_be(sa.port), 67 | )) 68 | } 69 | } 70 | 71 | impl From<&SocketAddrV4> for SockAddrIn { 72 | fn from(sa: &SocketAddrV4) -> Self { 73 | Self { 74 | len: SA_IN_SIZE, 75 | family: AF_INET, 76 | port: sa.port().to_be(), 77 | addr: sa.ip().octets(), 78 | zero: [0u8; 8], 79 | } 80 | } 81 | } 82 | 83 | impl From for SockAddrIn { 84 | fn from(ip: Ipv4Addr) -> Self { 85 | Self { 86 | len: SA_IN_SIZE, 87 | family: AF_INET, 88 | port: 0, 89 | addr: ip.octets(), 90 | zero: [0u8; 8], 91 | } 92 | } 93 | } 94 | 95 | /// `struct sockaddr_in6` from `netinet6/in6.h` 96 | #[repr(C)] 97 | pub(super) struct SockAddrIn6 { 98 | len: u8, 99 | family: u8, 100 | port: u16, 101 | flowinfo: u32, 102 | addr: [u8; 16], 103 | scope_id: u32, 104 | } 105 | 106 | impl SockAddrIn6 { 107 | /// This is needed for assigning IPv6 address to a network interface. 108 | /// Note, `len` and `family` fields are zero. 109 | #[must_use] 110 | pub(super) fn zeroed() -> Self { 111 | Self { 112 | len: 0, 113 | family: 0, 114 | port: 0, 115 | flowinfo: 0, 116 | addr: [0u8; 16], 117 | scope_id: 0, 118 | } 119 | } 120 | } 121 | 122 | impl SocketFromRaw for SockAddrIn6 { 123 | /// Construct `SockAddrIn6` from `libc::sockaddr`. 124 | unsafe fn from_raw(addr: *const libc::sockaddr) -> Option { 125 | if addr.is_null() || (*addr).sa_family != AF_INET6 { 126 | None 127 | } else { 128 | let mut sockaddr: Self = zeroed(); 129 | copy( 130 | addr.cast::(), 131 | from_mut::(&mut sockaddr).cast::(), 132 | (*addr).sa_len as usize, 133 | ); 134 | Some(sockaddr) 135 | } 136 | } 137 | } 138 | 139 | impl Default for SockAddrIn6 { 140 | fn default() -> Self { 141 | Self { 142 | len: SA_IN6_SIZE, 143 | family: AF_INET6, 144 | port: 0, 145 | flowinfo: 0, 146 | addr: [0u8; 16], 147 | scope_id: 0, 148 | } 149 | } 150 | } 151 | 152 | impl From<&SockAddrIn6> for SocketAddr { 153 | fn from(sa: &SockAddrIn6) -> Self { 154 | Self::V6(SocketAddrV6::new( 155 | Ipv6Addr::from(sa.addr), 156 | u16::from_be(sa.port), 157 | u32::from_be(sa.flowinfo), 158 | u32::from_be(sa.scope_id), 159 | )) 160 | } 161 | } 162 | 163 | impl From<&SocketAddrV6> for SockAddrIn6 { 164 | fn from(sa: &SocketAddrV6) -> Self { 165 | Self { 166 | len: SA_IN6_SIZE, 167 | family: AF_INET6, 168 | port: sa.port().to_be(), 169 | flowinfo: sa.flowinfo().to_be(), 170 | addr: sa.ip().octets(), 171 | scope_id: sa.scope_id().to_be(), 172 | } 173 | } 174 | } 175 | 176 | impl From for SockAddrIn6 { 177 | fn from(ip: Ipv6Addr) -> Self { 178 | Self { 179 | len: SA_IN6_SIZE, 180 | family: AF_INET6, 181 | port: 0, 182 | flowinfo: 0, 183 | addr: ip.octets(), 184 | scope_id: 0, 185 | } 186 | } 187 | } 188 | 189 | pub(super) fn pack_sockaddr(sockaddr: &SocketAddr) -> Vec { 190 | match sockaddr { 191 | SocketAddr::V4(sockaddr_v4) => { 192 | let sockaddr_in: SockAddrIn = sockaddr_v4.into(); 193 | let bytes = unsafe { cast_bytes(&sockaddr_in) }; 194 | Vec::from(bytes) 195 | } 196 | SocketAddr::V6(sockaddr_v6) => { 197 | let sockaddr_in6: SockAddrIn6 = sockaddr_v6.into(); 198 | let bytes = unsafe { cast_bytes(&sockaddr_in6) }; 199 | Vec::from(bytes) 200 | } 201 | } 202 | } 203 | 204 | pub(super) fn unpack_sockaddr(buf: &[u8]) -> Option { 205 | match buf.first() { 206 | Some(&SA_IN_SIZE) => { 207 | let sockaddr_in = unsafe { cast_ref::(buf) }; 208 | // sanity checks 209 | if sockaddr_in.family == AF_INET { 210 | Some(sockaddr_in.into()) 211 | } else { 212 | None 213 | } 214 | } 215 | Some(&SA_IN6_SIZE) => { 216 | let sockaddr_in6 = unsafe { cast_ref::(buf) }; 217 | // sanity checks 218 | if sockaddr_in6.family == AF_INET6 { 219 | Some(sockaddr_in6.into()) 220 | } else { 221 | None 222 | } 223 | } 224 | _ => None, 225 | } 226 | } 227 | 228 | /// `struct sockaddr_dl` from `net/if_dl.h` 229 | #[derive(Clone)] 230 | #[repr(C)] 231 | pub(super) struct SockAddrDl { 232 | len: u8, 233 | family: u8, 234 | index: u16, 235 | r#type: u8, 236 | nlen: u8, 237 | alen: u8, 238 | slen: u8, 239 | data: [u8; 12], 240 | } 241 | 242 | impl SockAddrDl { 243 | #[must_use] 244 | pub(super) fn new(index: u16) -> Self { 245 | Self { 246 | len: size_of::() as u8, 247 | family: AF_LINK, 248 | index, 249 | r#type: 0, 250 | nlen: 0, 251 | alen: 0, 252 | slen: 0, 253 | data: [0u8; 12], 254 | } 255 | } 256 | } 257 | 258 | #[cfg(test)] 259 | mod tests { 260 | use std::net::IpAddr; 261 | 262 | use super::*; 263 | 264 | #[test] 265 | fn pack_ip4() { 266 | let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 12, 34)), 7301); 267 | let buf = pack_sockaddr(&addr); 268 | assert_eq!( 269 | buf, 270 | [16, 2, 28, 133, 192, 168, 12, 34, 0, 0, 0, 0, 0, 0, 0, 0] 271 | ); 272 | } 273 | 274 | #[test] 275 | fn unpack_ip4() { 276 | let buf = [16, 2, 28, 133, 192, 168, 12, 34, 0, 0, 0, 0, 0, 0, 0, 0]; 277 | let addr = unpack_sockaddr(&buf).unwrap(); 278 | assert_eq!(addr.port(), 7301); 279 | assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::new(192, 168, 12, 34))); 280 | } 281 | 282 | #[test] 283 | fn pack_ip6() { 284 | let addr = SocketAddr::new( 285 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0c22)), 286 | 7301, 287 | ); 288 | let buf = pack_sockaddr(&addr); 289 | assert_eq!( 290 | buf, 291 | [ 292 | 28, AF_INET6, 28, 133, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 192, 293 | 168, 12, 34, 0, 0, 0, 0, 294 | ] 295 | ); 296 | } 297 | 298 | #[test] 299 | fn unpack_ip6() { 300 | let buf = [ 301 | 28, AF_INET6, 28, 133, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 192, 168, 302 | 12, 34, 0, 0, 0, 0, 303 | ]; 304 | let addr = unpack_sockaddr(&buf).unwrap(); 305 | assert_eq!(addr.port(), 7301); 306 | assert_eq!( 307 | addr.ip(), 308 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0c22)) 309 | ); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/bsd/timespec.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem::size_of, 3 | time::{Duration, SystemTime}, 4 | }; 5 | 6 | use super::{cast_bytes, cast_ref}; 7 | 8 | #[repr(C)] 9 | struct TimeSpec { 10 | tv_sec: i64, 11 | tv_nsec: i64, 12 | } 13 | 14 | impl TimeSpec { 15 | fn duration(&self) -> Duration { 16 | Duration::from_secs(self.tv_sec as u64) + Duration::from_nanos(self.tv_nsec as u64) 17 | } 18 | } 19 | 20 | impl From<&TimeSpec> for SystemTime { 21 | fn from(time_spec: &TimeSpec) -> SystemTime { 22 | SystemTime::UNIX_EPOCH + time_spec.duration() 23 | } 24 | } 25 | 26 | impl From<&SystemTime> for TimeSpec { 27 | fn from(system_time: &SystemTime) -> Self { 28 | if let Ok(duration) = system_time.duration_since(SystemTime::UNIX_EPOCH) { 29 | Self { 30 | tv_sec: duration.as_secs() as i64, 31 | tv_nsec: duration.as_nanos() as i64, 32 | } 33 | } else { 34 | Self { 35 | tv_sec: 0, 36 | tv_nsec: 0, 37 | } 38 | } 39 | } 40 | } 41 | 42 | pub(super) fn pack_timespec(system_time: &SystemTime) -> Vec { 43 | let timespec: TimeSpec = system_time.into(); 44 | let bytes = unsafe { cast_bytes(×pec) }; 45 | Vec::from(bytes) 46 | } 47 | 48 | pub(super) fn unpack_timespec(buf: &[u8]) -> Option { 49 | const TS_SIZE: usize = size_of::(); 50 | match buf.len() { 51 | TS_SIZE => { 52 | let ts = unsafe { cast_ref::(buf) }; 53 | Some(ts.into()) 54 | } 55 | _ => None, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/bsd/wgio.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | alloc::{alloc, dealloc, Layout}, 3 | os::fd::AsRawFd, 4 | ptr::null_mut, 5 | slice::from_raw_parts, 6 | }; 7 | 8 | use libc::IF_NAMESIZE; 9 | use nix::{ioctl_readwrite, sys::socket::AddressFamily}; 10 | 11 | use super::{create_socket, IoError}; 12 | 13 | // FIXME: `WgReadIo` and `WgWriteIo` have to be declared public. 14 | ioctl_readwrite!(write_wireguard_data, b'i', 210, WgWriteIo); 15 | ioctl_readwrite!(read_wireguard_data, b'i', 211, WgReadIo); 16 | 17 | /// Represent `struct wg_data_io` defined in 18 | /// https://github.com/freebsd/freebsd-src/blob/main/sys/dev/wg/if_wg.h 19 | #[repr(C)] 20 | pub struct WgReadIo { 21 | wgd_name: [u8; IF_NAMESIZE], 22 | wgd_data: *mut u8, // *void 23 | wgd_size: usize, 24 | } 25 | 26 | impl WgReadIo { 27 | /// Create `WgReadIo` without data buffer. 28 | #[must_use] 29 | pub fn new(if_name: &str) -> Self { 30 | let mut wgd_name = [0u8; IF_NAMESIZE]; 31 | if_name 32 | .bytes() 33 | .take(IF_NAMESIZE - 1) 34 | .enumerate() 35 | .for_each(|(i, b)| wgd_name[i] = b); 36 | Self { 37 | wgd_name, 38 | wgd_data: null_mut(), 39 | wgd_size: 0, 40 | } 41 | } 42 | 43 | /// Allocate data buffer. 44 | fn alloc_data(&mut self) -> Result<(), IoError> { 45 | if self.wgd_data.is_null() { 46 | if let Ok(layout) = Layout::array::(self.wgd_size) { 47 | unsafe { 48 | self.wgd_data = alloc(layout); 49 | } 50 | return Ok(()); 51 | } 52 | } 53 | Err(IoError::MemAlloc) 54 | } 55 | 56 | /// Return buffer as slice. 57 | pub(super) fn as_slice<'a>(&self) -> &'a [u8] { 58 | unsafe { from_raw_parts(self.wgd_data, self.wgd_size) } 59 | } 60 | 61 | pub(super) fn read_data(&mut self) -> Result<(), IoError> { 62 | let socket = create_socket(AddressFamily::Unix).map_err(IoError::ReadIo)?; 63 | unsafe { 64 | // First do ioctl with empty `wg_data` to obtain buffer size. 65 | if let Err(err) = read_wireguard_data(socket.as_raw_fd(), self) { 66 | error!("WgReadIo first read error {err}"); 67 | return Err(IoError::ReadIo(err)); 68 | } 69 | // Allocate buffer. 70 | self.alloc_data()?; 71 | // Second call to ioctl with allocated buffer. 72 | if let Err(err) = read_wireguard_data(socket.as_raw_fd(), self) { 73 | error!("WgReadIo second read error {err}"); 74 | return Err(IoError::ReadIo(err)); 75 | } 76 | } 77 | 78 | Ok(()) 79 | } 80 | } 81 | 82 | impl Drop for WgReadIo { 83 | fn drop(&mut self) { 84 | if self.wgd_size != 0 { 85 | let layout = Layout::array::(self.wgd_size).expect("Bad layout"); 86 | unsafe { 87 | dealloc(self.wgd_data, layout); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /// Same data layout as `WgReadIo`, but avoid `Drop`. 94 | #[repr(C)] 95 | pub struct WgWriteIo { 96 | wgd_name: [u8; IF_NAMESIZE], 97 | wgd_data: *mut u8, // *void 98 | wgd_size: usize, 99 | } 100 | 101 | impl WgWriteIo { 102 | /// Create `WgWriteIo` from slice. 103 | #[must_use] 104 | pub fn new(if_name: &str, buf: &mut [u8]) -> Self { 105 | let mut wgd_name = [0u8; IF_NAMESIZE]; 106 | if_name 107 | .bytes() 108 | .take(IF_NAMESIZE - 1) 109 | .enumerate() 110 | .for_each(|(i, b)| wgd_name[i] = b); 111 | Self { 112 | wgd_name, 113 | wgd_data: buf.as_mut_ptr(), 114 | wgd_size: buf.len(), 115 | } 116 | } 117 | 118 | pub(super) fn write_data(&mut self) -> Result<(), IoError> { 119 | let socket = create_socket(AddressFamily::Unix).map_err(IoError::WriteIo)?; 120 | unsafe { 121 | if let Err(err) = write_wireguard_data(socket.as_raw_fd(), self) { 122 | error!("WgWriteIo write error {err}"); 123 | return Err(IoError::WriteIo(err)); 124 | } 125 | } 126 | 127 | Ok(()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use crate::error::WireguardInterfaceError; 4 | 5 | #[cfg(target_os = "linux")] 6 | const COMMANDS: [&str; 2] = ["resolvconf", "ip"]; 7 | 8 | #[cfg(target_os = "windows")] 9 | const COMMANDS: [&str; 1] = [("wireguard.exe")]; 10 | 11 | #[cfg(target_os = "macos")] 12 | const COMMANDS: [&str; 2] = ["wireguard-go", "networksetup"]; 13 | 14 | #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] 15 | const COMMANDS: [&str; 1] = ["resolvconf"]; 16 | 17 | pub(crate) fn check_external_dependencies() -> Result<(), WireguardInterfaceError> { 18 | debug!("Checking if all commands required by wireguard-rs are available"); 19 | let paths = env::var_os("PATH").ok_or(WireguardInterfaceError::MissingDependency( 20 | "Environment variable `PATH` not found".into(), 21 | ))?; 22 | 23 | // Find the missing command to provide a more informative error message later. 24 | let missing = COMMANDS.iter().find(|cmd| { 25 | !env::split_paths(&paths).any(|dir| { 26 | trace!("Trying to find {cmd} in {dir:?}"); 27 | match dir.join(cmd).try_exists() { 28 | Ok(true) => { 29 | debug!("{cmd} found in {dir:?}"); 30 | true 31 | } 32 | Ok(false) => { 33 | trace!("{cmd} not found in {dir:?}"); 34 | false 35 | } 36 | Err(err) => { 37 | warn!("Error while checking for {cmd} in {dir:?}: {err}"); 38 | false 39 | } 40 | } 41 | }) 42 | }); 43 | 44 | if let Some(cmd) = missing { 45 | Err(WireguardInterfaceError::MissingDependency(format!( 46 | "Command `{cmd}` required by wireguard-rs couldn't be found. The following directories were checked: {paths:?}" 47 | ))) 48 | } else { 49 | debug!("All commands required by wireguard-rs are available"); 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Interface management errors 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | #[non_exhaustive] 7 | pub enum WireguardInterfaceError { 8 | #[error("Interface setup error: {0}")] 9 | Interface(String), 10 | #[error("Command execution failed")] 11 | CommandExecutionFailed(#[from] std::io::Error), 12 | #[error("WireGuard key error")] 13 | KeyDecode(#[from] base64::DecodeError), 14 | #[error("Command returned error status: `{stdout}`")] 15 | CommandExecutionError { stdout: String, stderr: String }, 16 | #[error("IP address/mask error")] 17 | IpAddrMask(#[from] crate::net::IpAddrParseError), 18 | #[error("Required dependency not found, details: {0}")] 19 | MissingDependency(String), 20 | #[error("Unix socket error: {0}")] 21 | UnixSockerError(String), 22 | #[error("Peer configuration error: {0}")] 23 | PeerConfigurationError(String), 24 | #[error("Interface data read error: {0}")] 25 | ReadInterfaceError(String), 26 | #[error("Netlink error: {0}")] 27 | NetlinkError(String), 28 | #[error("BSD error: {0}")] 29 | BsdError(String), 30 | #[error("Userspace support is not available on this platform")] 31 | UserspaceNotSupported, 32 | #[error("Kernel support is not available on this platform")] 33 | KernelNotSupported, 34 | #[error("DNS error: {0}")] 35 | DnsError(String), 36 | #[cfg(target_os = "windows")] 37 | #[error("Service installation failed: `{0}`")] 38 | ServiceInstallationFailed(String), 39 | #[cfg(target_os = "windows")] 40 | #[error("Tunnel service removal failed: `{0}`")] 41 | ServiceRemovalFailed(String), 42 | #[error("Socket is closed: {0}")] 43 | SocketClosed(String), 44 | } 45 | -------------------------------------------------------------------------------- /src/host.rs: -------------------------------------------------------------------------------- 1 | //! Host interface configuration 2 | 3 | use std::{ 4 | collections::HashMap, 5 | fmt::{Debug, Formatter}, 6 | io::{self, BufRead, BufReader, Read}, 7 | net::SocketAddr, 8 | str::FromStr, 9 | time::{Duration, SystemTime}, 10 | }; 11 | 12 | #[cfg(target_os = "linux")] 13 | use netlink_packet_wireguard::{ 14 | constants::{WGDEVICE_F_REPLACE_PEERS, WGPEER_F_REPLACE_ALLOWEDIPS}, 15 | nlas::{WgAllowedIpAttrs, WgDeviceAttrs, WgPeer, WgPeerAttrs}, 16 | }; 17 | #[cfg(feature = "serde")] 18 | use serde::{Deserialize, Serialize}; 19 | 20 | use crate::{error::WireguardInterfaceError, key::Key, net::IpAddrMask, utils::resolve}; 21 | 22 | /// WireGuard peer representation. 23 | #[derive(Clone, Debug, Default, PartialEq)] 24 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 25 | pub struct Peer { 26 | pub public_key: Key, 27 | pub preshared_key: Option, 28 | pub protocol_version: Option, 29 | pub endpoint: Option, 30 | pub last_handshake: Option, 31 | pub tx_bytes: u64, 32 | pub rx_bytes: u64, 33 | pub persistent_keepalive_interval: Option, 34 | pub allowed_ips: Vec, 35 | } 36 | 37 | impl Peer { 38 | /// Create new `Peer` with a given `public_key`. 39 | #[must_use] 40 | pub fn new(public_key: Key) -> Self { 41 | Self { 42 | public_key, 43 | preshared_key: None, 44 | protocol_version: None, 45 | endpoint: None, 46 | last_handshake: None, 47 | tx_bytes: 0, 48 | rx_bytes: 0, 49 | persistent_keepalive_interval: None, 50 | allowed_ips: Vec::new(), 51 | } 52 | } 53 | 54 | pub fn set_allowed_ips(&mut self, allowed_ips: Vec) { 55 | self.allowed_ips = allowed_ips; 56 | } 57 | 58 | /// Resolves endpoint address to [`SocketAddr`] and sets the field 59 | pub fn set_endpoint(&mut self, endpoint: &str) -> Result<(), WireguardInterfaceError> { 60 | self.endpoint = Some(resolve(endpoint)?); 61 | Ok(()) 62 | } 63 | 64 | #[must_use] 65 | pub fn as_uapi_update(&self) -> String { 66 | let mut output = format!("public_key={}\n", self.public_key.to_lower_hex()); 67 | if let Some(key) = &self.preshared_key { 68 | output.push_str("preshared_key="); 69 | output.push_str(&key.to_lower_hex()); 70 | output.push('\n'); 71 | } 72 | if let Some(endpoint) = &self.endpoint { 73 | output.push_str("endpoint="); 74 | output.push_str(&endpoint.to_string()); 75 | output.push('\n'); 76 | } 77 | if let Some(interval) = &self.persistent_keepalive_interval { 78 | output.push_str("persistent_keepalive_interval="); 79 | output.push_str(&interval.to_string()); 80 | output.push('\n'); 81 | } 82 | output.push_str("replace_allowed_ips=true\n"); 83 | for allowed_ip in &self.allowed_ips { 84 | output.push_str("allowed_ip="); 85 | output.push_str(&allowed_ip.to_string()); 86 | output.push('\n'); 87 | } 88 | 89 | output 90 | } 91 | 92 | #[must_use] 93 | pub fn as_uapi_remove(&self) -> String { 94 | format!( 95 | "public_key={}\nremove=true\n", 96 | self.public_key.to_lower_hex() 97 | ) 98 | } 99 | } 100 | 101 | #[cfg(target_os = "linux")] 102 | impl Peer { 103 | #[must_use] 104 | pub(crate) fn from_nlas(nlas: &[WgPeerAttrs]) -> Self { 105 | let mut peer = Self::default(); 106 | 107 | for nla in nlas { 108 | match nla { 109 | WgPeerAttrs::PublicKey(value) => peer.public_key = Key::new(*value), 110 | WgPeerAttrs::PresharedKey(value) => peer.preshared_key = Some(Key::new(*value)), 111 | WgPeerAttrs::Endpoint(value) => peer.endpoint = Some(*value), 112 | WgPeerAttrs::PersistentKeepalive(value) => { 113 | peer.persistent_keepalive_interval = Some(*value); 114 | } 115 | WgPeerAttrs::LastHandshake(value) => peer.last_handshake = Some(*value), 116 | WgPeerAttrs::RxBytes(value) => peer.rx_bytes = *value, 117 | WgPeerAttrs::TxBytes(value) => peer.tx_bytes = *value, 118 | WgPeerAttrs::AllowedIps(nlas) => { 119 | for nla in nlas { 120 | let ip = nla.iter().find_map(|nla| match nla { 121 | WgAllowedIpAttrs::IpAddr(ip) => Some(*ip), 122 | _ => None, 123 | }); 124 | let cidr = nla.iter().find_map(|nla| match nla { 125 | WgAllowedIpAttrs::Cidr(cidr) => Some(*cidr), 126 | _ => None, 127 | }); 128 | if let (Some(ip), Some(cidr)) = (ip, cidr) { 129 | peer.allowed_ips.push(IpAddrMask::new(ip, cidr)); 130 | } 131 | } 132 | } 133 | _ => (), 134 | } 135 | } 136 | 137 | peer 138 | } 139 | 140 | #[must_use] 141 | pub(crate) fn as_nlas(&self, ifname: &str) -> Vec { 142 | vec![ 143 | WgDeviceAttrs::IfName(ifname.into()), 144 | WgDeviceAttrs::Peers(vec![self.as_nlas_peer()]), 145 | ] 146 | } 147 | 148 | #[must_use] 149 | pub(crate) fn as_nlas_peer(&self) -> WgPeer { 150 | let mut attrs = vec![WgPeerAttrs::PublicKey(self.public_key.as_array())]; 151 | if let Some(keepalive) = self.persistent_keepalive_interval { 152 | attrs.push(WgPeerAttrs::PersistentKeepalive(keepalive)); 153 | } 154 | 155 | if let Some(endpoint) = self.endpoint { 156 | attrs.push(WgPeerAttrs::Endpoint(endpoint)); 157 | } 158 | 159 | if let Some(preshared_key) = &self.preshared_key { 160 | attrs.push(WgPeerAttrs::PresharedKey(preshared_key.as_array())); 161 | } 162 | 163 | attrs.push(WgPeerAttrs::Flags(WGPEER_F_REPLACE_ALLOWEDIPS)); 164 | let allowed_ips = self 165 | .allowed_ips 166 | .iter() 167 | .map(IpAddrMask::to_nlas_allowed_ip) 168 | .collect(); 169 | attrs.push(WgPeerAttrs::AllowedIps(allowed_ips)); 170 | 171 | WgPeer(attrs) 172 | } 173 | } 174 | 175 | /// WireGuard host representation. 176 | #[derive(Clone, Default)] 177 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 178 | pub struct Host { 179 | pub listen_port: u16, 180 | pub private_key: Option, 181 | pub(super) fwmark: Option, 182 | pub peers: HashMap, 183 | } 184 | 185 | // implement manually to avoid exposing private keys 186 | impl Debug for Host { 187 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 188 | f.debug_struct("Host") 189 | .field("listen_port", &self.listen_port) 190 | .field("fwmark", &self.fwmark) 191 | .field("peers", &self.peers) 192 | .finish_non_exhaustive() 193 | } 194 | } 195 | 196 | impl Host { 197 | /// Create new `Host` with a given `listen_port` and `private_key`. 198 | #[must_use] 199 | pub fn new(listen_port: u16, private_key: Key) -> Self { 200 | Self { 201 | listen_port, 202 | private_key: Some(private_key), 203 | fwmark: None, 204 | peers: HashMap::new(), 205 | } 206 | } 207 | 208 | #[must_use] 209 | pub fn as_uapi(&self) -> String { 210 | let mut output = format!("listen_port={}\n", self.listen_port); 211 | if let Some(key) = &self.private_key { 212 | output.push_str("private_key="); 213 | output.push_str(&key.to_lower_hex()); 214 | output.push('\n'); 215 | } 216 | if let Some(fwmark) = &self.fwmark { 217 | output.push_str("fwmark="); 218 | output.push_str(&fwmark.to_string()); 219 | output.push('\n'); 220 | } 221 | output.push_str("replace_peers=true\n"); 222 | for peer in self.peers.values() { 223 | output.push_str(peer.as_uapi_update().as_ref()); 224 | } 225 | 226 | output 227 | } 228 | 229 | // TODO: use custom Error 230 | pub fn parse_uapi(buf: impl Read) -> io::Result { 231 | let reader = BufReader::new(buf); 232 | let mut host = Self::default(); 233 | let mut peer_ref = None; 234 | 235 | for line_result in reader.lines() { 236 | let line = match line_result { 237 | Ok(line) => line, 238 | Err(err) => { 239 | error!("Error parsing buffer line: {err}"); 240 | continue; 241 | } 242 | }; 243 | if let Some((keyword, value)) = line.split_once('=') { 244 | match keyword { 245 | "listen_port" => host.listen_port = value.parse().unwrap_or_default(), 246 | "fwmark" => host.fwmark = value.parse().ok(), 247 | "private_key" => host.private_key = Key::decode(value).ok(), 248 | // "public_key" starts new peer definition 249 | "public_key" => { 250 | if let Ok(key) = Key::decode(value) { 251 | let peer = Peer::new(key.clone()); 252 | host.peers.insert(key.clone(), peer); 253 | peer_ref = host.peers.get_mut(&key); 254 | } else { 255 | peer_ref = None; 256 | } 257 | } 258 | "preshared_key" => { 259 | if let Some(ref mut peer) = peer_ref { 260 | peer.preshared_key = Key::decode(value).ok(); 261 | } 262 | } 263 | "protocol_version" => { 264 | if let Some(ref mut peer) = peer_ref { 265 | peer.protocol_version = value.parse().ok(); 266 | } 267 | } 268 | "endpoint" => { 269 | if let Some(ref mut peer) = peer_ref { 270 | peer.endpoint = SocketAddr::from_str(value).ok(); 271 | } 272 | } 273 | "persistent_keepalive_interval" => { 274 | if let Some(ref mut peer) = peer_ref { 275 | peer.persistent_keepalive_interval = value.parse().ok(); 276 | } 277 | } 278 | "allowed_ip" => { 279 | if let Some(ref mut peer) = peer_ref { 280 | if let Ok(addr) = value.parse() { 281 | peer.allowed_ips.push(addr); 282 | } 283 | } 284 | } 285 | "last_handshake_time_sec" => { 286 | if let Some(ref mut peer) = peer_ref { 287 | let handshake = 288 | peer.last_handshake.get_or_insert(SystemTime::UNIX_EPOCH); 289 | *handshake += Duration::from_secs(value.parse().unwrap_or_default()); 290 | } 291 | } 292 | "last_handshake_time_nsec" => { 293 | if let Some(ref mut peer) = peer_ref { 294 | let handshake = 295 | peer.last_handshake.get_or_insert(SystemTime::UNIX_EPOCH); 296 | *handshake += Duration::from_nanos(value.parse().unwrap_or_default()); 297 | } 298 | } 299 | "rx_bytes" => { 300 | if let Some(ref mut peer) = peer_ref { 301 | peer.rx_bytes = value.parse().unwrap_or_default(); 302 | } 303 | } 304 | "tx_bytes" => { 305 | if let Some(ref mut peer) = peer_ref { 306 | peer.tx_bytes = value.parse().unwrap_or_default(); 307 | } 308 | } 309 | // "errno" ends config 310 | "errno" => { 311 | if let Ok(errno) = value.parse::() { 312 | if errno == 0 { 313 | // Break here, or BufReader will wait for EOF. 314 | break; 315 | } 316 | } 317 | return Err(io::Error::other("error reading UAPI")); 318 | } 319 | _ => error!("Unknown UAPI keyword {keyword}"), 320 | } 321 | } 322 | } 323 | 324 | Ok(host) 325 | } 326 | } 327 | 328 | #[cfg(target_os = "linux")] 329 | impl Host { 330 | pub(crate) fn append_nlas(&mut self, nlas: &[WgDeviceAttrs]) { 331 | for nla in nlas { 332 | match nla { 333 | WgDeviceAttrs::PrivateKey(value) => self.private_key = Some(Key::new(*value)), 334 | WgDeviceAttrs::ListenPort(value) => self.listen_port = *value, 335 | WgDeviceAttrs::Fwmark(value) => self.fwmark = Some(*value), 336 | WgDeviceAttrs::Peers(nlas) => { 337 | for nla in nlas { 338 | let peer = Peer::from_nlas(nla); 339 | self.peers.insert(peer.public_key.clone(), peer); 340 | } 341 | } 342 | _ => (), 343 | } 344 | } 345 | } 346 | 347 | #[must_use] 348 | pub(crate) fn as_nlas(&self, ifname: &str) -> Vec { 349 | let mut nlas = vec![ 350 | WgDeviceAttrs::IfName(ifname.into()), 351 | WgDeviceAttrs::ListenPort(self.listen_port), 352 | ]; 353 | if let Some(key) = &self.private_key { 354 | nlas.push(WgDeviceAttrs::PrivateKey(key.as_array())); 355 | } 356 | if let Some(fwmark) = &self.fwmark { 357 | nlas.push(WgDeviceAttrs::Fwmark(*fwmark)); 358 | } 359 | nlas.push(WgDeviceAttrs::Flags(WGDEVICE_F_REPLACE_PEERS)); 360 | 361 | // IMPORTANT: To avoid buffer overflow, do not add peers here. 362 | // let peers = self.peers.values().map(Peer::as_nlas_peer).collect(); 363 | // nlas.push(WgDeviceAttrs::Peers(peers)); 364 | 365 | nlas 366 | } 367 | } 368 | 369 | #[cfg(test)] 370 | mod tests { 371 | use std::io::Cursor; 372 | 373 | use super::*; 374 | 375 | #[test] 376 | fn test_parse_config() { 377 | let uapi_output = 378 | b"private_key=000102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f\n\ 379 | listen_port=7301\n\ 380 | public_key=100102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f\n\ 381 | preshared_key=0000000000000000000000000000000000000000000000000000000000000000\n\ 382 | protocol_version=1\n\ 383 | last_handshake_time_sec=0\n\ 384 | last_handshake_time_nsec=0\n\ 385 | tx_bytes=0\n\ 386 | rx_bytes=0\n\ 387 | persistent_keepalive_interval=0\n\ 388 | allowed_ip=10.6.0.12/32\n\ 389 | public_key=200102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f\n\ 390 | preshared_key=0000000000000000000000000000000000000000000000000000000000000000\n\ 391 | protocol_version=1\n\ 392 | endpoint=83.11.218.160:51421\n\ 393 | last_handshake_time_sec=1654631933\n\ 394 | last_handshake_time_nsec=862977251\n\ 395 | tx_bytes=52759980\n\ 396 | rx_bytes=3683056\n\ 397 | persistent_keepalive_interval=0\n\ 398 | allowed_ip=10.6.0.25/32\n\ 399 | public_key=300102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f\n\ 400 | preshared_key=0000000000000000000000000000000000000000000000000000000000000000\n\ 401 | protocol_version=1\n\ 402 | endpoint=31.135.163.194:37712\n\ 403 | last_handshake_time_sec=1654776419\n\ 404 | last_handshake_time_nsec=732507856\n\ 405 | tx_bytes=1009094476\n\ 406 | rx_bytes=76734328\n\ 407 | persistent_keepalive_interval=0\n\ 408 | allowed_ip=10.6.0.23/32\n\ 409 | errno=0\n"; 410 | let buf = Cursor::new(uapi_output); 411 | let host = Host::parse_uapi(buf).unwrap(); 412 | assert_eq!(host.listen_port, 7301); 413 | assert_eq!(host.peers.len(), 3); 414 | } 415 | 416 | #[test] 417 | fn test_host_uapi() { 418 | let key_str = "000102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f"; 419 | let key = Key::decode(key_str).unwrap(); 420 | 421 | let host = Host::new(12345, key); 422 | assert_eq!( 423 | "listen_port=12345\n\ 424 | private_key=000102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f\n\ 425 | replace_peers=true\n", 426 | host.as_uapi() 427 | ); 428 | } 429 | 430 | #[test] 431 | fn test_peer_uapi() { 432 | let key_str = "000102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f"; 433 | let key = Key::decode(key_str).unwrap(); 434 | 435 | let peer = Peer::new(key); 436 | assert_eq!( 437 | "public_key=000102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f\n\ 438 | replace_allowed_ips=true\n", 439 | peer.as_uapi_update() 440 | ); 441 | 442 | let key_str = "00112233445566778899aaabbcbddeeff0e1d2c3b4a5968778695a4b3c2d1e0f"; 443 | let key = Key::decode(key_str).unwrap(); 444 | let peer = Peer::new(key); 445 | assert_eq!( 446 | "public_key=00112233445566778899aaabbcbddeeff0e1d2c3b4a5968778695a4b3c2d1e0f\n\ 447 | remove=true\n", 448 | peer.as_uapi_remove() 449 | ); 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/key.rs: -------------------------------------------------------------------------------- 1 | //! Public key utilities 2 | 3 | use std::{ 4 | fmt, 5 | hash::{Hash, Hasher}, 6 | str::FromStr, 7 | }; 8 | 9 | use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; 10 | #[cfg(feature = "serde")] 11 | use serde::{ 12 | de::{Unexpected, Visitor}, 13 | Deserialize, Deserializer, Serialize, Serializer, 14 | }; 15 | use x25519_dalek::{PublicKey, StaticSecret}; 16 | 17 | const KEY_LENGTH: usize = 32; 18 | 19 | /// Returns value of hex digit, if possible. 20 | fn hex_value(char: u8) -> Option { 21 | match char { 22 | b'A'..=b'F' => Some(char - b'A' + 10), 23 | b'a'..=b'f' => Some(char - b'a' + 10), 24 | b'0'..=b'9' => Some(char - b'0'), 25 | _ => None, 26 | } 27 | } 28 | 29 | /// WireGuard key representation in binary form. 30 | #[derive(Clone, Default)] 31 | pub struct Key([u8; KEY_LENGTH]); 32 | 33 | impl Key { 34 | /// Create a new key from buffer. 35 | #[must_use] 36 | pub fn new(buf: [u8; KEY_LENGTH]) -> Self { 37 | Self(buf) 38 | } 39 | 40 | #[must_use] 41 | pub fn as_array(&self) -> [u8; KEY_LENGTH] { 42 | self.0 43 | } 44 | 45 | #[must_use] 46 | pub fn as_slice(&self) -> &[u8] { 47 | self.0.as_slice() 48 | } 49 | 50 | /// Converts `Key` to `String` of lower case hexadecimal digits. 51 | #[must_use] 52 | pub fn to_lower_hex(&self) -> String { 53 | let mut hex = String::with_capacity(64); 54 | let to_char = |nibble: u8| -> char { 55 | (match nibble { 56 | 0..=9 => b'0' + nibble, 57 | _ => nibble + b'a' - 10, 58 | }) as char 59 | }; 60 | self.0.iter().for_each(|byte| { 61 | hex.push(to_char(*byte >> 4)); 62 | hex.push(to_char(*byte & 0xf)); 63 | }); 64 | hex 65 | } 66 | 67 | /// Converts a text string of hexadecimal digits to `Key`. 68 | /// 69 | /// # Errors 70 | /// Will return `DecodeError` if text string has wrong length, 71 | /// or contains an invalid character. 72 | pub fn decode>(hex: T) -> Result { 73 | let hex = hex.as_ref(); 74 | let length = hex.len(); 75 | if length != KEY_LENGTH * 2 { 76 | return Err(DecodeError::InvalidLength(length)); 77 | } 78 | 79 | let mut key = [0; KEY_LENGTH]; 80 | for (index, chunk) in hex.chunks(2).enumerate() { 81 | let Some(msd) = hex_value(chunk[0]) else { 82 | return Err(DecodeError::InvalidByte(index, chunk[0])); 83 | }; 84 | let Some(lsd) = hex_value(chunk[1]) else { 85 | return Err(DecodeError::InvalidByte(index, chunk[1])); 86 | }; 87 | key[index] = msd << 4 | lsd; 88 | } 89 | Ok(Self(key)) 90 | } 91 | 92 | /// Generate WireGuard private key. 93 | #[must_use] 94 | pub fn generate() -> Self { 95 | Self(StaticSecret::random().to_bytes()) 96 | } 97 | 98 | /// Make WireGuard public key from a private key. 99 | #[must_use] 100 | pub fn public_key(&self) -> Self { 101 | let secret = StaticSecret::from(self.0); 102 | Self(PublicKey::from(&secret).to_bytes()) 103 | } 104 | } 105 | 106 | impl TryFrom<&str> for Key { 107 | type Error = DecodeError; 108 | 109 | /// Try to decode `Key` from base16 or base64 encoded string. 110 | fn try_from(value: &str) -> Result { 111 | if value.len() == KEY_LENGTH * 2 { 112 | // Try base16 113 | Key::decode(value) 114 | } else { 115 | // Try base64 116 | let v = BASE64_STANDARD.decode(value)?; 117 | let length = v.len(); 118 | if length == KEY_LENGTH { 119 | let buf = v 120 | .try_into() 121 | .map_err(|_| Self::Error::InvalidLength(length))?; 122 | Ok(Self::new(buf)) 123 | } else { 124 | Err(Self::Error::InvalidLength(length)) 125 | } 126 | } 127 | } 128 | } 129 | 130 | impl TryFrom<&[u8]> for Key { 131 | type Error = DecodeError; 132 | 133 | fn try_from(value: &[u8]) -> Result { 134 | let length = value.len(); 135 | if length == KEY_LENGTH { 136 | let buf = <[u8; KEY_LENGTH]>::try_from(value) 137 | .map_err(|_| Self::Error::InvalidLength(length))?; 138 | Ok(Self::new(buf)) 139 | } else { 140 | Err(Self::Error::InvalidLength(length)) 141 | } 142 | } 143 | } 144 | 145 | impl FromStr for Key { 146 | type Err = DecodeError; 147 | 148 | /// Try to decode `Key` from base16 or base64 encoded string. 149 | fn from_str(value: &str) -> Result { 150 | value.try_into() 151 | } 152 | } 153 | 154 | impl Hash for Key { 155 | fn hash(&self, state: &mut H) { 156 | self.0.hash(state); 157 | } 158 | } 159 | 160 | impl PartialEq for Key { 161 | fn eq(&self, other: &Self) -> bool { 162 | self.0 == other.0 163 | } 164 | } 165 | 166 | impl Eq for Key {} 167 | 168 | impl fmt::Debug for Key { 169 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 170 | write!(f, "{}", self.to_lower_hex()) 171 | } 172 | } 173 | 174 | impl fmt::Display for Key { 175 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 176 | write!(f, "{}", BASE64_STANDARD.encode(self.0)) 177 | } 178 | } 179 | 180 | #[cfg(feature = "serde")] 181 | impl Serialize for Key { 182 | fn serialize(&self, serializer: S) -> Result 183 | where 184 | S: Serializer, 185 | { 186 | serializer.serialize_str(&BASE64_STANDARD.encode(self.0)) 187 | } 188 | } 189 | 190 | #[cfg(feature = "serde")] 191 | struct KeyVisitor; 192 | 193 | #[cfg(feature = "serde")] 194 | impl Visitor<'_> for KeyVisitor { 195 | type Value = Key; 196 | 197 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 198 | formatter.write_str("32-bytes encoded as either base16 or base64") 199 | } 200 | 201 | fn visit_str(self, s: &str) -> Result 202 | where 203 | E: serde::de::Error, 204 | { 205 | Key::try_from(s).map_err(|_| serde::de::Error::invalid_value(Unexpected::Str(s), &self)) 206 | } 207 | } 208 | 209 | #[cfg(feature = "serde")] 210 | impl<'de> Deserialize<'de> for Key { 211 | fn deserialize(deserializer: D) -> Result 212 | where 213 | D: Deserializer<'de>, 214 | { 215 | deserializer.deserialize_str(KeyVisitor) 216 | } 217 | } 218 | 219 | #[cfg(test)] 220 | mod tests { 221 | use super::*; 222 | 223 | #[cfg(feature = "serde")] 224 | use serde_test::{assert_tokens, Token}; 225 | 226 | // Same `Key` in different representations. 227 | static KEY_B64: &str = "AAECAwQFBgcICQoLDA0OD/Dh0sO0pZaHeGlaSzwtHg8="; 228 | static KEY_HEX: &str = "000102030405060708090a0b0c0d0e0ff0e1d2c3b4a5968778695a4b3c2d1e0f"; 229 | static KEY_BUF: [u8; KEY_LENGTH] = [ 230 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 231 | 0x0f, 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b, 0x3c, 0x2d, 232 | 0x1e, 0x0f, 233 | ]; 234 | 235 | #[test] 236 | fn decode_key() { 237 | let key = Key::decode(KEY_HEX).unwrap(); 238 | assert_eq!(key.0, KEY_BUF); 239 | assert_eq!(key.to_lower_hex(), KEY_HEX); 240 | assert_eq!(key.to_string(), KEY_B64); 241 | } 242 | 243 | #[test] 244 | fn parse_key() { 245 | let key: Key = KEY_B64.try_into().unwrap(); 246 | assert_eq!(key.0, KEY_BUF); 247 | assert_eq!(key.to_lower_hex(), KEY_HEX); 248 | assert_eq!(key.to_string(), KEY_B64); 249 | } 250 | 251 | #[cfg(feature = "serde")] 252 | #[test] 253 | fn serialize_key() { 254 | let key = Key(KEY_BUF); 255 | assert_tokens(&key, &[Token::Str(KEY_B64)]); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # `defguard_wireguard_rs` 2 | //! 3 | //! `defguard_wireguard_rs` is a multi-platform Rust library providing a unified high-level API 4 | //! for managing WireGuard interfaces using native OS kernel and userspace WireGuard protocol implementations. 5 | //! 6 | //! It can be used to create your own [WireGuard:tm:](https://www.wireguard.com/) VPN servers or clients for secure and private networking. 7 | //! 8 | //! It was developed as part of [defguard](https://github.com/defguard/defguard) security platform and used in the [gateway/server](https://github.com/defguard/gateway) as well as [desktop client](https://github.com/defguard/client). 9 | //! 10 | //! ## Example 11 | //! 12 | //! ```no_run 13 | //! use x25519_dalek::{EphemeralSecret, PublicKey}; 14 | //! use defguard_wireguard_rs::{InterfaceConfiguration, Userspace, WGApi, WireguardInterfaceApi, host::Peer}; 15 | //! # use defguard_wireguard_rs::error::WireguardInterfaceError; 16 | //! 17 | //! // Create new API struct for interface 18 | //! let ifname: String = if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { 19 | //! "wg0".into() 20 | //! } else { 21 | //! "utun3".into() 22 | //! }; 23 | //! let wgapi = WGApi::::new(ifname.clone())?; 24 | //! 25 | //! // Create host interfaces 26 | //! wgapi.create_interface()?; 27 | //! 28 | //! // Configure host interface 29 | //! let interface_config = InterfaceConfiguration { 30 | //! name: ifname.clone(), 31 | //! prvkey: "AAECAwQFBgcICQoLDA0OD/Dh0sO0pZaHeGlaSzwtHg8=".to_string(), 32 | //! addresses: vec!["10.6.0.30".parse().unwrap()], 33 | //! port: 12345, 34 | //! peers: vec![], 35 | //! mtu: None, 36 | //! }; 37 | //! wgapi.configure_interface(&interface_config)?; 38 | //! 39 | //! // Create, add & remove peers 40 | //! for _ in 0..32 { 41 | //! let secret = EphemeralSecret::random(); 42 | //! let key = PublicKey::from(&secret); 43 | //! let peer = Peer::new(key.as_ref().try_into().unwrap()); 44 | //! wgapi.configure_peer(&peer)?; 45 | //! wgapi.remove_peer(&peer.public_key)?; 46 | //! } 47 | //! 48 | //! // Remove host interface 49 | //! wgapi.remove_interface()?; 50 | //! # Ok::<(), WireguardInterfaceError>(()) 51 | //! ``` 52 | 53 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 54 | pub mod bsd; 55 | pub mod error; 56 | pub mod host; 57 | pub mod key; 58 | pub mod net; 59 | #[cfg(target_os = "linux")] 60 | pub(crate) mod netlink; 61 | mod utils; 62 | mod wgapi; 63 | 64 | #[cfg(feature = "check_dependencies")] 65 | mod dependencies; 66 | #[cfg(target_os = "freebsd")] 67 | mod wgapi_freebsd; 68 | #[cfg(target_os = "linux")] 69 | mod wgapi_linux; 70 | #[cfg(target_family = "unix")] 71 | mod wgapi_userspace; 72 | #[cfg(target_family = "windows")] 73 | mod wgapi_windows; 74 | mod wireguard_interface; 75 | 76 | #[macro_use] 77 | extern crate log; 78 | 79 | use std::fmt; 80 | #[cfg(not(target_os = "windows"))] 81 | use std::process::Output; 82 | 83 | #[cfg(feature = "serde")] 84 | use serde::{Deserialize, Serialize}; 85 | // public re-exports 86 | pub use wgapi::{Kernel, Userspace, WGApi}; 87 | pub use wireguard_interface::WireguardInterfaceApi; 88 | 89 | use self::{ 90 | error::WireguardInterfaceError, 91 | host::{Host, Peer}, 92 | key::Key, 93 | net::IpAddrMask, 94 | }; 95 | 96 | // Internet Protocol (IP) address variant. 97 | #[derive(Clone, Copy)] 98 | pub enum IpVersion { 99 | IPv4, 100 | IPv6, 101 | } 102 | 103 | /// Host WireGuard interface configuration. 104 | #[derive(Clone)] 105 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 106 | pub struct InterfaceConfiguration { 107 | pub name: String, 108 | pub prvkey: String, 109 | pub addresses: Vec, 110 | pub port: u32, 111 | pub peers: Vec, 112 | /// Maximum transfer unit. `None` means do not set MTU, but keep the system default. 113 | pub mtu: Option, 114 | } 115 | 116 | // implement manually to avoid exposing private keys 117 | impl fmt::Debug for InterfaceConfiguration { 118 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 119 | f.debug_struct("InterfaceConfiguration") 120 | .field("name", &self.name) 121 | .field("addresses", &self.addresses) 122 | .field("port", &self.port) 123 | .field("peers", &self.peers) 124 | .field("mtu", &self.mtu) 125 | .finish_non_exhaustive() 126 | } 127 | } 128 | 129 | impl TryFrom<&InterfaceConfiguration> for Host { 130 | type Error = WireguardInterfaceError; 131 | 132 | fn try_from(config: &InterfaceConfiguration) -> Result { 133 | let key = config.prvkey.as_str().try_into()?; 134 | let mut host = Host::new(config.port as u16, key); 135 | for peercfg in &config.peers { 136 | let peer = peercfg.clone(); 137 | let key: Key = peer.public_key.clone(); 138 | host.peers.insert(key, peer); 139 | } 140 | Ok(host) 141 | } 142 | } 143 | 144 | #[cfg(not(target_os = "windows"))] 145 | /// Utility function which checks external command output status. 146 | fn check_command_output_status(output: Output) -> Result<(), WireguardInterfaceError> { 147 | if !output.status.success() { 148 | let stdout = String::from_utf8(output.stdout).expect("Invalid UTF8 sequence in stdout"); 149 | let stderr = String::from_utf8(output.stderr).expect("Invalid UTF8 sequence in stderr"); 150 | return Err(WireguardInterfaceError::CommandExecutionError { stdout, stderr }); 151 | } 152 | Ok(()) 153 | } 154 | -------------------------------------------------------------------------------- /src/net.rs: -------------------------------------------------------------------------------- 1 | //! Network address utilities 2 | 3 | use std::{ 4 | error, fmt, 5 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 6 | str::FromStr, 7 | }; 8 | 9 | #[cfg(target_os = "linux")] 10 | use netlink_packet_wireguard::{ 11 | constants::{AF_INET, AF_INET6}, 12 | nlas::{WgAllowedIp, WgAllowedIpAttrs}, 13 | }; 14 | #[cfg(feature = "serde")] 15 | use serde::{Deserialize, Serialize}; 16 | 17 | /// IP address with CIDR. 18 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 19 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 20 | pub struct IpAddrMask { 21 | // IP v4 or v6 22 | pub ip: IpAddr, 23 | // Classless Inter-Domain Routing 24 | pub cidr: u8, 25 | } 26 | 27 | impl IpAddrMask { 28 | #[must_use] 29 | pub fn new(ip: IpAddr, cidr: u8) -> Self { 30 | Self { ip, cidr } 31 | } 32 | 33 | #[must_use] 34 | pub fn host(ip: IpAddr) -> Self { 35 | let cidr = match ip { 36 | IpAddr::V4(_) => 32, 37 | IpAddr::V6(_) => 128, 38 | }; 39 | Self { ip, cidr } 40 | } 41 | 42 | /// Returns broadcast address as `IpAddr`. 43 | /// Note: IPv6 does not really use broadcast. 44 | #[must_use] 45 | pub fn broadcast(&self) -> IpAddr { 46 | match self.ip { 47 | IpAddr::V4(ip) => { 48 | let addr = u32::from(ip); 49 | let bits = if self.cidr >= 32 { 50 | 0 51 | } else { 52 | u32::MAX >> self.cidr 53 | }; 54 | IpAddr::V4(Ipv4Addr::from(addr | bits)) 55 | } 56 | IpAddr::V6(ip) => { 57 | let addr = u128::from(ip); 58 | let bits = if self.cidr >= 128 { 59 | 0 60 | } else { 61 | u128::MAX >> self.cidr 62 | }; 63 | IpAddr::V6(Ipv6Addr::from(addr | bits)) 64 | } 65 | } 66 | } 67 | 68 | /// Returns network mask as `IpAddr`. 69 | #[must_use] 70 | pub fn mask(&self) -> IpAddr { 71 | match self.ip { 72 | IpAddr::V4(_) => { 73 | let mask = if self.cidr == 0 { 74 | 0 75 | } else { 76 | u32::MAX << (32 - self.cidr) 77 | }; 78 | IpAddr::V4(Ipv4Addr::from(mask)) 79 | } 80 | IpAddr::V6(_) => { 81 | let mask = if self.cidr == 0 { 82 | 0 83 | } else { 84 | u128::MAX << (128 - self.cidr) 85 | }; 86 | IpAddr::V6(Ipv6Addr::from(mask)) 87 | } 88 | } 89 | } 90 | 91 | /// Returns `true` if the address defines a host, `false` if it is a network. 92 | #[must_use] 93 | pub fn is_host(&self) -> bool { 94 | if self.ip.is_ipv4() { 95 | self.cidr == 32 96 | } else { 97 | self.cidr == 128 98 | } 99 | } 100 | 101 | #[cfg(target_os = "linux")] 102 | #[must_use] 103 | pub fn to_nlas_allowed_ip(&self) -> WgAllowedIp { 104 | let mut attrs = Vec::new(); 105 | attrs.push(WgAllowedIpAttrs::Family(if self.ip.is_ipv4() { 106 | AF_INET 107 | } else { 108 | AF_INET6 109 | })); 110 | attrs.push(WgAllowedIpAttrs::IpAddr(self.ip)); 111 | attrs.push(WgAllowedIpAttrs::Cidr(self.cidr)); 112 | WgAllowedIp(attrs) 113 | } 114 | } 115 | 116 | impl fmt::Display for IpAddrMask { 117 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 118 | write!(f, "{}/{}", self.ip, self.cidr) 119 | } 120 | } 121 | 122 | #[derive(Debug, PartialEq)] 123 | pub struct IpAddrParseError; 124 | 125 | impl error::Error for IpAddrParseError {} 126 | 127 | impl fmt::Display for IpAddrParseError { 128 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 129 | write!(f, "IP address/mask parse error") 130 | } 131 | } 132 | 133 | impl FromStr for IpAddrMask { 134 | type Err = IpAddrParseError; 135 | 136 | fn from_str(ip_str: &str) -> Result { 137 | if let Some((left, right)) = ip_str.split_once('/') { 138 | let ip = left.parse().map_err(|_| IpAddrParseError)?; 139 | let cidr = right.parse().map_err(|_| IpAddrParseError)?; 140 | let max_cidr = match ip { 141 | IpAddr::V4(_) => 32, 142 | IpAddr::V6(_) => 128, 143 | }; 144 | if cidr > max_cidr { 145 | return Err(IpAddrParseError); 146 | } 147 | Ok(IpAddrMask { ip, cidr }) 148 | } else { 149 | let ip = ip_str.parse().map_err(|_| IpAddrParseError)?; 150 | Ok(IpAddrMask { 151 | ip, 152 | cidr: if ip.is_ipv4() { 32 } else { 128 }, 153 | }) 154 | } 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | 162 | #[test] 163 | fn parse_ip_addr() { 164 | assert_eq!( 165 | "192.168.0.1/24".parse::(), 166 | Ok(IpAddrMask::new( 167 | IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)), 168 | 24 169 | )) 170 | ); 171 | 172 | assert_eq!( 173 | "10.11.12.13".parse::(), 174 | Ok(IpAddrMask::new( 175 | IpAddr::V4(Ipv4Addr::new(10, 11, 12, 13)), 176 | 32 177 | )) 178 | ); 179 | 180 | assert_eq!( 181 | "2001:0db8::1428:57ab/96".parse::(), 182 | Ok(IpAddrMask::new( 183 | IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0x1428, 0x57ab)), 184 | 96 185 | )) 186 | ); 187 | 188 | assert_eq!( 189 | "::1".parse::(), 190 | Ok(IpAddrMask::new( 191 | IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 192 | 128 193 | )) 194 | ); 195 | 196 | assert_eq!( 197 | "172.168.0.256/24".parse::(), 198 | Err(IpAddrParseError) 199 | ); 200 | 201 | assert_eq!( 202 | "172.168.0.0/256".parse::(), 203 | Err(IpAddrParseError) 204 | ); 205 | } 206 | 207 | #[test] 208 | fn valid_cidr() { 209 | assert!("192.168.0.1/32".parse::().is_ok()); 210 | assert!("192.168.0.1/33".parse::().is_err()); 211 | assert!("2001:0db8::1428:57ab/128".parse::().is_ok()); 212 | assert!("2001:0db8::1428:57ab/129".parse::().is_err()); 213 | } 214 | 215 | #[test] 216 | fn addr_mask() { 217 | let ip = IpAddrMask::new(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)), 24); 218 | assert_eq!(ip.broadcast(), IpAddr::V4(Ipv4Addr::new(192, 168, 0, 255))); 219 | assert_eq!(ip.mask(), IpAddr::V4(Ipv4Addr::new(255, 255, 255, 0))); 220 | 221 | let ip = IpAddrMask::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8); 222 | assert_eq!( 223 | ip.broadcast(), 224 | IpAddr::V4(Ipv4Addr::new(127, 255, 255, 255)) 225 | ); 226 | assert_eq!(ip.mask(), IpAddr::V4(Ipv4Addr::new(255, 0, 0, 0))); 227 | 228 | let ip = IpAddrMask::new(IpAddr::V4(Ipv4Addr::new(169, 254, 219, 59)), 16); 229 | assert_eq!( 230 | ip.broadcast(), 231 | IpAddr::V4(Ipv4Addr::new(169, 254, 255, 255)) 232 | ); 233 | assert_eq!(ip.mask(), IpAddr::V4(Ipv4Addr::new(255, 255, 0, 0))); 234 | 235 | let ip = IpAddrMask::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0); 236 | assert_eq!( 237 | ip.broadcast(), 238 | IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255)) 239 | ); 240 | assert_eq!(ip.mask(), IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); 241 | 242 | let ip = IpAddrMask::new(IpAddr::V4(Ipv4Addr::new(12, 34, 56, 78)), 32); 243 | assert_eq!(ip.broadcast(), IpAddr::V4(Ipv4Addr::new(12, 34, 56, 78))); 244 | assert_eq!(ip.mask(), IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255))); 245 | } 246 | 247 | #[test] 248 | fn addr_mask_v6() { 249 | let ip = IpAddrMask::new( 250 | IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0x1428, 0x57ab)), 251 | 96, 252 | ); 253 | assert_eq!( 254 | ip.broadcast(), 255 | IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0xffff, 0xffff)) 256 | ); 257 | assert_eq!( 258 | ip.mask(), 259 | IpAddr::V6(Ipv6Addr::new( 260 | 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0, 0 261 | )) 262 | ); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | use std::io::{BufRead, BufReader, Cursor, Error as IoError}; 3 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 4 | use std::net::{Ipv4Addr, Ipv6Addr}; 5 | use std::net::{SocketAddr, ToSocketAddrs}; 6 | #[cfg(target_os = "linux")] 7 | use std::{collections::HashSet, fs::OpenOptions}; 8 | #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] 9 | use std::{io::Write, process::Stdio}; 10 | #[cfg(not(target_os = "windows"))] 11 | use std::{net::IpAddr, process::Command}; 12 | 13 | #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] 14 | use crate::check_command_output_status; 15 | #[cfg(not(target_os = "windows"))] 16 | use crate::Peer; 17 | use crate::WireguardInterfaceError; 18 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 19 | use crate::{ 20 | bsd::{add_gateway, add_linked_route, get_gateway}, 21 | net::IpAddrMask, 22 | IpVersion, 23 | }; 24 | #[cfg(target_os = "linux")] 25 | use crate::{check_command_output_status, netlink, IpVersion}; 26 | 27 | #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] 28 | pub(crate) fn configure_dns( 29 | ifname: &str, 30 | dns: &[IpAddr], 31 | search_domains: &[&str], 32 | ) -> Result<(), WireguardInterfaceError> { 33 | // Build the resolvconf command 34 | debug!( 35 | "Starting DNS servers configuration for interface {ifname}, DNS: {dns:?}, search \ 36 | domains: {search_domains:?}" 37 | ); 38 | let mut cmd = Command::new("resolvconf"); 39 | let mut args = vec!["-a", ifname, "-m", "0"]; 40 | // Set the exclusive flag if no search domains are provided, 41 | // making the DNS servers a preferred route for any domain 42 | if search_domains.is_empty() { 43 | args.push("-x"); 44 | } 45 | debug!("Executing command resolvconf with args: {args:?}"); 46 | cmd.args(args); 47 | 48 | match cmd.stdin(Stdio::piped()).spawn() { 49 | Ok(mut child) => { 50 | debug!( 51 | "Command resolvconf spawned successfully, proceeding with writing nameservers \ 52 | and search domains to its stdin" 53 | ); 54 | if let Some(mut stdin) = child.stdin.take() { 55 | for entry in dns { 56 | debug!("Adding nameserver entry: {entry}"); 57 | writeln!(stdin, "nameserver {entry}")?; 58 | } 59 | for domain in search_domains { 60 | debug!("Adding search domain entry: {domain}"); 61 | writeln!(stdin, "search {domain}")?; 62 | } 63 | } 64 | debug!("Waiting for resolvconf command to finish"); 65 | 66 | let status = child.wait().expect("Failed to wait for command"); 67 | if status.success() { 68 | debug!("DNS servers and search domains set successfully for interface {ifname}"); 69 | Ok(()) 70 | } else { 71 | Err(WireguardInterfaceError::DnsError(format!( 72 | "Failed to execute resolvconf \ 73 | command while setting DNS servers and search domains: {status}" 74 | ))) 75 | } 76 | } 77 | Err(e) => Err(WireguardInterfaceError::DnsError(format!( 78 | "Failed to execute resolvconf command \ 79 | while setting DNS servers and search domains: {e}" 80 | ))), 81 | } 82 | } 83 | 84 | #[cfg(target_os = "macos")] 85 | /// Obtain list of network services 86 | fn network_services() -> Result, IoError> { 87 | let output = Command::new("networksetup") 88 | .arg("-listallnetworkservices") 89 | .output()?; 90 | 91 | if output.status.success() { 92 | let buf = BufReader::new(Cursor::new(output.stdout)); 93 | // Get all lines from stdout without asterisk (*). 94 | // An asterisk (*) denotes that a network service is disabled. 95 | let lines = buf 96 | .lines() 97 | .filter_map(|line| line.ok().filter(|line| !line.contains('*'))) 98 | .collect(); 99 | debug!("Found following network services: {lines:?}"); 100 | Ok(lines) 101 | } else { 102 | Err(IoError::other(format!( 103 | "network setup command failed: {}", 104 | output.status 105 | ))) 106 | } 107 | } 108 | 109 | #[cfg(target_os = "macos")] 110 | pub(crate) fn configure_dns( 111 | dns: &[IpAddr], 112 | search_domains: &[&str], 113 | ) -> Result<(), WireguardInterfaceError> { 114 | debug!( 115 | "Configuring DNS servers and search domains, DNS: {dns:?}, search domains: \ 116 | {search_domains:?}" 117 | ); 118 | 119 | debug!("Setting DNS servers and search domains for all network services"); 120 | for service in network_services()? { 121 | debug!( 122 | "Setting DNS entries (search domains and DNS servers) for network service {service}" 123 | ); 124 | let mut cmd = Command::new("networksetup"); 125 | cmd.arg("-setdnsservers").arg(&service); 126 | if dns.is_empty() { 127 | // This clears all DNS entries. 128 | cmd.arg("Empty"); 129 | } else { 130 | cmd.args(dns.iter().map(ToString::to_string)); 131 | } 132 | 133 | let status = cmd.status()?; 134 | if !status.success() { 135 | return Err(WireguardInterfaceError::DnsError(format!( 136 | "Command `networksetup` failed while setting DNS servers for {service}: {status}" 137 | ))); 138 | } 139 | debug!("DNS servers set successfully for {service}"); 140 | 141 | // Set search domains, if empty, clear all search domains. 142 | debug!("Setting search domains for {service}"); 143 | let mut cmd = Command::new("networksetup"); 144 | cmd.arg("-setsearchdomains").arg(&service); 145 | if search_domains.is_empty() { 146 | // This clears all search domains. 147 | cmd.arg("Empty"); 148 | } else { 149 | cmd.args(search_domains.iter()); 150 | } 151 | 152 | let status = cmd.status()?; 153 | if !status.success() { 154 | return Err(WireguardInterfaceError::DnsError(format!( 155 | "Command `networksetup` failed \ 156 | while setting search domains for {service}: {status}" 157 | ))); 158 | } 159 | 160 | debug!("Search domains set successfully for {service}"); 161 | } 162 | 163 | debug!( 164 | "The following DNS servers and search domains were set successfully: DNS: {dns:?}, \ 165 | search domains: {search_domains:?}" 166 | ); 167 | Ok(()) 168 | } 169 | 170 | #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] 171 | pub(crate) fn clear_dns(ifname: &str) -> Result<(), WireguardInterfaceError> { 172 | debug!("Removing DNS configuration for interface {ifname}"); 173 | let args = ["-d", ifname, "-f"]; 174 | debug!("Executing resolvconf with args: {args:?}"); 175 | let mut cmd = Command::new("resolvconf"); 176 | let output = cmd.args(args).output()?; 177 | check_command_output_status(output)?; 178 | debug!("DNS configuration removed successfully for interface {ifname}"); 179 | Ok(()) 180 | } 181 | 182 | #[cfg(target_os = "linux")] 183 | const DEFAULT_FWMARK_TABLE: u32 = 51820; 184 | 185 | /// Helper function to add routing. 186 | #[cfg(target_os = "linux")] 187 | pub(crate) fn add_peer_routing( 188 | peers: &[Peer], 189 | ifname: &str, 190 | ) -> Result<(), WireguardInterfaceError> { 191 | debug!("Adding peer routing for interface: {ifname}"); 192 | 193 | let mut unique_allowed_ips = HashSet::new(); 194 | let mut default_route = None; 195 | for peer in peers { 196 | for addr in &peer.allowed_ips { 197 | if addr.ip.is_unspecified() { 198 | // Handle default route 199 | default_route = Some(addr); 200 | break; 201 | } 202 | unique_allowed_ips.insert(addr); 203 | } 204 | } 205 | debug!("Allowed IPs that will be used during the peer routing setup: {unique_allowed_ips:?}"); 206 | 207 | // If there is default route skip adding other routes. 208 | if let Some(default_route) = default_route { 209 | debug!("Found default route in AllowedIPs: {default_route:?}"); 210 | let is_ipv6 = default_route.ip.is_ipv6(); 211 | let proto = if is_ipv6 { "-6" } else { "-4" }; 212 | debug!("Using the following IP version: {proto}"); 213 | 214 | debug!("Getting current host configuration for interface {ifname}"); 215 | let mut host = netlink::get_host(ifname)?; 216 | debug!("Host configuration read for interface {ifname}"); 217 | trace!("Current host: {host:?}"); 218 | 219 | debug!("Choosing fwmark for marking WireGuard traffic"); 220 | let fwmark = match host.fwmark { 221 | Some(fwmark) if fwmark != 0 => fwmark, 222 | Some(_) | None => { 223 | let mut table = DEFAULT_FWMARK_TABLE; 224 | loop { 225 | let output = Command::new("ip") 226 | .args([proto, "route", "show", "table", &table.to_string()]) 227 | .output()?; 228 | if output.stdout.is_empty() { 229 | host.fwmark = Some(table); 230 | netlink::set_host(ifname, &host)?; 231 | debug!("Assigned fwmark: {table}"); 232 | break; 233 | } 234 | table += 1; 235 | } 236 | table 237 | } 238 | }; 239 | debug!("Using the following fwmark for marking WireGuard traffic: {fwmark}"); 240 | 241 | // Add routes and table rules 242 | debug!("Adding default route: {default_route}"); 243 | netlink::add_route(ifname, default_route, Some(fwmark))?; 244 | debug!("Default route added successfully"); 245 | debug!("Adding fwmark rule for the WireGuard interface to prevent routing loops"); 246 | netlink::add_fwmark_rule(default_route, fwmark)?; 247 | debug!("Fwmark rule added successfully"); 248 | 249 | debug!("Adding rule for main table to suppress current default gateway"); 250 | netlink::add_main_table_rule(default_route, 0)?; 251 | debug!("Main table rule added successfully"); 252 | 253 | if !is_ipv6 { 254 | debug!("Setting net.ipv4.conf.all.src_valid_mark=1"); 255 | OpenOptions::new() 256 | .write(true) 257 | .open("/proc/sys/net/ipv4/conf/all/src_valid_mark")? 258 | .write_all(b"1")?; 259 | debug!("net.ipv4.conf.all.src_valid_mark=1 set successfully"); 260 | } 261 | } else { 262 | for allowed_ip in unique_allowed_ips { 263 | debug!("Adding a route for allowed IP: {allowed_ip}"); 264 | netlink::add_route(ifname, allowed_ip, None)?; 265 | debug!("Route added for allowed IP: {allowed_ip}"); 266 | } 267 | } 268 | debug!("Peers routing added successfully"); 269 | Ok(()) 270 | } 271 | 272 | /// Helper function to add routing. 273 | #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))] 274 | pub(crate) fn add_peer_routing( 275 | peers: &[Peer], 276 | ifname: &str, 277 | ) -> Result<(), WireguardInterfaceError> { 278 | use nix::errno::Errno; 279 | 280 | use crate::bsd::{delete_gateway, IoError}; 281 | 282 | debug!("Adding peer routing for interface: {ifname}"); 283 | for peer in peers { 284 | debug!("Processing peer: {}", peer.public_key); 285 | let mut default_route_v4 = false; 286 | let mut default_route_v6 = false; 287 | let mut gateway_v4 = Ok(None); 288 | let mut gateway_v6 = Ok(None); 289 | for addr in &peer.allowed_ips { 290 | debug!("Processing route for allowed IP: {addr}, interface: {ifname}"); 291 | // FIXME: currently it is impossible to add another default route, so use the hack from 292 | // wg-quick for Darwin. 293 | if addr.ip.is_unspecified() && addr.cidr == 0 { 294 | debug!( 295 | "Found following default route in the allowed IPs: {addr}, interface: \ 296 | {ifname}, proceeding with default route initial setup..." 297 | ); 298 | let default1; 299 | let default2; 300 | if addr.ip.is_ipv4() { 301 | // 0.0.0.0/1 302 | default1 = IpAddrMask::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 1); 303 | // 128.0.0.0/1 304 | default2 = IpAddrMask::new(IpAddr::V4(Ipv4Addr::new(128, 0, 0, 0)), 1); 305 | gateway_v4 = get_gateway(IpVersion::IPv4); 306 | debug!("Default gateway for IPv4 value: {gateway_v4:?}"); 307 | default_route_v4 = true; 308 | } else { 309 | // ::/1 310 | default1 = IpAddrMask::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 1); 311 | // 8000::/1 312 | default2 = 313 | IpAddrMask::new(IpAddr::V6(Ipv6Addr::new(0x8000, 0, 0, 0, 0, 0, 0, 0)), 1); 314 | gateway_v6 = get_gateway(IpVersion::IPv6); 315 | debug!("Default gateway for IPv6 value: {gateway_v6:?}"); 316 | default_route_v6 = true; 317 | } 318 | match add_linked_route(&default1, ifname) { 319 | Ok(()) => debug!("Route to {default1} has been added for interface {ifname}"), 320 | Err(err) => match err { 321 | IoError::WriteIo(Errno::ENETUNREACH) => { 322 | warn!( 323 | "Failed to add default route {default1} for interface \ 324 | {ifname}: Network is unreachable. This may happen if your \ 325 | interface's IP address is not the same IP version as the \ 326 | default gateway ({default1}) that was tried to be set, in this \ 327 | case this warning can be ignored. Otherwise, there may be some \ 328 | other issues with your network configuration." 329 | ); 330 | } 331 | _ => { 332 | error!( 333 | "Failed to add route to {default1} for interface {ifname}: \ 334 | {err}" 335 | ); 336 | } 337 | }, 338 | } 339 | match add_linked_route(&default2, ifname) { 340 | Ok(()) => debug!("Route to {default2} has been added for interface {ifname}"), 341 | Err(err) => match err { 342 | IoError::WriteIo(Errno::ENETUNREACH) => { 343 | warn!( 344 | "Failed to add default route {default2} for interface \ 345 | {ifname}: Network is unreachable. This may happen if your \ 346 | interface's IP address is not the same IP version as the \ 347 | default gateway ({default2}) that was tried to be set, in this \ 348 | case this warning can be ignored. Otherwise, there may be some \ 349 | other issues with your network configuration." 350 | ); 351 | } 352 | _ => { 353 | error!( 354 | "Failed to add route to {default2} for interface {ifname}: \ 355 | {err}" 356 | ); 357 | } 358 | }, 359 | } 360 | } else { 361 | // Equivalent to `route -n add -inet[6] -interface `. 362 | match add_linked_route(addr, ifname) { 363 | Ok(()) => debug!("Route to {addr} has been added for interface {ifname}"), 364 | Err(err) => { 365 | error!("Failed to add route to {addr} for interface {ifname}: {err}"); 366 | } 367 | } 368 | } 369 | } 370 | 371 | if default_route_v4 || default_route_v6 { 372 | if let Some(endpoint) = peer.endpoint { 373 | debug!("Default routes have been set, proceeding with further configuration..."); 374 | let host = IpAddrMask::host(endpoint.ip()); 375 | let localhost = if endpoint.is_ipv4() { 376 | IpAddr::V4(Ipv4Addr::LOCALHOST) 377 | } else { 378 | IpAddr::V6(Ipv6Addr::LOCALHOST) 379 | }; 380 | debug!("Cleaning up old route to {host}, if it exists..."); 381 | match delete_gateway(&host) { 382 | Ok(()) => { 383 | debug!( 384 | "Previously existing route to {host} has been removed, if it existed" 385 | ); 386 | } 387 | Err(err) => { 388 | debug!("Previously existing route to {host} has not been removed: {err}"); 389 | } 390 | } 391 | if endpoint.is_ipv6() && default_route_v6 { 392 | debug!( 393 | "Endpoint is an IPv6 address and a default route (IPv6) is present in \ 394 | the alloweds IPs, proceeding with further configuration..." 395 | ); 396 | match gateway_v6 { 397 | Ok(Some(gateway)) => { 398 | debug!( 399 | "Default gateway for IPv4 has been found before: {gateway}, \ 400 | routing the traffic destined to {host} through it..." 401 | ); 402 | match add_gateway(&host, gateway, false) { 403 | Ok(()) => { 404 | debug!("Route to {host} has been added for gateway {gateway}"); 405 | } 406 | Err(err) => { 407 | error!( 408 | "Failed to add route to {host} for gateway {gateway}: \ 409 | {err}" 410 | ); 411 | } 412 | } 413 | } 414 | Ok(None) => { 415 | debug!( 416 | "Default gateway for IPv6 has not been found, routing the \ 417 | traffic destined to {host} through localhost as a blackhole \ 418 | route..." 419 | ); 420 | match add_gateway(&host, localhost, true) { 421 | Ok(()) => debug!("Blackhole route to {host} has been added"), 422 | Err(err) => { 423 | error!("Failed to add blackhole route to {host}: {err}"); 424 | } 425 | } 426 | } 427 | Err(err) => { 428 | error!("Failed to get gateway for {host}: {err}"); 429 | } 430 | } 431 | } else if default_route_v4 { 432 | debug!( 433 | "Endpoint is an IPv4 address and a default route (IPv4) is present in \ 434 | the alloweds IPs, proceeding with further configuration..." 435 | ); 436 | match gateway_v4 { 437 | Ok(Some(gateway)) => { 438 | debug!( 439 | "Default gateway for IPv4 has been found before: {gateway}, \ 440 | routing the traffic destined to {host} through it..." 441 | ); 442 | match add_gateway(&host, gateway, false) { 443 | Ok(()) => { 444 | debug!("Added route to {host} for gateway {gateway}"); 445 | } 446 | Err(err) => { 447 | error!( 448 | "Failed to add route to {host} for gateway {gateway}: \ 449 | {err}" 450 | ); 451 | } 452 | } 453 | } 454 | Ok(None) => { 455 | debug!( 456 | "Default gateway for IPv4 has not been found, routing the \ 457 | traffic destined to {host} through localhost as a blackhole \ 458 | route..." 459 | ); 460 | match add_gateway(&host, localhost, true) { 461 | Ok(()) => debug!("Blackhole route to {host} has been added"), 462 | Err(err) => { 463 | error!("Failed to add blackhole route to {host}: {err}"); 464 | } 465 | } 466 | } 467 | Err(err) => { 468 | error!("Failed to get gateway for {host}: {err}"); 469 | } 470 | } 471 | } 472 | } 473 | } 474 | } 475 | 476 | debug!("Peers routing added successfully"); 477 | Ok(()) 478 | } 479 | 480 | /// Clean fwmark rules while removing interface same as in wg-quick 481 | #[cfg(target_os = "linux")] 482 | pub(crate) fn clean_fwmark_rules(fwmark: u32) -> Result<(), WireguardInterfaceError> { 483 | debug!("Removing firewall rules."); 484 | netlink::delete_rule(IpVersion::IPv4, fwmark)?; 485 | netlink::delete_main_table_rule(IpVersion::IPv4, 0)?; 486 | netlink::delete_rule(IpVersion::IPv6, fwmark)?; 487 | netlink::delete_main_table_rule(IpVersion::IPv6, 0)?; 488 | Ok(()) 489 | } 490 | 491 | /// Resolves domain name to [`SocketAddr`]. 492 | pub(crate) fn resolve(addr: &str) -> Result { 493 | let error = || { 494 | WireguardInterfaceError::PeerConfigurationError(format!( 495 | "Failed to resolve address: {addr}" 496 | )) 497 | }; 498 | addr.to_socket_addrs() 499 | .map_err(|_| error())? 500 | .next() 501 | .ok_or_else(error) 502 | } 503 | -------------------------------------------------------------------------------- /src/wgapi.rs: -------------------------------------------------------------------------------- 1 | //! Shared multi-platform management API abstraction 2 | use std::marker::PhantomData; 3 | 4 | #[cfg(feature = "check_dependencies")] 5 | use crate::dependencies::check_external_dependencies; 6 | use crate::error::WireguardInterfaceError; 7 | 8 | pub struct Kernel; 9 | pub struct Userspace; 10 | 11 | /// Shared multi-platform WireGuard management API 12 | /// 13 | /// This struct adds an additional level of abstraction and can be used 14 | /// to detect the correct API implementation for most common platforms. 15 | pub struct WGApi { 16 | pub(super) ifname: String, 17 | pub(super) _api: PhantomData, 18 | } 19 | 20 | impl WGApi { 21 | /// Create new instance of `WGApi`. 22 | pub fn new(ifname: String) -> Result { 23 | #[cfg(feature = "check_dependencies")] 24 | check_external_dependencies()?; 25 | Ok(WGApi { 26 | ifname, 27 | _api: PhantomData, 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/wgapi_freebsd.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use crate::{ 4 | bsd, 5 | utils::{add_peer_routing, clear_dns, configure_dns}, 6 | wgapi::{Kernel, WGApi}, 7 | Host, InterfaceConfiguration, IpAddrMask, Key, Peer, WireguardInterfaceApi, 8 | WireguardInterfaceError, 9 | }; 10 | 11 | /// Manages interfaces created with FreeBSD kernel WireGuard module. 12 | /// 13 | /// Requires FreeBSD version 13+. 14 | impl WireguardInterfaceApi for WGApi { 15 | /// Creates a WireGuard network interface. 16 | fn create_interface(&self) -> Result<(), WireguardInterfaceError> { 17 | let _ = bsd::load_wireguard_kernel_module(); 18 | debug!("Creating interface {}", &self.ifname); 19 | bsd::create_interface(&self.ifname)?; 20 | debug!("Interface {} created successfully", &self.ifname); 21 | Ok(()) 22 | } 23 | 24 | fn assign_address(&self, address: &IpAddrMask) -> Result<(), WireguardInterfaceError> { 25 | debug!("Assigning address {address} to interface {}", self.ifname); 26 | bsd::assign_address(&self.ifname, address)?; 27 | debug!( 28 | "Address {address} assigned to interface {} successfully", 29 | self.ifname 30 | ); 31 | Ok(()) 32 | } 33 | 34 | /// Add peer addresses to network routing table. 35 | /// 36 | /// For every allowed IP, it runs: 37 | /// - `route -q -n add allowed_ip -interface if_name` 38 | /// `ifname` - interface name while creating api 39 | /// `allowed_ip`- one of [Peer](crate::Peer) allowed ip 40 | /// For `0.0.0.0/0` or `::/0` allowed IP, it adds default routing and skips other using: 41 | /// - `route -q -n add 0.0.0.0/1 -interface if_name`. 42 | /// - `route -q -n add 128.0.0.0/1 -interface if_name`. 43 | /// - `route -q -n add -gateway ` 44 | /// `` - Add routing for every unique Peer endpoint. 45 | /// ``- Gateway extracted using `netstat -nr -f `. 46 | /// ## Note: 47 | /// Based on ip type `` will be equal to `-inet` or `-inet6` 48 | fn configure_peer_routing(&self, peers: &[Peer]) -> Result<(), WireguardInterfaceError> { 49 | debug!("Configuring peer routing for interface {}", self.ifname); 50 | add_peer_routing(peers, &self.ifname)?; 51 | info!( 52 | "Peer routing configured successfully for interface {}", 53 | self.ifname 54 | ); 55 | Ok(()) 56 | } 57 | 58 | fn configure_interface( 59 | &self, 60 | config: &InterfaceConfiguration, 61 | ) -> Result<(), WireguardInterfaceError> { 62 | debug!( 63 | "Configuring interface {} with config: {config:?}", 64 | self.ifname 65 | ); 66 | 67 | // Assign IP address to the interface. 68 | for address in &config.addresses { 69 | self.assign_address(address)?; 70 | } 71 | 72 | // configure interface 73 | debug!( 74 | "Applying the WireGuard host configuration for interface {}", 75 | self.ifname 76 | ); 77 | let host = config.try_into()?; 78 | bsd::set_host(&self.ifname, &host)?; 79 | debug!( 80 | "WireGuard host configuration set for interface {}.", 81 | self.ifname 82 | ); 83 | trace!("WireGuard host configuration: {host:?}"); 84 | 85 | // Set maximum transfer unit (MTU). 86 | if let Some(mtu) = config.mtu { 87 | debug!("Setting MTU of {mtu} for interface {}", self.ifname); 88 | bsd::set_mtu(&self.ifname, mtu)?; 89 | debug!( 90 | "MTU of {mtu} set for interface {}, value: {mtu}", 91 | self.ifname 92 | ); 93 | } 94 | 95 | info!( 96 | "Interface {} has been successfully configured. \ 97 | It has been assigned the following addresses: {:?}", 98 | self.ifname, config.addresses 99 | ); 100 | debug!( 101 | "Interface {} configured with config: {config:?}", 102 | self.ifname 103 | ); 104 | 105 | Ok(()) 106 | } 107 | 108 | /// Remove WireGuard network interface. 109 | fn remove_interface(&self) -> Result<(), WireguardInterfaceError> { 110 | debug!("Removing interface {}", &self.ifname); 111 | bsd::delete_interface(&self.ifname)?; 112 | debug!("Interface {} removed successfully", &self.ifname); 113 | 114 | clear_dns(&self.ifname)?; 115 | 116 | info!("Interface {} removed successfully", &self.ifname); 117 | Ok(()) 118 | } 119 | 120 | fn configure_peer(&self, peer: &Peer) -> Result<(), WireguardInterfaceError> { 121 | debug!("Configuring peer {peer:?} on interface {}", self.ifname); 122 | bsd::set_peer(&self.ifname, peer)?; 123 | debug!( 124 | "Peer {peer:?} configured successfully on interface {}", 125 | self.ifname 126 | ); 127 | Ok(()) 128 | } 129 | 130 | fn remove_peer(&self, peer_pubkey: &Key) -> Result<(), WireguardInterfaceError> { 131 | debug!( 132 | "Removing peer with public key {peer_pubkey} from interface {}", 133 | self.ifname 134 | ); 135 | bsd::delete_peer(&self.ifname, peer_pubkey)?; 136 | debug!( 137 | "Peer with public key {peer_pubkey} removed successfully from interface {}", 138 | self.ifname 139 | ); 140 | Ok(()) 141 | } 142 | 143 | fn read_interface_data(&self) -> Result { 144 | debug!("Reading host info for interface {}", self.ifname); 145 | let host = bsd::get_host(&self.ifname)?; 146 | debug!("Host info read for interface {}", self.ifname); 147 | trace!("Host configuration: {host:?}"); 148 | Ok(host) 149 | } 150 | 151 | /// Sets DNS configuration for a Wireguard interface using the `resolvconf` command. 152 | /// 153 | /// It executes the `resolvconf` command with appropriate arguments to update DNS 154 | /// configurations for the specified Wireguard interface. The DNS entries are filtered 155 | /// for nameservers and search domains before being piped to the `resolvconf` command. 156 | fn configure_dns( 157 | &self, 158 | dns: &[IpAddr], 159 | search_domains: &[&str], 160 | ) -> Result<(), WireguardInterfaceError> { 161 | if dns.is_empty() { 162 | warn!("Received empty DNS server list. Skipping DNS configuration..."); 163 | return Ok(()); 164 | } 165 | configure_dns(&self.ifname, dns, search_domains)?; 166 | Ok(()) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/wgapi_linux.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use crate::{ 4 | netlink, 5 | utils::{add_peer_routing, clean_fwmark_rules, clear_dns, configure_dns}, 6 | wgapi::{Kernel, WGApi}, 7 | Host, InterfaceConfiguration, IpAddrMask, Key, Peer, WireguardInterfaceApi, 8 | WireguardInterfaceError, 9 | }; 10 | 11 | /// Manages interfaces created with Linux kernel WireGuard module. 12 | /// 13 | /// Communicates with kernel module using `Netlink` IPC protocol. 14 | /// Requires Linux kernel version 5.6+. 15 | impl WireguardInterfaceApi for WGApi { 16 | fn create_interface(&self) -> Result<(), WireguardInterfaceError> { 17 | debug!("Creating interface {}", self.ifname); 18 | netlink::create_interface(&self.ifname)?; 19 | debug!("Interface {} created successfully", self.ifname); 20 | Ok(()) 21 | } 22 | 23 | fn assign_address(&self, address: &IpAddrMask) -> Result<(), WireguardInterfaceError> { 24 | debug!("Assigning address {address} to interface {}", self.ifname); 25 | netlink::address_interface(&self.ifname, address)?; 26 | debug!( 27 | "Address {address} assigned to interface {} successfully", 28 | self.ifname 29 | ); 30 | Ok(()) 31 | } 32 | 33 | fn configure_interface( 34 | &self, 35 | config: &InterfaceConfiguration, 36 | ) -> Result<(), WireguardInterfaceError> { 37 | debug!( 38 | "Configuring interface {} with config: {config:?}", 39 | self.ifname 40 | ); 41 | 42 | // flush all IP addresses 43 | debug!( 44 | "Flushing all existing IP addresses from interface {} before assigning a new one", 45 | self.ifname 46 | ); 47 | netlink::flush_interface(&self.ifname)?; 48 | debug!( 49 | "All existing IP addresses flushed from interface {}", 50 | self.ifname 51 | ); 52 | 53 | // Assign IP addresses to the interface. 54 | for address in &config.addresses { 55 | debug!("Assigning address {address} to interface {}", self.ifname); 56 | self.assign_address(address)?; 57 | debug!( 58 | "Address {address} assigned to interface {} successfully", 59 | self.ifname 60 | ); 61 | } 62 | 63 | // configure interface 64 | debug!( 65 | "Applying the WireGuard host configuration for interface {}", 66 | self.ifname 67 | ); 68 | let host = config.try_into()?; 69 | netlink::set_host(&self.ifname, &host)?; 70 | debug!( 71 | "WireGuard host configuration set for interface {}.", 72 | self.ifname 73 | ); 74 | trace!("WireGuard host configuration: {host:?}"); 75 | 76 | // set maximum transfer unit 77 | if let Some(mtu) = config.mtu { 78 | debug!("Setting MTU of {mtu} for interface {}", self.ifname); 79 | netlink::set_mtu(&self.ifname, mtu)?; 80 | debug!("MTU of {mtu} set for interface {}, value: {{", self.ifname); 81 | } else { 82 | debug!( 83 | "Skipping setting the MTU for interface {}, as it has not been provided", 84 | self.ifname 85 | ); 86 | } 87 | 88 | info!( 89 | "Interface {} has been successfully configured. \ 90 | It has been assigned the following addresses: {:?}", 91 | self.ifname, config.addresses 92 | ); 93 | debug!( 94 | "Interface {} configured with config: {config:?}", 95 | self.ifname 96 | ); 97 | 98 | Ok(()) 99 | } 100 | 101 | /// Configures peer routing. Internally uses netlink to set up routing rules for each peer. 102 | /// If allowed IPs contain a default route, instead of adding a route for every peer, the following changes are made: 103 | /// - A new default route is added 104 | /// - The current default route is suppressed by modifying the main routing table rule with `suppress_prefixlen 0`, this makes 105 | /// it so that the whole main routing table rules are still applied except for the default route rules (so the new default route is used instead) 106 | /// - A rule pushing all traffic through the WireGuard interface is added with the exception of traffic marked with 51820 (default) fwmark which 107 | /// is used for the WireGuard traffic itself (so it doesn't get stuck in a loop) 108 | /// 109 | fn configure_peer_routing(&self, peers: &[Peer]) -> Result<(), WireguardInterfaceError> { 110 | add_peer_routing(peers, &self.ifname)?; 111 | Ok(()) 112 | } 113 | 114 | fn remove_interface(&self) -> Result<(), WireguardInterfaceError> { 115 | debug!( 116 | "Removing interface {}. Getting its WireGuard host configuration first...", 117 | self.ifname 118 | ); 119 | let host = netlink::get_host(&self.ifname)?; 120 | debug!( 121 | "WireGuard host configuration read for interface {}", 122 | self.ifname 123 | ); 124 | trace!("WireGuard host configuration: {host:?}"); 125 | if let Some(fwmark) = host.fwmark { 126 | if fwmark != 0 { 127 | debug!("Cleaning fwmark rules for interface {}", self.ifname); 128 | clean_fwmark_rules(fwmark)?; 129 | debug!("Fwmark rules cleaned for interface {}", self.ifname); 130 | } 131 | } 132 | debug!("Performing removal of interface {}", self.ifname); 133 | netlink::delete_interface(&self.ifname)?; 134 | debug!( 135 | "Interface {} removed successfully. Clearing the dns...", 136 | self.ifname 137 | ); 138 | clear_dns(&self.ifname)?; 139 | debug!("DNS cleared for interface {}", self.ifname); 140 | 141 | info!("Interface {} removed successfully", self.ifname); 142 | Ok(()) 143 | } 144 | 145 | fn configure_peer(&self, peer: &Peer) -> Result<(), WireguardInterfaceError> { 146 | debug!("Configuring peer {peer:?} on interface {}", self.ifname); 147 | netlink::set_peer(&self.ifname, peer)?; 148 | debug!("Peer {peer:?} configured on interface {}", self.ifname); 149 | Ok(()) 150 | } 151 | 152 | fn remove_peer(&self, peer_pubkey: &Key) -> Result<(), WireguardInterfaceError> { 153 | debug!( 154 | "Removing peer with public key {peer_pubkey} from interface {}", 155 | self.ifname 156 | ); 157 | netlink::delete_peer(&self.ifname, peer_pubkey)?; 158 | debug!( 159 | "Peer with public key {peer_pubkey} removed from interface {}", 160 | self.ifname 161 | ); 162 | Ok(()) 163 | } 164 | 165 | fn read_interface_data(&self) -> Result { 166 | debug!("Reading host info for interface {}", self.ifname); 167 | let host = netlink::get_host(&self.ifname)?; 168 | debug!("Host info read for interface {}", self.ifname); 169 | Ok(host) 170 | } 171 | 172 | /// Sets DNS configuration for a Wireguard interface using the `resolvconf` command. 173 | /// 174 | /// It executes the `resolvconf` command with appropriate arguments to update DNS 175 | /// configurations for the specified Wireguard interface. The DNS entries are filtered 176 | /// for nameservers and search domains before being piped to the `resolvconf` command. 177 | fn configure_dns( 178 | &self, 179 | dns: &[IpAddr], 180 | search_domains: &[&str], 181 | ) -> Result<(), WireguardInterfaceError> { 182 | if dns.is_empty() { 183 | warn!("Received empty DNS server list. Skipping DNS configuration..."); 184 | return Ok(()); 185 | } 186 | configure_dns(&self.ifname, dns, search_domains)?; 187 | Ok(()) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/wgapi_userspace.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | io::{self, BufRead, BufReader, ErrorKind, Read, Write}, 4 | net::{IpAddr, Shutdown}, 5 | os::unix::net::UnixStream, 6 | process::Command, 7 | time::Duration, 8 | }; 9 | 10 | #[cfg(feature = "check_dependencies")] 11 | use crate::dependencies::check_external_dependencies; 12 | #[cfg(target_os = "linux")] 13 | use crate::netlink; 14 | #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] 15 | use crate::utils::clear_dns; 16 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 17 | use crate::{bsd, utils::resolve}; 18 | use crate::{ 19 | check_command_output_status, 20 | error::WireguardInterfaceError, 21 | utils::{add_peer_routing, configure_dns}, 22 | wgapi::{Userspace, WGApi}, 23 | wireguard_interface::WireguardInterfaceApi, 24 | Host, InterfaceConfiguration, IpAddrMask, Key, Peer, 25 | }; 26 | 27 | const USERSPACE_EXECUTABLE: &str = "wireguard-go"; 28 | 29 | /// Manages interfaces created with `wireguard-go`. 30 | /// 31 | /// We assume that `wireguard-go` executable is managed externally and available in `PATH`. 32 | /// Currently works on Unix platforms. 33 | impl WGApi { 34 | fn socket_path(&self) -> String { 35 | format!("/var/run/wireguard/{}.sock", self.ifname) 36 | } 37 | 38 | /// Create UNIX socket to communicate with `wireguard-go`. 39 | fn socket(&self) -> io::Result { 40 | let path = self.socket_path(); 41 | let socket = UnixStream::connect(path)?; 42 | socket.set_read_timeout(Some(Duration::new(3, 0)))?; 43 | Ok(socket) 44 | } 45 | 46 | // FIXME: currently other errors are ignored and result in 0 being returned. 47 | fn parse_errno(buf: impl Read) -> u32 { 48 | let reader = BufReader::new(buf); 49 | for line_result in reader.lines() { 50 | let line = match line_result { 51 | Ok(line) => line, 52 | Err(err) => { 53 | error!("Error parsing errno buffer line: {err}, continuing with next line..."); 54 | continue; 55 | } 56 | }; 57 | if let Some((keyword, value)) = line.split_once('=') { 58 | if keyword == "errno" { 59 | match value.parse() { 60 | Ok(errno) => return errno, 61 | Err(err) => { 62 | error!("Failed to parse errno: {err}, using default value 0"); 63 | return 0; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 0 70 | } 71 | 72 | /// Read host information using user-space API. 73 | pub fn read_host(&self) -> io::Result { 74 | let mut socket = self.socket()?; 75 | socket.write_all(b"get=1\n\n")?; 76 | Host::parse_uapi(socket) 77 | } 78 | 79 | /// Write host information using user-space API. 80 | pub fn write_host(&self, host: &Host) -> io::Result<()> { 81 | let mut socket = self.socket()?; 82 | socket.write_all(b"set=1\n")?; 83 | socket.write_all(host.as_uapi().as_bytes())?; 84 | socket.write_all(b"\n")?; 85 | 86 | if Self::parse_errno(socket) == 0 { 87 | Ok(()) 88 | } else { 89 | Err(io::Error::new( 90 | io::ErrorKind::InvalidData, 91 | "write configuration error", 92 | )) 93 | } 94 | } 95 | } 96 | 97 | impl WireguardInterfaceApi for WGApi { 98 | fn create_interface(&self) -> Result<(), WireguardInterfaceError> { 99 | debug!("Creating userspace interface {}", self.ifname); 100 | let output = Command::new(USERSPACE_EXECUTABLE) 101 | .arg(&self.ifname) 102 | .output()?; 103 | check_command_output_status(output)?; 104 | debug!("Userspace interface {} created successfully", self.ifname); 105 | Ok(()) 106 | } 107 | 108 | /// Sets DNS configuration for a WireGuard interface using the `resolvconf` command. 109 | /// 110 | /// This function is platform-specific and is intended for use on Linux and FreeBSD. 111 | /// It executes the `resolvconf -a -m -0 -x` command with appropriate arguments to update DNS 112 | /// configurations for the specified Wireguard interface. The DNS entries are filtered 113 | /// for nameservers and search domains before being piped to the `resolvconf` command. 114 | /// 115 | /// # Errors 116 | /// 117 | /// Returns a `WireguardInterfaceError::DnsError` if there is an error in setting the DNS configuration. 118 | /// 119 | /// # Platform Support 120 | /// 121 | /// - Linux 122 | /// - FreeBSD 123 | fn configure_dns( 124 | &self, 125 | dns: &[IpAddr], 126 | search_domains: &[&str], 127 | ) -> Result<(), WireguardInterfaceError> { 128 | if dns.is_empty() { 129 | warn!("Received empty DNS server list. Skipping DNS configuration..."); 130 | return Ok(()); 131 | } 132 | debug!("Beginning DNS configuration for interface {}", self.ifname); 133 | // Setting DNS is not supported for macOS. 134 | #[cfg(target_os = "macos")] 135 | { 136 | configure_dns(dns, search_domains)?; 137 | } 138 | #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "netbsd"))] 139 | { 140 | configure_dns(&self.ifname, dns, search_domains)?; 141 | } 142 | debug!("Finished configuring DNS for interface {}", self.ifname); 143 | Ok(()) 144 | } 145 | 146 | /// Assign IP address to network interface. 147 | fn assign_address(&self, address: &IpAddrMask) -> Result<(), WireguardInterfaceError> { 148 | debug!("Assigning address {address} to interface {}", self.ifname); 149 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 150 | bsd::assign_address(&self.ifname, address)?; 151 | #[cfg(target_os = "linux")] 152 | netlink::address_interface(&self.ifname, address)?; 153 | debug!("Address {address} assigned to interface {}", self.ifname); 154 | 155 | Ok(()) 156 | } 157 | 158 | /// Configure network interface. 159 | fn configure_interface( 160 | &self, 161 | config: &InterfaceConfiguration, 162 | ) -> Result<(), WireguardInterfaceError> { 163 | debug!( 164 | "Configuring interface {} with config: {config:?}", 165 | self.ifname 166 | ); 167 | 168 | // Assign IP addresses to the interface. 169 | for address in &config.addresses { 170 | self.assign_address(address)?; 171 | } 172 | 173 | // configure interface 174 | debug!( 175 | "Applying the WireGuard host configuration for interface {}", 176 | self.ifname 177 | ); 178 | let host = config.try_into()?; 179 | self.write_host(&host)?; 180 | debug!( 181 | "WireGuard host configuration set for interface {}.", 182 | self.ifname 183 | ); 184 | trace!("WireGuard host configuration: {host:?}"); 185 | 186 | // Set maximum transfer unit (MTU). 187 | if let Some(mtu) = config.mtu { 188 | debug!("Setting MTU of {mtu} for interface {}", self.ifname); 189 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 190 | bsd::set_mtu(&self.ifname, mtu)?; 191 | #[cfg(target_os = "linux")] 192 | netlink::set_mtu(&self.ifname, mtu)?; 193 | debug!( 194 | "MTU of {mtu} set for interface {}, value: {mtu}", 195 | self.ifname 196 | ); 197 | } 198 | 199 | info!( 200 | "Interface {} has been successfully configured. \ 201 | It has been assigned the following addresses: {:?}", 202 | self.ifname, config.addresses 203 | ); 204 | debug!( 205 | "Interface {} configured with config: {config:?}", 206 | self.ifname 207 | ); 208 | 209 | Ok(()) 210 | } 211 | 212 | /// Add peer addresses to network routing table. 213 | /// 214 | /// # Linux: 215 | /// On a Linux system, the `sysctl` command is required to work if using `0.0.0.0/0` or `::/0`. 216 | /// For every allowed IP, it runs: 217 | /// `ip route add dev ` 218 | /// `` - interface name while creating api 219 | /// `` - `-4` or `-6` based on allowed ip type 220 | /// ``- one of [Peer](crate::Peer) allowed ip 221 | /// 222 | /// For `0.0.0.0/0` or `::/0` allowed IP, it runs belowed additional commands in order: 223 | /// - `ip route add 0.0.0.0/0 dev table ` 224 | /// `` - fwmark attribute of [Host](crate::Host) or 51820 default if value is `None`. 225 | /// `` - Interface name. 226 | /// - `ip rule add not fwmark table `. 227 | /// - `ip rule add table main suppress_prefixlength 0`. 228 | /// - `sysctl -q net.ipv4.conf.all.src_valid_mark=1` - runs only for `0.0.0.0/0`. 229 | /// - `iptables-restore -n`. For `0.0.0.0/0` only. 230 | /// - `iptables6-restore -n`. For `::/0` only. 231 | /// 232 | /// Based on IP type `` will be equal to `-4` or `-6`. 233 | /// 234 | /// 235 | /// # macOS, FreeBSD: 236 | /// For every allowed IP, it runs: 237 | /// - `route -q -n add allowed_ip -interface if_name` 238 | /// `ifname` - interface name while creating api 239 | /// `allowed_ip`- one of [Peer](crate::Peer) allowed ip 240 | /// 241 | /// For `0.0.0.0/0` or `::/0` allowed IP, it adds default routing and skips other routings. 242 | /// - `route -q -n add 0.0.0.0/1 -interface if_name`. 243 | /// - `route -q -n add 128.0.0.0/1 -interface if_name`. 244 | /// - `route -q -n add -gateway ` 245 | /// `` - Add routing for every unique Peer endpoint. 246 | /// ``- Gateway extracted using `netstat -nr -f `. 247 | fn configure_peer_routing(&self, peers: &[Peer]) -> Result<(), WireguardInterfaceError> { 248 | add_peer_routing(peers, &self.ifname)?; 249 | Ok(()) 250 | } 251 | 252 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 253 | fn remove_endpoint_routing(&self, endpoint: &str) -> Result<(), WireguardInterfaceError> { 254 | debug!("Removing routing to {endpoint}, interface: {}", self.ifname); 255 | let endpoint_addr = resolve(endpoint)?; 256 | let host = IpAddrMask::host(endpoint_addr.ip()); 257 | match bsd::delete_gateway(&host) { 258 | Ok(()) => debug!("Removed routing to {host}"), 259 | Err(err) => debug!("Failed to remove routing to {host}: {err}"), 260 | } 261 | 262 | Ok(()) 263 | } 264 | 265 | /// Remove WireGuard network interface. 266 | fn remove_interface(&self) -> Result<(), WireguardInterfaceError> { 267 | debug!("Removing interface {}", self.ifname); 268 | // `wireguard-go` should by design shut down if the socket is removed 269 | debug!( 270 | "Shutting down socket for interface {}, checking if the socket to remove exists...", 271 | self.ifname 272 | ); 273 | match self.socket() { 274 | Ok(socket) => { 275 | debug!( 276 | "Socket exists, removing the socket for interface {}", 277 | self.ifname 278 | ); 279 | socket.shutdown(Shutdown::Both).map_err(|err| { 280 | WireguardInterfaceError::UnixSockerError(format!( 281 | "Failed to shutdown socket for interface {}: {err}", 282 | self.ifname 283 | )) 284 | })?; 285 | fs::remove_file(self.socket_path())?; 286 | debug!("Socket removed for interface {}", self.ifname); 287 | } 288 | Err(err) if err.kind() == io::ErrorKind::NotFound => { 289 | debug!("Socket not found for interface {}, skipping removal as there is nothing to remove. Continuing with further cleanup.", self.ifname); 290 | } 291 | Err(err) => { 292 | return Err(WireguardInterfaceError::UnixSockerError(format!( 293 | "Failed to remove socket for interface {}: {err}", 294 | self.ifname 295 | ))); 296 | } 297 | } 298 | 299 | #[cfg(target_os = "macos")] 300 | { 301 | debug!("Clearing DNS entries by applying an empty DNS list to all network services, interface {}", self.ifname); 302 | configure_dns(&[], &[])?; 303 | } 304 | #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))] 305 | { 306 | debug!("Clearing DNS entries for interface {}", self.ifname); 307 | clear_dns(&self.ifname)?; 308 | } 309 | debug!("DNS entries cleared, interface {}", self.ifname); 310 | 311 | info!("Interface {} removed successfully", self.ifname); 312 | Ok(()) 313 | } 314 | 315 | fn configure_peer(&self, peer: &Peer) -> Result<(), WireguardInterfaceError> { 316 | debug!("Configuring peer {peer:?} on interface {}", self.ifname); 317 | let mut socket = self.socket()?; 318 | socket.write_all(b"set=1\n")?; 319 | socket.write_all(peer.as_uapi_update().as_bytes())?; 320 | socket.write_all(b"\n")?; 321 | let errno = Self::parse_errno(socket); 322 | 323 | if errno == 0 { 324 | info!("Peer {peer:?} configured on interface {}", self.ifname); 325 | Ok(()) 326 | } else { 327 | Err(WireguardInterfaceError::PeerConfigurationError(format!( 328 | "Failed to configure peer {peer:?} on interface {}, errno: {errno}", 329 | self.ifname 330 | ))) 331 | } 332 | } 333 | 334 | fn remove_peer(&self, peer_pubkey: &Key) -> Result<(), WireguardInterfaceError> { 335 | debug!( 336 | "Removing peer with public key {peer_pubkey} from interface {}", 337 | self.ifname 338 | ); 339 | let mut socket = self.socket()?; 340 | socket.write_all(b"set=1\n")?; 341 | socket.write_all( 342 | format!("public_key={}\nremove=true\n", peer_pubkey.to_lower_hex()).as_bytes(), 343 | )?; 344 | socket.write_all(b"\n")?; 345 | 346 | let errno = Self::parse_errno(socket); 347 | 348 | if errno == 0 { 349 | info!( 350 | "Peer with public key {peer_pubkey} removed from interface {}", 351 | self.ifname 352 | ); 353 | Ok(()) 354 | } else { 355 | Err(WireguardInterfaceError::PeerConfigurationError(format!( 356 | "Failed to remove peer with public key {peer_pubkey} from interface {}, errno: {errno}", 357 | self.ifname 358 | ))) 359 | } 360 | } 361 | 362 | fn read_interface_data(&self) -> Result { 363 | debug!( 364 | "Reading interface configuration and statistics for interface {}", 365 | self.ifname 366 | ); 367 | match self.read_host() { 368 | Ok(host) => { 369 | debug!("Interface configuration and statistics read successfully for interface {}", self.ifname); 370 | trace!("Network information: {host:?}"); 371 | Ok(host) 372 | } 373 | Err(err) => match err { 374 | err if err.kind() == ErrorKind::NotFound => { 375 | Err(WireguardInterfaceError::SocketClosed(format!( 376 | "Failed to read network information for interface {} data, the socket may have been closed before we've attempted to read. If the socket has been closed intentionally, this message can be ignored. Error details: {err}", 377 | self.ifname 378 | ))) 379 | } 380 | _ => Err(WireguardInterfaceError::ReadInterfaceError(format!( 381 | "Failed to read network information for interface {} data, error: {err}", 382 | self.ifname 383 | ))), 384 | }, 385 | } 386 | } 387 | } 388 | 389 | #[cfg(test)] 390 | mod tests { 391 | use std::io::Cursor; 392 | 393 | use super::*; 394 | 395 | #[test] 396 | fn test_parse_errno() { 397 | let buf = Cursor::new(b"errno=0\n"); 398 | assert_eq!(WGApi::::parse_errno(buf), 0); 399 | 400 | let buf = Cursor::new(b"errno=12345\n"); 401 | assert_eq!(WGApi::::parse_errno(buf), 12345); 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/wgapi_windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | fs::File, 4 | io::{BufRead, BufReader, Cursor, Write}, 5 | net::{IpAddr, SocketAddr}, 6 | process::Command, 7 | str::FromStr, 8 | thread::sleep, 9 | time::{Duration, SystemTime}, 10 | }; 11 | 12 | use crate::{ 13 | error::WireguardInterfaceError, 14 | host::{Host, Peer}, 15 | key::Key, 16 | net::IpAddrMask, 17 | wgapi::{Kernel, WGApi}, 18 | InterfaceConfiguration, WireguardInterfaceApi, 19 | }; 20 | 21 | /// Manages interfaces created with Windows kernel using https://git.zx2c4.com/wireguard-nt. 22 | impl WireguardInterfaceApi for WGApi { 23 | fn create_interface(&self) -> Result<(), WireguardInterfaceError> { 24 | info!("Opening/creating interface {}", self.ifname); 25 | Ok(()) 26 | } 27 | 28 | fn assign_address(&self, address: &IpAddrMask) -> Result<(), WireguardInterfaceError> { 29 | debug!("Assigning address {address} to interface {}", self.ifname); 30 | Ok(()) 31 | } 32 | 33 | fn configure_interface( 34 | &self, 35 | config: &InterfaceConfiguration, 36 | dns: &[IpAddr], 37 | search_domains: &[&str], 38 | ) -> Result<(), WireguardInterfaceError> { 39 | debug!( 40 | "Configuring interface {} with config: {config:?}", 41 | self.ifname 42 | ); 43 | 44 | // Interface is created here so that there is no need to pass private key only for Windows 45 | let file_name = format!("{}.conf", &self.ifname); 46 | let path = env::current_dir()?; 47 | let file_path_buf = path.join(&file_name); 48 | let file_path = file_path_buf.to_str().unwrap_or_default(); 49 | 50 | debug!("Creating WireGuard configuration file {file_name} in: {file_path}"); 51 | 52 | let mut file = File::create(&file_name)?; 53 | 54 | debug!("WireGuard configuration file {file_name} created in {file_path}. Preparing configuration..."); 55 | 56 | let address = config 57 | .addresses 58 | .iter() 59 | .map(|addr| addr.to_string()) 60 | .collect::>() 61 | .join(","); 62 | let mut wireguard_configuration = format!( 63 | "[Interface]\nPrivateKey = {}\nAddress = {address}\n", 64 | config.prvkey 65 | ); 66 | 67 | if !dns.is_empty() { 68 | // Format: 69 | // DNS = , 70 | // If search domains are present: 71 | // DNS = ,,, 72 | let dns_addresses = format!( 73 | "\nDNS = {}{}", 74 | // DNS addresses part 75 | dns.iter() 76 | .map(|v| v.to_string()) 77 | .collect::>() 78 | .join(","), 79 | // Search domains part, optional 80 | if !search_domains.is_empty() { 81 | format!( 82 | ",{}", 83 | search_domains 84 | .iter() 85 | .map(|v| v.to_string()) 86 | .collect::>() 87 | .join(",") 88 | ) 89 | } else { 90 | "".to_string() 91 | } 92 | ); 93 | wireguard_configuration.push_str(dns_addresses.as_str()); 94 | } 95 | 96 | for peer in &config.peers { 97 | wireguard_configuration 98 | .push_str(format!("\n[Peer]\nPublicKey = {}", peer.public_key).as_str()); 99 | 100 | if let Some(preshared_key) = &peer.preshared_key { 101 | wireguard_configuration 102 | .push_str(format!("\nPresharedKey = {}", preshared_key).as_str()); 103 | } 104 | 105 | if let Some(keep_alive) = peer.persistent_keepalive_interval { 106 | wireguard_configuration 107 | .push_str(format!("\nPersistentKeepalive = {}", keep_alive).as_str()); 108 | } 109 | 110 | if let Some(endpoint) = peer.endpoint { 111 | wireguard_configuration.push_str(format!("\nEndpoint = {}", endpoint).as_str()); 112 | } 113 | 114 | if !peer.allowed_ips.is_empty() { 115 | let allowed_ips = format!( 116 | "\nAllowedIPs = {}", 117 | peer.allowed_ips 118 | .iter() 119 | .map(|v| v.to_string()) 120 | .collect::>() 121 | .join(",") 122 | ); 123 | wireguard_configuration.push_str(allowed_ips.as_str()); 124 | } 125 | } 126 | 127 | debug!( 128 | "WireGuard configuration prepared: {wireguard_configuration}, writing to the file at {file_path}..." 129 | ); 130 | file.write_all(wireguard_configuration.as_bytes())?; 131 | info!("WireGuard configuration written to file: {file_path}",); 132 | 133 | // Check for existing service and remove it 134 | debug!( 135 | "Checking for existing wireguard service for interface {}", 136 | self.ifname 137 | ); 138 | let output = Command::new("wg") 139 | .arg("show") 140 | .arg(&self.ifname) 141 | .output() 142 | .map_err(|err| { 143 | error!("Failed to read interface data. Error: {err}"); 144 | WireguardInterfaceError::ReadInterfaceError(err.to_string()) 145 | })?; 146 | debug!("WireGuard service check output: {output:?}",); 147 | 148 | // Service already exists 149 | if output.status.success() { 150 | debug!("Service already exists, removing it first"); 151 | Command::new("wireguard") 152 | .arg("/uninstalltunnelservice") 153 | .arg(&self.ifname) 154 | .output()?; 155 | 156 | debug!("Waiting for service to be removed"); 157 | let mut counter = 1; 158 | loop { 159 | // Occasionally the tunnel is still available even though wg show cannot find it, causing /installtunnelservice to fail 160 | // This might be excessive as closing the application closes the WireGuard tunnel. 161 | sleep(Duration::from_secs(1)); 162 | 163 | let output = Command::new("wg") 164 | .arg("show") 165 | .arg(&self.ifname) 166 | .output() 167 | .map_err(|err| { 168 | error!("Failed to read interface data. Error: {err}"); 169 | WireguardInterfaceError::ReadInterfaceError(err.to_string()) 170 | })?; 171 | 172 | // Service has been removed 173 | if !output.status.success() || counter == 5 { 174 | break; 175 | } 176 | 177 | counter += 1; 178 | } 179 | debug!("Finished waiting for service to be removed, the service is considered to be removed, proceeding further"); 180 | } 181 | 182 | debug!("Installing the new service for interface {}", self.ifname); 183 | let service_installation_output = Command::new("wireguard") 184 | .arg("/installtunnelservice") 185 | .arg(file_path) 186 | .output() 187 | .map_err(|err| { 188 | error!("Failed to create interface. Error: {err}"); 189 | WireguardInterfaceError::ServiceInstallationFailed(err.to_string()) 190 | })?; 191 | 192 | debug!("Done installing the new service. Service installation output: {service_installation_output:?}",); 193 | 194 | if !service_installation_output.status.success() { 195 | let message = format!( 196 | "Failed to install WireGuard tunnel as a Windows service: {:?}", 197 | service_installation_output.stdout 198 | ); 199 | return Err(WireguardInterfaceError::ServiceInstallationFailed(message)); 200 | } 201 | 202 | debug!( 203 | "Disabling automatic restart for interface {} tunnel service", 204 | self.ifname 205 | ); 206 | let service_update_output = Command::new("sc") 207 | .arg("config") 208 | .arg(format!("WireGuardTunnel${}", self.ifname)) 209 | .arg("start=demand") 210 | .output() 211 | .map_err(|err| { 212 | error!("Failed to configure tunnel service. Error: {err}"); 213 | WireguardInterfaceError::ServiceInstallationFailed(err.to_string()) 214 | })?; 215 | 216 | debug!("Done disabling automatic restart for the new service. Service update output: {service_update_output:?}",); 217 | if !service_update_output.status.success() { 218 | let message = format!( 219 | "Failed to configure WireGuard tunnel service: {:?}", 220 | service_update_output.stdout 221 | ); 222 | return Err(WireguardInterfaceError::ServiceInstallationFailed(message)); 223 | } 224 | 225 | // TODO: set maximum transfer unit (MTU) 226 | 227 | info!( 228 | "Interface {} has been successfully configured.", 229 | self.ifname 230 | ); 231 | debug!( 232 | "Interface {} configured with config: {config:?}", 233 | self.ifname 234 | ); 235 | Ok(()) 236 | } 237 | 238 | fn configure_peer_routing(&self, _peers: &[Peer]) -> Result<(), WireguardInterfaceError> { 239 | Ok(()) 240 | } 241 | 242 | fn remove_interface(&self) -> Result<(), WireguardInterfaceError> { 243 | debug!("Removing interface {}", self.ifname); 244 | 245 | let command_output = Command::new("wireguard") 246 | .arg("/uninstalltunnelservice") 247 | .arg(&self.ifname) 248 | .output() 249 | .map_err(|err| { 250 | error!("Failed to remove interface. Error: {err}"); 251 | WireguardInterfaceError::CommandExecutionFailed(err) 252 | })?; 253 | 254 | if !command_output.status.success() { 255 | let message = format!( 256 | "Failed to remove WireGuard tunnel service: {:?}", 257 | command_output.stdout 258 | ); 259 | return Err(WireguardInterfaceError::ServiceRemovalFailed(message)); 260 | } 261 | 262 | info!("Interface {} removed successfully", self.ifname); 263 | Ok(()) 264 | } 265 | 266 | fn configure_peer(&self, peer: &Peer) -> Result<(), WireguardInterfaceError> { 267 | debug!("Configuring peer {peer:?} on interface {}", self.ifname); 268 | Ok(()) 269 | } 270 | 271 | fn remove_peer(&self, peer_pubkey: &Key) -> Result<(), WireguardInterfaceError> { 272 | debug!( 273 | "Removing peer with public key {peer_pubkey} from interface {}", 274 | self.ifname 275 | ); 276 | Ok(()) 277 | } 278 | 279 | fn read_interface_data(&self) -> Result { 280 | debug!("Reading host info for interface {}", self.ifname); 281 | 282 | let output = Command::new("wg") 283 | .arg("show") 284 | .arg(&self.ifname) 285 | .arg("dump") 286 | .output() 287 | .map_err(|err| { 288 | error!("Failed to read interface. Error: {err}"); 289 | WireguardInterfaceError::CommandExecutionFailed(err) 290 | })?; 291 | 292 | let reader = BufReader::new(Cursor::new(output.stdout)); 293 | let mut host = Host::default(); 294 | let lines = reader.lines(); 295 | 296 | for (index, line_result) in lines.enumerate() { 297 | let line = match &line_result { 298 | Ok(line) => line, 299 | Err(_err) => { 300 | continue; 301 | } 302 | }; 303 | 304 | let data: Vec<&str> = line.split("\t").collect(); 305 | 306 | // First line contains [Interface] section data, every other line is a separate [Peer] 307 | if index == 0 { 308 | // Interface data: private key, public key, listen port, fwmark 309 | host.private_key = Key::from_str(data[0]).ok(); 310 | host.listen_port = data[2].parse().unwrap_or_default(); 311 | 312 | if data[3] != "off" { 313 | host.fwmark = Some(data[3].parse().unwrap()); 314 | } 315 | } else { 316 | // Peer data: public key, preshared key, endpoint, allowed ips, latest handshake, transfer-rx, transfer-tx, persistent-keepalive 317 | if let Ok(public_key) = Key::from_str(data[0]) { 318 | let mut peer = Peer::new(public_key.clone()); 319 | 320 | if data[1] != "(none)" { 321 | peer.preshared_key = Key::from_str(data[0]).ok(); 322 | } 323 | 324 | peer.endpoint = SocketAddr::from_str(data[2]).ok(); 325 | 326 | for allowed_ip in data[3].split(",") { 327 | let addr = IpAddrMask::from_str(allowed_ip.trim())?; 328 | peer.allowed_ips.push(addr); 329 | } 330 | 331 | let handshake = peer.last_handshake.get_or_insert(SystemTime::UNIX_EPOCH); 332 | *handshake += Duration::from_secs(data[4].parse().unwrap_or_default()); 333 | 334 | peer.rx_bytes = data[5].parse().unwrap_or_default(); 335 | peer.tx_bytes = data[6].parse().unwrap_or_default(); 336 | peer.persistent_keepalive_interval = data[7].parse().ok(); 337 | 338 | host.peers.insert(public_key.clone(), peer); 339 | } 340 | } 341 | } 342 | 343 | debug!("Read interface data: {host:?}"); 344 | Ok(host) 345 | } 346 | 347 | fn configure_dns( 348 | &self, 349 | dns: &[IpAddr], 350 | _search_domains: &[&str], 351 | ) -> Result<(), WireguardInterfaceError> { 352 | debug!( 353 | "Configuring DNS for interface {}, using address: {dns:?}", 354 | self.ifname 355 | ); 356 | Ok(()) 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/wireguard_interface.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use crate::{error::WireguardInterfaceError, Host, InterfaceConfiguration, IpAddrMask, Key, Peer}; 4 | 5 | /// API for managing a WireGuard interface. 6 | /// 7 | /// Specific interface being managed is identified by name. 8 | pub trait WireguardInterfaceApi { 9 | /// Creates a new WireGuard interface. 10 | fn create_interface(&self) -> Result<(), WireguardInterfaceError>; 11 | 12 | /// Assigns IP address to an existing interface. 13 | fn assign_address(&self, address: &IpAddrMask) -> Result<(), WireguardInterfaceError>; 14 | 15 | /// Add peer routing, basically a copy of `wg-quick up ` routing. 16 | /// Extracts all uniques allowed ips from [Peer](crate::Peer) slice and add routing for every 17 | /// address. 18 | fn configure_peer_routing(&self, peers: &[Peer]) -> Result<(), WireguardInterfaceError>; 19 | 20 | /// Remove routing to the given endpoint. 21 | /// This is needed for proper routing table cleanup. 22 | fn remove_endpoint_routing(&self, _endpoint: &str) -> Result<(), WireguardInterfaceError> { 23 | Ok(()) 24 | } 25 | 26 | /// Updates configuration of an existing WireGuard interface. 27 | #[cfg(not(target_os = "windows"))] 28 | fn configure_interface( 29 | &self, 30 | config: &InterfaceConfiguration, 31 | ) -> Result<(), WireguardInterfaceError>; 32 | 33 | #[cfg(target_os = "windows")] 34 | fn configure_interface( 35 | &self, 36 | config: &InterfaceConfiguration, 37 | dns: &[IpAddr], 38 | search_domains: &[&str], 39 | ) -> Result<(), WireguardInterfaceError>; 40 | 41 | /// Removes the WireGuard interface being managed. 42 | /// 43 | /// Meant to be used in `drop` method for a given API struct. 44 | fn remove_interface(&self) -> Result<(), WireguardInterfaceError>; 45 | 46 | /// Adds a peer or updates peer configuration. 47 | fn configure_peer(&self, peer: &Peer) -> Result<(), WireguardInterfaceError>; 48 | 49 | /// Removes a configured peer with a given pubkey. 50 | fn remove_peer(&self, peer_pubkey: &Key) -> Result<(), WireguardInterfaceError>; 51 | 52 | /// Reads current WireGuard interface configuration and stats. 53 | /// 54 | /// Similar to `wg show ` command. 55 | fn read_interface_data(&self) -> Result; 56 | 57 | /// Sets the DNS configuration for the WireGuard interface. 58 | /// 59 | /// This function takes a slice of DNS server addresses (`dns`) and search domains (`search_domains`) and configures the 60 | /// WireGuard interface to use them. If the search domain vector is empty it sets the "exclusive" flag making the DNS servers a 61 | /// preferred route for any domain. This method is equivalent to specifying the 62 | /// DNS section in a WireGuard configuration file and using `wg-quick` to apply the 63 | /// configuration. 64 | /// 65 | /// # Arguments 66 | /// 67 | /// * `dns` - A slice of [`IpAddr`](std::net::IpAddr) representing the DNS server addresses to be set for 68 | /// the WireGuard interface. 69 | /// 70 | /// * `search_domains` - A slice of [`&str`](std::str) representing the search domains to be set for 71 | /// the WireGuard interface. 72 | /// 73 | /// # Returns 74 | /// 75 | /// Returns `Ok(())` if the DNS configuration is successfully set, or an 76 | /// `Err(WireguardInterfaceError)` if there is an error during the configuration process. 77 | fn configure_dns( 78 | &self, 79 | dns: &[IpAddr], 80 | search_domains: &[&str], 81 | ) -> Result<(), WireguardInterfaceError>; 82 | } 83 | --------------------------------------------------------------------------------