Compare commits

..

17 commits

Author SHA1 Message Date
593bee9024
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 2s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-02-23 00:10:23 +01:00
09345f2193
fix(deps): update rust crate ring to v0.17.11
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
| datasource | package | from    | to      |
| ---------- | ------- | ------- | ------- |
| crate      | ring    | 0.17.10 | 0.17.11 |
2025-02-22 19:50:25 +01:00
71d1e43ef2
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-02-21 23:00:24 +01:00
528aad1d8e
fix(deps): update rust crate ring to v0.17.10
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 6s
/ check (module-nginx-test) (push) Successful in 6s
/ check (nextest) (push) Successful in 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 1s
| datasource | package | from   | to      |
| ---------- | ------- | ------ | ------- |
| crate      | ring    | 0.17.9 | 0.17.10 |
2025-02-21 18:30:23 +01:00
cb7e4d554b
fix(deps): update rust crate serde to v1.0.218
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 3s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
| datasource | package | from    | to      |
| ---------- | ------- | ------- | ------- |
| crate      | serde   | 1.0.217 | 1.0.218 |
2025-02-20 06:30:30 +01:00
01f53b2bf0
fix(deps): update rust crate serde_json to v1.0.139
All checks were successful
/ build (push) Successful in 2s
/ check (clippy) (push) Successful in 3s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
| datasource | package    | from    | to      |
| ---------- | ---------- | ------- | ------- |
| crate      | serde_json | 1.0.138 | 1.0.139 |
2025-02-20 04:10:23 +01:00
60662ff1f0
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
2025-02-19 17:41:04 +01:00
eaed7b2302
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 6s
/ check (module-ipv4-test) (push) Successful in 6s
/ check (module-ipv6-only-test) (push) Successful in 6s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
2025-02-17 23:00:41 +01:00
8a04c2726f
fix(deps): update rust crate clap to v4.5.30
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 3s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | clap    | 4.5.29 | 4.5.30 |
2025-02-17 20:20:29 +01:00
1a88dbaeb2
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 4s
/ report-size (push) Successful in 2s
2025-02-16 20:30:52 +01:00
c41008f800
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 6s
/ check (module-ipv6-only-test) (push) Successful in 6s
/ check (module-ipv6-test) (push) Successful in 6s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 1s
2025-02-15 23:00:22 +01:00
3c18f07a2a
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 2s
/ check (clippy) (push) Successful in 3s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 6s
/ check (module-nginx-test) (push) Successful in 6s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-02-14 02:10:35 +01:00
0a5348097d
fix(deps): update rust crate ring to v0.17.9
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 6s
/ check (module-ipv4-test) (push) Successful in 6s
/ check (module-ipv6-only-test) (push) Successful in 6s
/ check (module-ipv6-test) (push) Successful in 6s
/ check (module-nginx-test) (push) Successful in 6s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | ring    | 0.17.8 | 0.17.9 |
2025-02-14 02:00:26 +01:00
bdb27d7cb1
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 6s
/ check (module-ipv6-only-test) (push) Successful in 6s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-02-12 23:00:37 +01:00
41c30372fb
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 3s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
2025-02-11 21:40:30 +01:00
e99bc52de2
fix(deps): update rust crate clap to v4.5.29
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 3s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 6s
/ check (module-ipv6-only-test) (push) Successful in 6s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 7s
/ check (nextest) (push) Successful in 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | clap    | 4.5.28 | 4.5.29 |
2025-02-11 21:20:45 +01:00
338e296683
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 3s
/ check (module-ipv4-only-test) (push) Successful in 7s
/ check (module-ipv4-test) (push) Successful in 7s
/ check (module-ipv6-only-test) (push) Successful in 6s
/ check (module-ipv6-test) (push) Successful in 6s
/ check (module-nginx-test) (push) Successful in 6s
/ check (nextest) (push) Successful in 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 1s
2025-02-10 09:20:41 +01:00
16 changed files with 458 additions and 582 deletions

83
Cargo.lock generated
View file

@ -185,9 +185,9 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.12" version = "1.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -200,9 +200,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.28" version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -220,9 +220,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.27" version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -304,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
dependencies = [ dependencies = [
"nonempty", "nonempty",
"thiserror 1.0.69", "thiserror",
] ]
[[package]] [[package]]
@ -454,7 +454,6 @@ dependencies = [
"linked-hash-map", "linked-hash-map",
"once_cell", "once_cell",
"pin-project", "pin-project",
"serde",
"similar", "similar",
] ]
@ -502,9 +501,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.25" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]] [[package]]
name = "matchers" name = "matchers"
@ -543,7 +542,7 @@ dependencies = [
"supports-unicode", "supports-unicode",
"terminal_size", "terminal_size",
"textwrap", "textwrap",
"thiserror 1.0.69", "thiserror",
"unicode-width", "unicode-width",
] ]
@ -566,9 +565,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.3" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [ dependencies = [
"adler2", "adler2",
] ]
@ -623,9 +622,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "4.1.0" version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" checksum = "c1338d6deb23bc10a7767b72185570dd73cb3616188eec1088e19b5835f8cabb"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
@ -729,15 +728,14 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom",
"libc", "libc",
"spin",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -775,18 +773,18 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -795,9 +793,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.138" version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -859,9 +857,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]] [[package]]
name = "socket2" name = "socket2"
@ -873,12 +871,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -949,16 +941,7 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ dependencies = [
"thiserror-impl 1.0.69", "thiserror-impl",
]
[[package]]
name = "thiserror"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl 2.0.11",
] ]
[[package]] [[package]]
@ -972,17 +955,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "thiserror-impl"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.8" version = "1.1.8"
@ -1128,9 +1100,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.16" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]] [[package]]
name = "unicode-linebreak" name = "unicode-linebreak"
@ -1183,7 +1155,6 @@ dependencies = [
"ring", "ring",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.11",
"tokio", "tokio",
"tower-http", "tower-http",
"tracing", "tracing",

View file

@ -1,3 +1,5 @@
cargo-features = ["codegen-backend"]
[package] [package]
description = "An HTTP server using HTTP basic auth to make secure calls to nsupdate" description = "An HTTP server using HTTP basic auth to make secure calls to nsupdate"
name = "webnsupdate" name = "webnsupdate"
@ -27,14 +29,13 @@ miette = { version = "7", features = ["fancy"] }
ring = { version = "0.17", features = ["std"] } ring = { version = "0.17", features = ["std"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thiserror = "2"
tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] } tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] }
tower-http = { version = "0.6", features = ["validate-request"] } tower-http = { version = "0.6", features = ["validate-request"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies] [dev-dependencies]
insta = { version = "1", features = ["json"] } insta = "1"
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"
@ -45,3 +46,4 @@ codegen-units = 1
[profile.dev] [profile.dev]
debug = 0 debug = 0
codegen-backend = "cranelift"

View file

@ -14,38 +14,8 @@ let
mkPackageOption mkPackageOption
types types
; ;
format = pkgs.formats.json { };
in in
{ {
# imports = [
# (lib.mkRenamedOptionModule
# [ "services" "webnsupdate" "passwordFile" ]
# [ "services" "webnsupdate" "settings" "password_file" ]
# )
# (lib.mkRenamedOptionModule
# [ "services" "webnsupdate" "keyFile" ]
# [ "services" "webnsupdate" "settings" "key_file" ]
# )
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "allowedIPVersion" ] ''
# This option was replaced with 'services.webnsupdate.settings.ip_type' which defaults to Both.
# '')
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "bindIp" ] ''
# This option was replaced with 'services.webnsupdate.settings.address' which defaults to 127.0.0.1:5353.
# '')
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "bindPort" ] ''
# This option was replaced with 'services.webnsupdate.settings.address' which defaults to 127.0.0.1:5353.
# '')
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "records" ] ''
# This option was replaced with 'services.webnsupdate.settings.records' which defaults to [].
# '')
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "recordsFile" ] ''
# This option was replaced with 'services.webnsupdate.settings.records' which defaults to [].
# '')
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "ttl" ] ''
# This option was replaced with 'services.webnsupdate.settings.ttl' which defaults to 600s.
# '')
# ];
options.services.webnsupdate = mkOption { options.services.webnsupdate = mkOption {
description = "An HTTP server for nsupdate."; description = "An HTTP server for nsupdate.";
default = { }; default = { };
@ -61,35 +31,34 @@ let
example = [ "--ip-source" ]; example = [ "--ip-source" ];
}; };
package = mkPackageOption pkgs "webnsupdate" { }; package = mkPackageOption pkgs "webnsupdate" { };
settings = mkOption { bindIp = mkOption {
description = "The webnsupdate JSON configuration";
default = { };
type = types.submodule {
freeformType = format.type;
options = {
address = mkOption {
description = '' description = ''
IP address and port to bind to. IP address to bind to.
Setting it to anything other than localhost is very Setting it to anything other than localhost is very insecure as
insecure as `webnsupdate` only supports plain HTTP and `webnsupdate` only supports plain HTTP and should always be behind a
should always be behind a reverse proxy. reverse proxy.
''; '';
type = types.str; type = types.str;
default = "127.0.0.1:5353"; default = "localhost";
example = "[::1]:5353"; example = "0.0.0.0";
}; };
ip_type = mkOption { bindPort = mkOption {
description = "Port to bind to.";
type = types.port;
default = 5353;
};
allowedIPVersion = mkOption {
description = ''The allowed IP versions to accept updates from.''; description = ''The allowed IP versions to accept updates from.'';
type = types.enum [ type = types.enum [
"Both" "both"
"Ipv4Only" "ipv4-only"
"Ipv6Only" "ipv6-only"
]; ];
default = "Both"; default = "both";
example = "Ipv4Only"; example = "ipv4-only";
}; };
password_file = mkOption { passwordFile = mkOption {
description = '' description = ''
The file where the password is stored. The file where the password is stored.
@ -98,7 +67,7 @@ let
type = types.path; type = types.path;
example = "/secrets/webnsupdate.pass"; example = "/secrets/webnsupdate.pass";
}; };
key_file = mkOption { keyFile = mkOption {
description = '' description = ''
The TSIG key that `nsupdate` should use. The TSIG key that `nsupdate` should use.
@ -110,43 +79,34 @@ let
}; };
ttl = mkOption { ttl = mkOption {
description = "The TTL that should be set on the zone records created by `nsupdate`."; description = "The TTL that should be set on the zone records created by `nsupdate`.";
default = { type = types.ints.positive;
secs = 600; default = 60;
};
example = {
secs = 600;
nanos = 50000;
};
type = types.submodule {
options = {
secs = mkOption {
description = "The TTL (in seconds) that should be set on the zone records created by `nsupdate`.";
example = 3600; example = 3600;
}; };
nanos = mkOption {
description = "The TTL (in nanoseconds) that should be set on the zone records created by `nsupdate`.";
default = 0;
example = 50000;
};
};
};
};
records = mkOption { records = mkOption {
description = '' description = ''
The fqdn of records that should be updated. The fqdn of records that should be updated.
Empty lines will be ignored, but whitespace will not be. Empty lines will be ignored, but whitespace will not be.
''; '';
type = types.listOf types.str; type = types.nullOr types.lines;
default = [ ]; default = null;
example = [ example = ''
"example.com." example.com.
"example.org."
"ci.example.org." example.org.
]; ci.example.org.
}; '';
};
}; };
recordsFile = mkOption {
description = ''
The fqdn of records that should be updated.
Empty lines will be ignored, but whitespace will not be.
'';
type = types.nullOr types.path;
default = null;
example = "/secrets/webnsupdate.records";
}; };
user = mkOption { user = mkOption {
description = "The user to run as."; description = "The user to run as.";
@ -164,14 +124,41 @@ let
config = config =
let let
configFile = format.generate "webnsupdate.json" cfg.settings; recordsFile =
args = lib.strings.escapeShellArgs ([ "--config=${configFile}" ] ++ cfg.extraArgs); if cfg.recordsFile != null then cfg.recordsFile else pkgs.writeText "webnsrecords" cfg.records;
args = lib.strings.escapeShellArgs (
[
"--records"
recordsFile
"--key-file"
cfg.keyFile
"--password-file"
cfg.passwordFile
"--address"
cfg.bindIp
"--ip-type"
cfg.allowedIPVersion
"--port"
(builtins.toString cfg.bindPort)
"--ttl"
(builtins.toString cfg.ttl)
"--data-dir=%S/webnsupdate"
]
++ cfg.extraArgs
);
cmd = "${lib.getExe cfg.package} ${args}"; cmd = "${lib.getExe cfg.package} ${args}";
in in
lib.mkIf cfg.enable { lib.mkIf cfg.enable {
# FIXME: re-enable once I stop using the patched version of bind
# warnings = # warnings =
# lib.optional (!config.services.bind.enable) "`webnsupdate` is expected to be used alongside `bind`. This is an unsupported configuration."; # lib.optional (!config.services.bind.enable) "`webnsupdate` is expected to be used alongside `bind`. This is an unsupported configuration.";
assertions = [
{
assertion =
(cfg.records != null || cfg.recordsFile != null)
&& !(cfg.records != null && cfg.recordsFile != null);
message = "Exactly one of `services.webnsupdate.records` and `services.webnsupdate.recordsFile` must be set.";
}
];
systemd.services.webnsupdate = { systemd.services.webnsupdate = {
description = "Web interface for nsupdate."; description = "Web interface for nsupdate.";
@ -180,10 +167,9 @@ let
"network.target" "network.target"
"bind.service" "bind.service"
]; ];
preStart = "${lib.getExe cfg.package} verify ${configFile}"; preStart = "${cmd} verify";
path = [ pkgs.dig ]; path = [ pkgs.dig ];
startLimitIntervalSec = 60; startLimitIntervalSec = 60;
environment.DATA_DIR = "%S/webnsupdate";
serviceConfig = { serviceConfig = {
ExecStart = [ cmd ]; ExecStart = [ cmd ];
Type = "exec"; Type = "exec";

View file

@ -9,7 +9,7 @@
lastIPPath = "/var/lib/webnsupdate/last-ip.json"; lastIPPath = "/var/lib/webnsupdate/last-ip.json";
zoneFile = pkgs.writeText "${testDomain}.zoneinfo" '' zoneFile = pkgs.writeText "${testDomain}.zoneinfo" ''
$TTL 600 ; 10 minutes $TTL 60 ; 1 minute
$ORIGIN ${testDomain}. $ORIGIN ${testDomain}.
@ IN SOA ns1.${testDomain}. admin.${testDomain}. ( @ IN SOA ns1.${testDomain}. admin.${testDomain}. (
1 ; serial 1 ; serial
@ -73,19 +73,20 @@
webnsupdate = { webnsupdate = {
enable = true; enable = true;
bindIp = lib.mkDefault "127.0.0.1";
keyFile = "/etc/bind/rndc.key";
# test:test (user:password)
passwordFile = pkgs.writeText "webnsupdate.pass" "FQoNmuU1BKfg8qsU96F6bK5ykp2b0SLe3ZpB3nbtfZA";
package = self'.packages.webnsupdate; package = self'.packages.webnsupdate;
extraArgs = [ "-vvv" ]; # debug messages extraArgs = [
settings = { "-vvv" # debug messages
address = lib.mkDefault "127.0.0.1:5353"; "--ip-source=ConnectInfo"
key_file = "/etc/bind/rndc.key";
password_file = pkgs.writeText "webnsupdate.pass" "FQoNmuU1BKfg8qsU96F6bK5ykp2b0SLe3ZpB3nbtfZA"; # test:test
ip_source = lib.mkDefault "ConnectInfo";
records = [
"test1.${testDomain}."
"test2.${testDomain}."
"test3.${testDomain}."
]; ];
}; records = ''
test1.${testDomain}.
test2.${testDomain}.
test3.${testDomain}.
'';
}; };
}; };
}; };
@ -96,7 +97,7 @@
webnsupdate-ipv4-machine webnsupdate-ipv4-machine
]; ];
config.services.webnsupdate.settings.address = "[::1]:5353"; config.services.webnsupdate.bindIp = "::1";
}; };
webnsupdate-nginx-machine = webnsupdate-nginx-machine =
@ -108,26 +109,26 @@
config.services = { config.services = {
# Use default IP Source # Use default IP Source
webnsupdate.settings.ip_source = "RightmostXForwardedFor"; webnsupdate.extraArgs = lib.mkForce [ "-vvv" ]; # debug messages
nginx = { nginx = {
enable = true; enable = true;
recommendedProxySettings = true; recommendedProxySettings = true;
virtualHosts.webnsupdate.locations."/".proxyPass = virtualHosts.webnsupdate.locations."/".proxyPass =
"http://${config.services.webnsupdate.settings.address}"; "http://${config.services.webnsupdate.bindIp}:${builtins.toString config.services.webnsupdate.bindPort}";
}; };
}; };
}; };
webnsupdate-ipv4-only-machine = { webnsupdate-ipv4-only-machine = {
imports = [ webnsupdate-nginx-machine ]; imports = [ webnsupdate-nginx-machine ];
config.services.webnsupdate.settings.ip_type = "Ipv4Only"; config.services.webnsupdate.allowedIPVersion = "ipv4-only";
}; };
webnsupdate-ipv6-only-machine = { webnsupdate-ipv6-only-machine = {
imports = [ webnsupdate-nginx-machine ]; imports = [ webnsupdate-nginx-machine ];
config.services.webnsupdate.settings.ip_type = "Ipv6Only"; config.services.webnsupdate.allowedIPVersion = "ipv6-only";
}; };
# "A" for IPv4, "AAAA" for IPv6, "ANY" for any # "A" for IPv4, "AAAA" for IPv6, "ANY" for any
@ -157,9 +158,9 @@
STATIC_DOMAINS: list[str] = ["${testDomain}", "ns1.${testDomain}", "nsupdate.${testDomain}"] STATIC_DOMAINS: list[str] = ["${testDomain}", "ns1.${testDomain}", "nsupdate.${testDomain}"]
DYNAMIC_DOMAINS: list[str] = ["test1.${testDomain}", "test2.${testDomain}", "test3.${testDomain}"] DYNAMIC_DOMAINS: list[str] = ["test1.${testDomain}", "test2.${testDomain}", "test3.${testDomain}"]
def dig_cmd(domain: str, record: str, ip: str | None) -> tuple[str, str]: def dig_cmd(domain: str, record: str, ip: str | None) -> str:
match_ip = "" if ip is None else f"\\s\\+600\\s\\+IN\\s\\+{record}\\s\\+{ip}$" match_ip = "" if ip is None else f"\\s\\+60\\s\\+IN\\s\\+{record}\\s\\+{ip}$"
return f"dig @localhost {record} {domain} +noall +answer", f"grep '^{domain}.{match_ip}'" return f"dig @localhost {record} {domain} +noall +answer | grep '^{domain}.{match_ip}'"
def curl_cmd(domain: str, identity: str, path: str, query: dict[str, str]) -> str: def curl_cmd(domain: str, identity: str, path: str, query: dict[str, str]) -> str:
from urllib.parse import urlencode from urllib.parse import urlencode
@ -167,16 +168,10 @@
return f"{CURL} -u {identity} -X GET 'http://{domain}{"" if NGINX else ":5353"}/{path}{q}'" return f"{CURL} -u {identity} -X GET 'http://{domain}{"" if NGINX else ":5353"}/{path}{q}'"
def domain_available(domain: str, record: str, ip: str | None=None): def domain_available(domain: str, record: str, ip: str | None=None):
dig, grep = dig_cmd(domain, record, ip) machine.succeed(dig_cmd(domain, record, ip))
rc, output = machine.execute(dig)
print(f"{dig}[{rc}]: {output}")
machine.succeed(f"{dig} | {grep}")
def domain_missing(domain: str, record: str, ip: str | None=None): def domain_missing(domain: str, record: str, ip: str | None=None):
dig, grep = dig_cmd(domain, record, ip) machine.fail(dig_cmd(domain, record, ip))
rc, output = machine.execute(dig)
print(f"{dig}[{rc}]: {output}")
machine.fail(f"{dig} | {grep}")
def update_records(domain: str="localhost", /, *, path: str="update", **kwargs): def update_records(domain: str="localhost", /, *, path: str="update", **kwargs):
machine.succeed(curl_cmd(domain, "test:test", path, kwargs)) machine.succeed(curl_cmd(domain, "test:test", path, kwargs))

18
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1738652123, "lastModified": 1739936662,
"narHash": "sha256-zdZek5FXK/k95J0vnLF0AMnYuZl4AjARq83blKuJBYY=", "narHash": "sha256-x4syUjNUuRblR07nDPeLDP7DpphaBVbUaSoeZkFbGSk=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "c7e015a5fcefb070778c7d91734768680188a9cd", "rev": "19de14aaeb869287647d9461cbd389187d8ecdb7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -37,11 +37,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1738680400, "lastModified": 1739866667,
"narHash": "sha256-ooLh+XW8jfa+91F1nhf9OF7qhuA/y1ChLx6lXDNeY5U=", "narHash": "sha256-EO1ygNKZlsAC9avfcwHkKGMsmipUk1Uc0TbrEZpkn64=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "799ba5bffed04ced7067a91798353d360788b30d", "rev": "73cf49b8ad837ade2de76f87eb53fc85ed5d4680",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -82,11 +82,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1738680491, "lastModified": 1739829690,
"narHash": "sha256-8X7tR3kFGkE7WEF5EXVkt4apgaN85oHZdoTGutCFs6I=", "narHash": "sha256-mL1szCeIsjh6Khn3nH2cYtwO5YXG6gBiTw1A30iGeDU=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "64dbb922d51a42c0ced6a7668ca008dded61c483", "rev": "3d0579f5cc93436052d94b73925b48973a104204",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,217 +0,0 @@
use std::{
fs::File,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
time::Duration,
};
use axum_client_ip::SecureClientIpSource;
use miette::{Context, IntoDiagnostic};
#[derive(Debug, Default, Clone, Copy, serde::Deserialize, serde::Serialize)]
pub enum IpType {
#[default]
Both,
Ipv4Only,
Ipv6Only,
}
impl IpType {
pub fn valid_for_type(self, ip: IpAddr) -> bool {
match self {
IpType::Both => true,
IpType::Ipv4Only => ip.is_ipv4(),
IpType::Ipv6Only => ip.is_ipv6(),
}
}
}
impl std::fmt::Display for IpType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpType::Both => f.write_str("both"),
IpType::Ipv4Only => f.write_str("ipv4-only"),
IpType::Ipv6Only => f.write_str("ipv6-only"),
}
}
}
impl std::str::FromStr for IpType {
type Err = miette::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"both" => Ok(Self::Both),
"ipv4-only" => Ok(Self::Ipv4Only),
"ipv6-only" => Ok(Self::Ipv6Only),
_ => miette::bail!("expected one of 'ipv4-only', 'ipv6-only' or 'both', got '{s}'"),
}
}
}
/// Webserver settings
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Server {
/// Ip address and port of the server
#[serde(default = "default_address")]
pub address: SocketAddr,
}
/// Password settings
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Password {
/// File containing password to match against
///
/// Should be of the format `username:password` and contain a single password
#[serde(default, skip_serializing_if = "Option::is_none")]
pub password_file: Option<PathBuf>,
/// Salt to get more unique hashed passwords and prevent table based attacks
#[serde(default = "default_salt")]
pub salt: Box<str>,
}
/// Records settings
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Records {
/// Time To Live (in seconds) to set on the DNS records
#[serde(default = "default_ttl")]
pub ttl: Duration,
/// List of domain names for which to update the IP when an update is requested
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[allow(clippy::struct_field_names)]
pub records: Vec<Box<str>>,
/// If provided, when an IPv6 prefix is provided with an update, this will be used to derive
/// the full IPv6 address of the client
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<Ipv6Addr>,
/// If a client id is provided the ipv6 update will be ignored (only the prefix will be used).
/// This domain will point to the ipv6 address instead of the address derived from the client
/// id (usually this is the router).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub router_domain: Option<Box<str>>,
/// Set client IP source
///
/// see: <https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html>
#[serde(default = "default_ip_source")]
pub ip_source: SecureClientIpSource,
/// Set which IPs to allow updating (ipv4, ipv6 or both)
#[serde(default = "default_ip_type")]
pub ip_type: IpType,
/// Keyfile `nsupdate` should use
///
/// If specified, then `webnsupdate` must have read access to the file
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_file: Option<PathBuf>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Config {
/// Server Configuration
#[serde(flatten)]
pub server: Server,
/// Password Configuration
#[serde(flatten)]
pub password: Password,
/// Records Configuration
#[serde(flatten)]
pub records: Records,
}
impl Config {
/// Load the configuration without verifying it
pub fn load(path: &std::path::Path) -> miette::Result<Self> {
serde_json::from_reader::<File, Self>(
File::open(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed open {}", path.display()))?,
)
.into_diagnostic()
.wrap_err_with(|| format!("failed to load configuration from {}", path.display()))
}
/// Ensure only a verified configuration is returned
pub fn verified(self) -> miette::Result<Self> {
self.verify()?;
Ok(self)
}
/// Verify the configuration
pub fn verify(&self) -> Result<(), Invalid> {
let mut invalid_records: Vec<miette::Error> = self
.records
.records
.iter()
.filter_map(|record| crate::records::validate_record_str(record).err())
.collect();
invalid_records.extend(
self.records
.router_domain
.as_ref()
.and_then(|domain| crate::records::validate_record_str(domain).err()),
);
let err = Invalid { invalid_records };
if err.invalid_records.is_empty() {
Ok(())
} else {
Err(err)
}
}
}
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("the configuration was invalid")]
pub struct Invalid {
#[related]
pub invalid_records: Vec<miette::Error>,
}
// --- Default Values (sadly serde doesn't have a way to specify a constant as a default value) ---
fn default_ttl() -> Duration {
super::DEFAULT_TTL
}
fn default_salt() -> Box<str> {
super::DEFAULT_SALT.into()
}
fn default_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5353)
}
fn default_ip_source() -> SecureClientIpSource {
SecureClientIpSource::RightmostXForwardedFor
}
fn default_ip_type() -> IpType {
IpType::Both
}
#[test]
fn default_values_config_snapshot() {
let config: Config = serde_json::from_str("{}").unwrap();
insta::assert_json_snapshot!(config, @r#"
{
"address": "127.0.0.1:5353",
"salt": "UpdateMyDNS",
"ttl": {
"secs": 60,
"nanos": 0
},
"ip_source": "RightmostXForwardedFor",
"ip_type": "Both"
}
"#);
}

View file

@ -10,18 +10,16 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use axum_client_ip::SecureClientIp; use axum_client_ip::{SecureClientIp, SecureClientIpSource};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity; use clap_verbosity_flag::Verbosity;
use config::Config;
use http::StatusCode; use http::StatusCode;
use miette::{bail, ensure, Context, IntoDiagnostic, Result}; use miette::{bail, ensure, Context, IntoDiagnostic, Result};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
mod auth; mod auth;
mod config;
mod nsupdate; mod nsupdate;
mod password; mod password;
mod records; mod records;
@ -34,52 +32,120 @@ struct Opts {
#[command(flatten)] #[command(flatten)]
verbosity: Verbosity<clap_verbosity_flag::InfoLevel>, verbosity: Verbosity<clap_verbosity_flag::InfoLevel>,
/// Ip address of the server
#[arg(long, default_value = "127.0.0.1")]
address: IpAddr,
/// Port of the server
#[arg(long, default_value_t = 5353)]
port: u16,
/// File containing password to match against
///
/// Should be of the format `username:password` and contain a single password
#[arg(long)]
password_file: Option<PathBuf>,
/// Salt to get more unique hashed passwords and prevent table based attacks
#[arg(long, default_value = DEFAULT_SALT)]
salt: String,
/// Time To Live (in seconds) to set on the DNS records
#[arg(long, default_value_t = DEFAULT_TTL.as_secs())]
ttl: u64,
/// Data directory /// Data directory
#[arg(long, env, default_value = ".")] #[arg(long, default_value = ".")]
data_dir: PathBuf, data_dir: PathBuf,
/// File containing the records that should be updated when an update request is made
///
/// There should be one record per line:
///
/// ```text
/// example.com.
/// mail.example.com.
/// ```
#[arg(long)]
records: PathBuf,
/// Keyfile `nsupdate` should use
///
/// If specified, then `webnsupdate` must have read access to the file
#[arg(long)]
key_file: Option<PathBuf>,
/// Allow not setting a password /// Allow not setting a password
#[arg(long)] #[arg(long)]
insecure: bool, insecure: bool,
#[clap(flatten)] /// Set client IP source
config_or_command: ConfigOrCommand, ///
} /// see: <https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html>
#[clap(long, default_value = "RightmostXForwardedFor")]
ip_source: SecureClientIpSource,
#[derive(clap::Args, Debug)] /// Set which IPs to allow updating
#[group(multiple = false)] #[clap(long, default_value_t = IpType::Both)]
struct ConfigOrCommand { ip_type: IpType,
/// Path to the configuration file
#[arg(long, short)]
config: Option<PathBuf>,
#[clap(subcommand)] #[clap(subcommand)]
subcommand: Option<Cmd>, subcommand: Option<Cmd>,
} }
impl ConfigOrCommand { #[derive(Debug, Default, Clone, Copy)]
pub fn take(&mut self) -> (Option<PathBuf>, Option<Cmd>) { enum IpType {
(self.config.take(), self.subcommand.take()) #[default]
Both,
IPv4Only,
IPv6Only,
}
impl IpType {
fn valid_for_type(self, ip: IpAddr) -> bool {
match self {
IpType::Both => true,
IpType::IPv4Only => ip.is_ipv4(),
IpType::IPv6Only => ip.is_ipv6(),
}
}
}
impl std::fmt::Display for IpType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpType::Both => f.write_str("both"),
IpType::IPv4Only => f.write_str("ipv4-only"),
IpType::IPv6Only => f.write_str("ipv6-only"),
}
}
}
impl std::str::FromStr for IpType {
type Err = miette::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"both" => Ok(Self::Both),
"ipv4-only" => Ok(Self::IPv4Only),
"ipv6-only" => Ok(Self::IPv6Only),
_ => bail!("expected one of 'ipv4-only', 'ipv6-only' or 'both', got '{s}'"),
}
} }
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
enum Cmd { enum Cmd {
Mkpasswd(password::Mkpasswd), Mkpasswd(password::Mkpasswd),
/// Verify the configuration file /// Verify the records file
Verify { Verify,
/// Path to the configuration file
config: PathBuf,
},
} }
impl Cmd { impl Cmd {
pub fn process(self, args: &Opts) -> Result<()> { pub fn process(self, args: &Opts) -> Result<()> {
match self { match self {
Cmd::Mkpasswd(mkpasswd) => mkpasswd.process(args), Cmd::Mkpasswd(mkpasswd) => mkpasswd.process(args),
Cmd::Verify { config } => config::Config::load(&config) // load config Cmd::Verify => records::load(&args.records).map(drop),
.and_then(Config::verified) // verify config
.map(drop), // ignore config data
} }
} }
} }
@ -102,7 +168,7 @@ struct AppState<'a> {
last_ips: std::sync::Arc<tokio::sync::Mutex<SavedIPs>>, last_ips: std::sync::Arc<tokio::sync::Mutex<SavedIPs>>,
/// The IP type for which to allow updates /// The IP type for which to allow updates
ip_type: config::IpType, ip_type: IpType,
} }
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
@ -145,38 +211,33 @@ impl SavedIPs {
} }
impl AppState<'static> { impl AppState<'static> {
fn from_args(args: &Opts, config: &config::Config) -> miette::Result<Self> { fn from_args(args: &Opts) -> miette::Result<Self> {
let Opts { let Opts {
verbosity: _, verbosity: _,
address: _,
port: _,
password_file: _,
data_dir, data_dir,
key_file,
insecure, insecure,
config_or_command: _, subcommand: _,
} = args;
let config::Records {
ttl,
records, records,
client_id: _, salt: _,
router_domain: _, ttl,
ip_source: _, ip_source: _,
ip_type, ip_type,
key_file, } = args;
} = &config.records;
// Set state
let ttl = Duration::from_secs(*ttl);
// Use last registered IP address if available // Use last registered IP address if available
let ip_file = Box::leak(data_dir.join("last-ip.json").into_boxed_path()); let ip_file = Box::leak(data_dir.join("last-ip.json").into_boxed_path());
// Leak DNS records
let records: &[&str] = &*Vec::leak(
records
.iter()
.map(|record| &*Box::leak(record.clone()))
.collect(),
);
let state = AppState { let state = AppState {
ttl: *ttl, ttl,
records, // Load DNS records
records: records::load_no_verify(records)?,
// Load keyfile // Load keyfile
key_file: key_file key_file: key_file
.as_deref() .as_deref()
@ -279,37 +340,34 @@ fn main() -> Result<()> {
debug!("{args:?}"); debug!("{args:?}");
let config = match args.config_or_command.take() {
// process subcommand // process subcommand
(None, Some(cmd)) => return cmd.process(&args), if let Some(cmd) = args.subcommand.take() {
(Some(path), None) => { return cmd.process(&args);
let config = config::Config::load(&path)?;
if let Err(err) = config.verify() {
error!("failed to verify configuration: {err}");
} }
config
}
(None, None) | (Some(_), Some(_)) => unreachable!(
"bad state, one of config or subcommand should be available (clap should enforce this)"
),
};
// Initialize state // Initialize state
let state = AppState::from_args(&args, &config)?; let state = AppState::from_args(&args)?;
let Opts { let Opts {
verbosity: _, verbosity: _,
address: ip,
port,
password_file,
data_dir: _, data_dir: _,
key_file: _,
insecure, insecure,
config_or_command: _, subcommand: _,
records: _,
salt,
ttl: _,
ip_source,
ip_type,
} = args; } = args;
info!("checking environment"); info!("checking environment");
// Load password hash // Load password hash
let password_hash = config let password_hash = password_file
.password
.password_file
.map(|path| -> miette::Result<_> { .map(|path| -> miette::Result<_> {
let path = path.as_path(); let path = path.as_path();
let pass = std::fs::read_to_string(path).into_diagnostic()?; let pass = std::fs::read_to_string(path).into_diagnostic()?;
@ -340,13 +398,11 @@ fn main() -> Result<()> {
// Update DNS record with previous IPs (if available) // Update DNS record with previous IPs (if available)
let ips = state.last_ips.lock().await.clone(); let ips = state.last_ips.lock().await.clone();
let mut actions = ips let actions = ips
.ips() .ips()
.filter(|ip| config.records.ip_type.valid_for_type(*ip)) .filter(|ip| ip_type.valid_for_type(*ip))
.flat_map(|ip| nsupdate::Action::from_records(ip, state.ttl, state.records)) .flat_map(|ip| nsupdate::Action::from_records(ip, state.ttl, state.records));
.peekable();
if actions.peek().is_some() {
match nsupdate::nsupdate(state.key_file, actions).await { match nsupdate::nsupdate(state.key_file, actions).await {
Ok(status) => { Ok(status) => {
if !status.success() { if !status.success() {
@ -361,30 +417,24 @@ fn main() -> Result<()> {
.wrap_err("failed to update records with previous IP"); .wrap_err("failed to update records with previous IP");
} }
} }
}
// Create services // Create services
let app = Router::new().route("/update", get(update_records)); let app = Router::new().route("/update", get(update_records));
// if a password is provided, validate it // if a password is provided, validate it
let app = if let Some(pass) = password_hash { let app = if let Some(pass) = password_hash {
app.layer(auth::layer( app.layer(auth::layer(Box::leak(pass), String::leak(salt)))
Box::leak(pass),
Box::leak(config.password.salt),
))
} else { } else {
app app
} }
.layer(config.records.ip_source.into_extension()) .layer(ip_source.into_extension())
.with_state(state); .with_state(state);
let config::Server { address } = config.server;
// Start services // Start services
info!("starting listener on {address}"); info!("starting listener on {ip}:{port}");
let listener = tokio::net::TcpListener::bind(address) let listener = tokio::net::TcpListener::bind(SocketAddr::new(ip, port))
.await .await
.into_diagnostic()?; .into_diagnostic()?;
info!("listening on {address}"); info!("listening on {ip}:{port}");
axum::serve( axum::serve(
listener, listener,
app.into_make_service_with_connect_info::<SocketAddr>(), app.into_make_service_with_connect_info::<SocketAddr>(),
@ -523,15 +573,6 @@ async fn trigger_update(
state: &AppState<'static>, state: &AppState<'static>,
) -> axum::response::Result<&'static str> { ) -> axum::response::Result<&'static str> {
let actions = nsupdate::Action::from_records(ip, state.ttl, state.records); let actions = nsupdate::Action::from_records(ip, state.ttl, state.records);
if actions.len() == 0 {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Nothing to do (e.g. we are ipv4-only but an ipv6 update was requested)",
)
.into());
}
match nsupdate::nsupdate(state.key_file, actions).await { match nsupdate::nsupdate(state.key_file, actions).await {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let ips = { let ips = {

View file

@ -25,7 +25,7 @@ impl<'a> Action<'a> {
to: IpAddr, to: IpAddr,
ttl: Duration, ttl: Duration,
records: &'a [&'a str], records: &'a [&'a str],
) -> impl IntoIterator<Item = Self> + std::iter::ExactSizeIterator + 'a { ) -> impl IntoIterator<Item = Self> + 'a {
records records
.iter() .iter()
.map(move |&domain| Action::Reassign { domain, to, ttl }) .map(move |&domain| Action::Reassign { domain, to, ttl })
@ -91,7 +91,7 @@ fn update_ns_records<'a>(
) -> std::io::Result<()> { ) -> std::io::Result<()> {
writeln!(buf, "server 127.0.0.1")?; writeln!(buf, "server 127.0.0.1")?;
for action in actions { for action in actions {
write!(buf, "{action}")?; writeln!(buf, "{action}")?;
} }
writeln!(buf, "send")?; writeln!(buf, "send")?;
writeln!(buf, "quit") writeln!(buf, "quit")

View file

@ -4,7 +4,7 @@
//! records //! records
use std::io::Write; use std::io::Write;
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf; use std::path::Path;
use base64::prelude::*; use base64::prelude::*;
use miette::{Context, IntoDiagnostic, Result}; use miette::{Context, IntoDiagnostic, Result};
@ -20,18 +20,11 @@ pub struct Mkpasswd {
/// The password /// The password
password: String, password: String,
/// An application specific value
#[arg(long, default_value = crate::DEFAULT_SALT)]
salt: String,
/// The file to write the password to
password_file: Option<PathBuf>,
} }
impl Mkpasswd { impl Mkpasswd {
pub fn process(self, _args: &crate::Opts) -> Result<()> { pub fn process(self, args: &crate::Opts) -> Result<()> {
mkpasswd(self) mkpasswd(self, args.password_file.as_deref(), &args.salt)
} }
} }
@ -52,16 +45,13 @@ pub fn hash_identity(username: &str, password: &str, salt: &str) -> Digest {
} }
pub fn mkpasswd( pub fn mkpasswd(
Mkpasswd { Mkpasswd { username, password }: Mkpasswd,
username, password_file: Option<&Path>,
password, salt: &str,
salt,
password_file,
}: Mkpasswd,
) -> miette::Result<()> { ) -> miette::Result<()> {
let hash = hash_identity(&username, &password, &salt); let hash = hash_identity(&username, &password, salt);
let encoded = BASE64_URL_SAFE_NO_PAD.encode(hash.as_ref()); let encoded = BASE64_URL_SAFE_NO_PAD.encode(hash.as_ref());
let Some(path) = password_file.as_deref() else { let Some(path) = password_file else {
println!("{encoded}"); println!("{encoded}");
return Ok(()); return Ok(());
}; };

View file

@ -1,9 +1,52 @@
//! Deal with the DNS records //! Deal with the DNS records
use miette::{ensure, miette, LabeledSpan, Result}; use std::path::Path;
pub fn validate_record_str(record: &str) -> Result<()> { use miette::{ensure, miette, Context, IntoDiagnostic, LabeledSpan, NamedSource, Result};
validate_line(0, record).map_err(|err| err.with_source_code(String::from(record)))
/// Loads and verifies the records from a file
pub fn load(path: &Path) -> Result<()> {
let records = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read records from {}", path.display()))?;
verify(&records, path)?;
Ok(())
}
/// Load records without verifying them
pub fn load_no_verify(path: &Path) -> Result<&'static [&'static str]> {
let records = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read records from {}", path.display()))?;
if let Err(err) = verify(&records, path) {
tracing::error!("Failed to verify records: {err}");
}
// leak memory: we only do this here and it prevents a bunch of allocations
let records: &str = records.leak();
let records: Box<[&str]> = records.lines().collect();
Ok(Box::leak(records))
}
/// Verifies that a list of records is valid
pub fn verify(data: &str, path: &Path) -> Result<()> {
let mut offset = 0usize;
for line in data.lines() {
validate_line(offset, line).map_err(|err| {
err.with_source_code(NamedSource::new(
path.display().to_string(),
data.to_string(),
))
})?;
offset += line.len() + 1;
}
Ok(())
} }
fn validate_line(offset: usize, line: &str) -> Result<()> { fn validate_line(offset: usize, line: &str) -> Result<()> {
@ -113,7 +156,7 @@ fn validate_octet(offset: usize, octet: u8) -> Result<()> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::records::validate_record_str; use crate::records::verify;
macro_rules! assert_miette_snapshot { macro_rules! assert_miette_snapshot {
($diag:expr) => {{ ($diag:expr) => {{
@ -137,51 +180,104 @@ mod test {
#[test] #[test]
fn valid_records() -> miette::Result<()> { fn valid_records() -> miette::Result<()> {
for record in [ verify(
"example.com.", "\
"example.org.", example.com.\n\
"example.net.", example.org.\n\
"subdomain.example.com.", example.net.\n\
] { subdomain.example.com.\n\
validate_record_str(record)?; ",
} std::path::Path::new("test_records_valid"),
Ok(()) )
} }
#[test] #[test]
fn hostname_too_long() { fn hostname_too_long() {
let err = validate_record_str("example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net.").unwrap_err(); let err = verify(
"\
example.com.\n\
example.org.\n\
example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net.\n\
subdomain.example.com.\n\
",
std::path::Path::new("test_records_invalid"),
)
.unwrap_err();
assert_miette_snapshot!(err); assert_miette_snapshot!(err);
} }
#[test] #[test]
fn not_fqd() { fn not_fqd() {
let err = validate_record_str("example.net").unwrap_err(); let err = verify(
"\
example.com.\n\
example.org.\n\
example.net\n\
subdomain.example.com.\n\
",
std::path::Path::new("test_records_invalid"),
)
.unwrap_err();
assert_miette_snapshot!(err); assert_miette_snapshot!(err);
} }
#[test] #[test]
fn empty_label() { fn empty_label() {
let err = validate_record_str("name..example.org.").unwrap_err(); let err = verify(
"\
example.com.\n\
name..example.org.\n\
example.net.\n\
subdomain.example.com.\n\
",
std::path::Path::new("test_records_invalid"),
)
.unwrap_err();
assert_miette_snapshot!(err); assert_miette_snapshot!(err);
} }
#[test] #[test]
fn label_too_long() { fn label_too_long() {
let err = validate_record_str("name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.").unwrap_err(); let err = verify(
"\
example.com.\n\
name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.\n\
example.net.\n\
subdomain.example.com.\n\
",
std::path::Path::new("test_records_invalid"),
)
.unwrap_err();
assert_miette_snapshot!(err); assert_miette_snapshot!(err);
} }
#[test] #[test]
fn invalid_ascii() { fn invalid_ascii() {
let err = validate_record_str("name.this-is-not-ascii-ß.example.org.").unwrap_err(); let err = verify(
"\
example.com.\n\
name.this-is-not-ascii-ß.example.org.\n\
example.net.\n\
subdomain.example.com.\n\
",
std::path::Path::new("test_records_invalid"),
)
.unwrap_err();
assert_miette_snapshot!(err); assert_miette_snapshot!(err);
} }
#[test] #[test]
fn invalid_octet() { fn invalid_octet() {
let err = let err = verify(
validate_record_str("name.this-character:-is-not-allowed.example.org.").unwrap_err(); "\
example.com.\n\
name.this-character:-is-not-allowed.example.org.\n\
example.net.\n\
subdomain.example.com.\n\
",
std::path::Path::new("test_records_invalid"),
)
.unwrap_err();
assert_miette_snapshot!(err); assert_miette_snapshot!(err);
} }
} }

View file

@ -6,9 +6,11 @@ expression: out
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ ]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
× empty label × empty label
╭──── ╭─[test_records_invalid:2:6]
1 │ name..example.org. 1 │ example.com.
2 │ name..example.org.
· ▲ · ▲
· ╰── label · ╰── label
3 │ example.net.
╰──── ╰────
help: each label should have at least one character help: each label should have at least one character

View file

@ -6,9 +6,11 @@ expression: out
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ ]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
× hostname too long (260 octets) × hostname too long (260 octets)
╭──── ╭─[test_records_invalid:3:1]
1 │ example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net. 2 │ example.org.
3 │ example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net.
· ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── · ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
· ╰── this line · ╰── this line
4 │ subdomain.example.com.
╰──── ╰────
help: fully qualified domain names can be at most 255 characters long help: fully qualified domain names can be at most 255 characters long

View file

@ -6,9 +6,11 @@ expression: out
]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\ ]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\
× invalid octet: '\xc3' × invalid octet: '\xc3'
╭──── ╭─[test_records_invalid:2:24]
1 │ name.this-is-not-ascii-ß.example.org. 1 │ example.com.
2 │ name.this-is-not-ascii-ß.example.org.
· ┬ · ┬
· ╰── octet · ╰── octet
3 │ example.net.
╰──── ╰────
help: we only accept ascii characters help: we only accept ascii characters

View file

@ -6,9 +6,11 @@ expression: out
]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\ ]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\
× invalid octet: ':' × invalid octet: ':'
╭──── ╭─[test_records_invalid:2:20]
1 │ name.this-character:-is-not-allowed.example.org. 1 │ example.com.
2 │ name.this-character:-is-not-allowed.example.org.
· ┬ · ┬
· ╰── octet · ╰── octet
3 │ example.net.
╰──── ╰────
help: hostnames are only allowed to contain characters in [a-zA-Z0-9_-] help: hostnames are only allowed to contain characters in [a-zA-Z0-9_-]

View file

@ -6,9 +6,11 @@ expression: out
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ ]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
× label too long (78 octets) × label too long (78 octets)
╭──── ╭─[test_records_invalid:2:6]
1 │ name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org. 1 │ example.com.
2 │ name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.
· ───────────────────────────────────────┬────────────────────────────────────── · ───────────────────────────────────────┬──────────────────────────────────────
· ╰── label · ╰── label
3 │ example.net.
╰──── ╰────
help: labels should be at most 63 octets help: labels should be at most 63 octets

View file

@ -6,9 +6,11 @@ expression: out
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\ ]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
× not a fully qualified domain name × not a fully qualified domain name
╭──── ╭─[test_records_invalid:3:11]
1 │ example.net 2 │ example.org.
3 │ example.net
· ┬ · ┬
· ╰── last character · ╰── last character
4 │ subdomain.example.com.
╰──── ╰────
help: hostname should be a fully qualified domain name (end with a '.') help: hostname should be a fully qualified domain name (end with a '.')