├── .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 |
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 |
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 |
--------------------------------------------------------------------------------