Compare commits

...

93 commits
v0.3.6 ... main

Author SHA1 Message Date
0f4ff679d2
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 4s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-04-03 22:10:16 +02:00
e28cf9be3f
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-04-03 22:00:34 +02:00
1dac706aeb
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-04-02 22:00:44 +02:00
29916c0841 fix(deps): update rust crate axum-client-ip to v1
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 0s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
| datasource | package        | from  | to    |
| ---------- | -------------- | ----- | ----- |
| crate      | axum-client-ip | 0.7.0 | 1.0.0 |
2025-04-02 19:43:49 +02:00
3c9587bd4c
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-04-01 22:00:24 +02:00
7dcd2c6a4c
fix(deps): update rust crate clap to v4.5.35
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 3s
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | clap    | 4.5.34 | 4.5.35 |
2025-04-01 19:20:25 +02:00
5a98e8205f
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 4s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-03-31 22:00:32 +02:00
2183c81d70
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 1s
2025-03-30 22:00:20 +02:00
4ab2f709a9
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 4s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-03-29 22:00:33 +01:00
269a37c920
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 4s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-03-28 22:00:17 +01:00
6ac1133486
fix(deps): update rust crate axum to v0.8.3
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 0s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
| datasource | package | from  | to    |
| ---------- | ------- | ----- | ----- |
| crate      | axum    | 0.8.1 | 0.8.3 |
2025-03-28 21:50:30 +01:00
c5c51645ba
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-03-27 22:00:14 +01:00
fe58c295ff
fix(deps): update rust crate clap to v4.5.34
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 13s
/ report-size (push) Successful in 2s
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | clap    | 4.5.33 | 4.5.34 |
2025-03-27 03:10:24 +01:00
f356463079
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 4s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-03-26 22:00:30 +01:00
caa7c1165f
fix(deps): update rust crate clap to v4.5.33
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 0s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | clap    | 4.5.32 | 4.5.33 |
2025-03-26 20:20:14 +01:00
7dca0efdf2
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 4s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-03-24 22:00:31 +01:00
5f2ec1089f
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
2025-03-23 22:00:37 +01:00
ba2babb0ae
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ build (push) Successful in 2s
/ test (push) Successful in 11s
/ report-size (push) Successful in 7s
2025-03-21 22:00:34 +01:00
f9da4aecd4
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 2s
/ test (push) Successful in 11s
/ report-size (push) Successful in 7s
2025-03-20 22:00:15 +01:00
2d00dd3818
chore(deps): pin rust crate insta to =1.42.2
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 2s
/ test (push) Successful in 11s
/ report-size (push) Successful in 6s
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | insta   | 1.42.2 | 1.42.2 |
2025-03-19 23:20:34 +01:00
2daf620a4a
chore: pin nix-flake-outputs-size
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
Renovate seems to be erroring out when trying to do it.
2025-03-19 23:10:10 +01:00
316f2bf576
feat: add config file to webnsupdate
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ build (push) Successful in 1s
/ test (push) Successful in 12s
/ report-size (push) Successful in 2s
Move flags to config file, and add more options. Mirror some in the
module.
2025-03-19 23:05:38 +01:00
3d660314cf
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 2s
/ test (push) Successful in 12s
/ report-size (push) Successful in 7s
2025-03-19 22:00:49 +01:00
f207cbe859
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ build (push) Successful in 2s
/ test (push) Successful in 12s
/ report-size (push) Successful in 7s
2025-03-18 22:00:32 +01:00
c589fb40c3
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 2s
/ test (push) Successful in 12s
/ report-size (push) Successful in 6s
2025-03-16 22:00:43 +01:00
08ea3271c1
ci: use nix-fast-build to speedup checks
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ test (push) Successful in 11s
/ report-size (push) Successful in 2s
Should be a noticeable improvement c:
2025-03-16 19:27:05 +01:00
8787adae30
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ 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 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-03-14 22:00:31 +01:00
130c949723
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ 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 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-03-13 22:00:46 +01:00
e236aa424b
fix(deps): update rust crate tokio to v1.44.1
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ build (push) Successful in 0s
/ 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 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      | tokio   | 1.44.0 | 1.44.1 |
2025-03-13 09:40:24 +01:00
baeb98a2e6
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ 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-03-12 01:00:32 +01:00
9b41d7d2a5
fix(deps): update rust crate http to v1.3.1
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ 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 2s
| datasource | package | from  | to    |
| ---------- | ------- | ----- | ----- |
| crate      | http    | 1.3.0 | 1.3.1 |
2025-03-11 21:50:30 +01:00
33f8b1570d
fix(deps): update rust crate ring to v0.17.14
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ 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 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
| datasource | package | from    | to      |
| ---------- | ------- | ------- | ------- |
| crate      | ring    | 0.17.13 | 0.17.14 |
2025-03-11 21:20:24 +01:00
06995416d2
fix(deps): update rust crate http to v1.3.0
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ 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 2s
| datasource | package | from  | to    |
| ---------- | ------- | ----- | ----- |
| crate      | http    | 1.2.0 | 1.3.0 |
2025-03-11 18:00:26 +01:00
f8848e669e
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 3s
/ 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 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 1s
2025-03-10 22:00:53 +01:00
72843b8f52
fix(deps): update rust crate clap to v4.5.32
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ 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 6s
/ 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.31 | 4.5.32 |
2025-03-10 21:50:28 +01:00
4cdffc20bc chore(config): migrate config .renovaterc.json
All checks were successful
/ check-renovaterc (push) Successful in 3s
/ 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 8s
/ check (module-nginx-test) (push) Successful in 10s
/ check (nextest) (push) Successful in 4s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
2025-03-10 08:09:25 +01:00
cf66c77136
chore(deps): lock file maintenance
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ 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 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-03-09 22:00:32 +01:00
5fc53886f2
chore(deps): pin dependencies
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ build (push) Successful in 0s
/ 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 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 1s
2025-03-09 21:50:22 +01:00
881983dd6c ci: validate renovaterc
All checks were successful
/ check-renovaterc (push) Successful in 2s
/ 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
Also use config:best-practices
2025-03-09 21:38:51 +01:00
632250d544
fix(deps): update rust crate serde to v1.0.219
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 6s
/ 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   | 1.0.218 | 1.0.219 |
2025-03-09 20:20:28 +01:00
c71a8b418c
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 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 1s
2025-03-07 22:00:42 +01:00
873dd980ff
fix(deps): update rust crate tokio to v1.44.0
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 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      | tokio   | 1.43.0 | 1.44.0 |
2025-03-07 21:20:23 +01:00
3e1140ffe5
fix(deps): update rust crate ring to v0.17.13
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 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 1s
| datasource | package | from    | to      |
| ---------- | ------- | ------- | ------- |
| crate      | ring    | 0.17.12 | 0.17.13 |
2025-03-07 01:40:27 +01:00
6c78e8d78e
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-03-06 02:20:27 +01:00
3ed76c094c
fix(deps): update rust crate ring to v0.17.12
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 6s
/ 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      | ring    | 0.17.11 | 0.17.12 |
2025-03-06 02:10:27 +01:00
f16c3b9138
fix(typo): typos corrected typ to typo
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
This is wrong for us (we use typ instead of type, not typo, because type
is a rust keyword).
2025-03-04 09:46:38 +01:00
42482574ac
chore(deps): lock file maintenance
Some checks failed
/ 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 7s
/ check (module-ipv6-test) (push) Successful in 7s
/ check (module-nginx-test) (push) Successful in 6s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Failing after 3s
/ report-size (push) Successful in 2s
2025-03-03 22:00:50 +01:00
855963bc85
fix(deps): update rust crate serde_json to v1.0.140
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 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
| datasource | package    | from    | to      |
| ---------- | ---------- | ------- | ------- |
| crate      | serde_json | 1.0.139 | 1.0.140 |
2025-03-03 10:20:22 +01:00
7cc182b23e
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 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-03-02 02:10:30 +01:00
bc62fd7c1d
chore(deps): update rust crate insta to v1.42.2
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
| datasource | package | from   | to     |
| ---------- | ------- | ------ | ------ |
| crate      | insta   | 1.42.1 | 1.42.2 |
2025-03-02 02:00:39 +01:00
cdef8078cc
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-03-01 22:00:54 +01:00
a6f0785dc3
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 8s
/ check (module-ipv4-test) (push) Successful in 6s
/ 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 1s
2025-03-01 00:00:35 +01:00
5d4c0fdb70
ci(renovate): don't overlap schedules
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 7s
/ 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
Branch creation schedule and automerge schedule should not overlap, or
we'll get PRs that don't contain updates, which stop renovate from
creating further PRs.
2025-02-28 23:52:07 +01:00
2a2f9ef06c
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 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 1s
2025-02-27 23:00:49 +01:00
ef6e955b90
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-24 17:00:45 +01:00
b581e2adf1
fix(deps): update rust crate clap to v4.5.31
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 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      | clap    | 4.5.30 | 4.5.31 |
2025-02-24 16:50:22 +01:00
48034ec6e5
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 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
2025-02-23 23:00:42 +01:00
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
29f7315f67
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 7s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-02-06 23:00:24 +01:00
738fa8accf
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-05 23:30:28 +01:00
172076eaad
feat(webnsupdate): parse IPv6 prefixes
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 3s
/ report-size (push) Successful in 2s
This allows us to better support IPv6 from fritzbox updates in the
future.
2025-02-05 23:17:49 +01:00
b775f8e811
refactor(nsupdate): send all commands at once
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 3s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
This ensures `nsupdate` is only called once per IP update (even for both
IPv4 and IPv6 in a single call).
2025-02-05 22:47:13 +01:00
48c2e5be4d
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 2s
/ report-size (push) Successful in 2s
2025-02-04 23:00:40 +01:00
d56af9ecfe
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 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 1s
2025-02-04 00:00:46 +01:00
72aa4f365e
fix(deps): update rust crate clap to v4.5.28
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      | clap    | 4.5.27 | 4.5.28 |
2025-02-03 23:50:29 +01:00
d8630aa8cb
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-01 23:00:21 +01:00
5655d7de67
fix(deps): update rust crate miette to v7.5.0
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      | miette  | 7.4.0 | 7.5.0 |
2025-02-01 04:20:25 +01:00
d2f6c3cd66
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 6s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 2s
2025-01-31 22:00:20 +01:00
13c9c544a7
fix(webnsupdate): updating IPv6 in ipv4-only mode
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 6s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 2s
/ report-size (push) Successful in 1s
Dumb logic error T-T.
2025-01-31 21:46:27 +01:00
70ed898f1d
fix(tests): add case for when query has empty string
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 1s
This is produced by the FRITZ!Box DDNS client.
2025-01-31 21:25:41 +01:00
2f97008475
fix(webnsupdate): make IP none when query is empty
This happens when one of the IPs hasn't changed (but the other has) in
the FRITZ!Box DDNS client.
2025-01-31 21:25:41 +01:00
09bd450a46
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 1s
/ check (module-ipv4-only-test) (push) Successful in 1s
/ check (module-ipv4-test) (push) Successful in 1s
/ check (module-ipv6-only-test) (push) Successful in 1s
/ check (module-ipv6-test) (push) Successful in 1s
/ check (module-nginx-test) (push) Successful in 1s
/ check (nextest) (push) Successful in 1s
/ check (treefmt) (push) Successful in 0s
/ report-size (push) Successful in 1s
2025-01-29 23:00:44 +01:00
a6bb8bf817
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 6s
/ 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
2025-01-29 00:00:30 +01:00
a39fa354e4
feat(webnsupdate): add support for fritzbox style updates
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 10s
/ check (module-ipv6-test) (push) Successful in 8s
/ check (module-nginx-test) (push) Successful in 10s
/ check (nextest) (push) Successful in 2s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 2s
Update now optionally accepts query params (e.g. `/update?ipv4=1.2.3.4`)
if present they will control how the update is made (only ipv4 and ipv6
are implemented right now).

Closes #80
2025-01-28 23:56:52 +01:00
26566fd612
fix(deps): update rust crate serde_json to v1.0.138
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-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 1s
| datasource | package    | from    | to      |
| ---------- | ---------- | ------- | ------- |
| crate      | serde_json | 1.0.137 | 1.0.138 |
2025-01-28 19:00:24 +01:00
09be5627c3
refactor(module): NixOS tests
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-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
Way easier to extend/maintain.
2025-01-28 00:31:32 +01:00
dff29cab77
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1s
/ check (clippy) (push) Successful in 2s
/ check (module-ipv4-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 1s
2025-01-26 23:00:37 +01:00
20 changed files with 1312 additions and 984 deletions

View file

@ -1,41 +1,39 @@
on: [push]
jobs:
check-renovaterc:
runs-on: nixos
steps:
- uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Validate renovaterc
run: |
nix --version
nix shell nixpkgs#renovate --command renovate-config-validator
build:
runs-on: nixos
steps:
- uses: https://git.salame.cl/actions/checkout@v4
- uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Build Package
run: |
nix --version
nix build --print-build-logs .#
check:
test:
needs: build # we use the built binaries in the checks
runs-on: nixos
strategy:
matrix:
check:
- treefmt
- clippy
- nextest
- module-ipv4-test
- module-ipv6-test
- module-nginx-test
steps:
- uses: https://git.salame.cl/actions/checkout@v4
- name: Check
- uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Run tests
run: |
set -x
nix --version
# shellcheck disable=SC2016
nix build --print-build-logs '.#checks.x86_64-linux.${{ matrix.check }}'
nix-fast-build --max-jobs 2 --no-nom --skip-cached --no-link \
--flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)"
report-size:
runs-on: nixos
needs: build
steps:
- uses: https://git.salame.cl/actions/checkout@v4
- uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- run: nix --version
- name: Generate size report
uses: https://git.salame.cl/jalil/nix-flake-outputs-size@main
uses: "https://git.salame.cl/jalil/nix-flake-outputs-size@5c40a31e3e2ed0ea28f8ba68deca41d05fdf2e71" # main
with:
comment-on-pr: ${{ github.ref_name != 'main' }}
generate-artifact: ${{ github.ref_name == 'main' }}

View file

@ -9,7 +9,7 @@
"commitBodyTable": true,
"dependencyDashboard": true,
"extends": [
"config:recommended"
"config:best-practices"
],
"prCreation": "immediate",
"cargo": {
@ -31,14 +31,14 @@
"lockFileMaintenance": {
"enabled": true,
"recreateWhen": "always",
"rebaseStalePrs": true,
"rebaseWhen": "behind-base-branch",
"branchTopic": "lock-file-maintenance",
"commitMessageAction": "Lock file maintenance",
"schedule": [
"* 23 * * *"
"* 22 * * *"
]
},
"automergeSchedule": [
"* 0-1 * * *"
"* 23 * * *"
]
}

234
Cargo.lock generated
View file

@ -78,9 +78,9 @@ dependencies = [
[[package]]
name = "axum"
version = "0.8.1"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
dependencies = [
"axum-core",
"bytes",
@ -112,9 +112,9 @@ dependencies = [
[[package]]
name = "axum-client-ip"
version = "0.7.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff8ee1869817523c8f91c20bf17fd932707f66c2e7e0b0f811b29a227289562"
checksum = "b9329923fe6c30624095e63cb6c25796b32ffbf5d1da8c3a95d1054c301db92a"
dependencies = [
"axum",
"forwarded-header-value",
@ -123,12 +123,12 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
"bytes",
"futures-util",
"futures-core",
"http",
"http-body",
"http-body-util",
@ -173,21 +173,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bytes"
version = "1.9.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.10"
version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [
"shlex",
]
@ -200,9 +200,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.27"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
dependencies = [
"clap_builder",
"clap_derive",
@ -220,9 +220,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.27"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
dependencies = [
"anstream",
"anstyle",
@ -232,9 +232,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.24"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
@ -256,9 +256,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "console"
version = "0.15.10"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
@ -304,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
dependencies = [
"nonempty",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@ -365,9 +365,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.2.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
@ -386,12 +386,12 @@ dependencies = [
[[package]]
name = "http-body-util"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-util",
"futures-core",
"http",
"http-body",
"pin-project-lite",
@ -399,9 +399,9 @@ dependencies = [
[[package]]
name = "httparse"
version = "1.9.5"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
@ -410,10 +410,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.5.2"
name = "humantime"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
[[package]]
name = "hyper"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
@ -430,9 +436,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
dependencies = [
"bytes",
"futures-util",
@ -446,14 +452,15 @@ dependencies = [
[[package]]
name = "insta"
version = "1.42.1"
version = "1.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71c1b125e30d93896b365e156c33dadfffab45ee8400afcbba4752f59de08a86"
checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084"
dependencies = [
"console",
"linked-hash-map",
"once_cell",
"pin-project",
"serde",
"similar",
]
@ -471,9 +478,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "lazy_static"
@ -483,9 +490,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "linked-hash-map"
@ -495,15 +502,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
[[package]]
name = "log"
version = "0.4.25"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "matchers"
@ -528,9 +535,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miette"
version = "7.4.0"
version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64"
checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484"
dependencies = [
"backtrace",
"backtrace-ext",
@ -542,15 +549,15 @@ dependencies = [
"supports-unicode",
"terminal_size",
"textwrap",
"thiserror",
"unicode-width",
"thiserror 1.0.69",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.4.0"
version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67"
checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147"
dependencies = [
"proc-macro2",
"quote",
@ -565,9 +572,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.8.3"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430"
dependencies = [
"adler2",
]
@ -610,9 +617,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.20.2"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "overload"
@ -622,9 +629,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "4.1.0"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564"
[[package]]
name = "percent-encoding"
@ -634,18 +641,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.8"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.8"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
@ -666,18 +673,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.93"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
@ -728,15 +735,14 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.8"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
@ -749,9 +755,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.44"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
dependencies = [
"bitflags",
"errno",
@ -762,30 +768,30 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.19"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.18"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
@ -794,9 +800,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.137"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@ -806,9 +812,9 @@ dependencies = [
[[package]]
name = "serde_path_to_error"
version = "0.1.16"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
dependencies = [
"itoa",
"serde",
@ -858,26 +864,20 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "smallvec"
version = "1.13.2"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "socket2"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"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]]
name = "strsim"
version = "0.11.1"
@ -907,9 +907,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.96"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
@ -924,9 +924,9 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "terminal_size"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
"rustix",
"windows-sys 0.59.0",
@ -934,12 +934,12 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.16.1"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"unicode-linebreak",
"unicode-width",
"unicode-width 0.2.0",
]
[[package]]
@ -948,7 +948,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[package]]
@ -962,6 +971,17 @@ dependencies = [
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.8"
@ -974,9 +994,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.43.0"
version = "1.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
dependencies = [
"backtrace",
"bytes",
@ -1107,9 +1127,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.15"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-linebreak"
@ -1123,6 +1143,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -1157,11 +1183,13 @@ dependencies = [
"clap",
"clap-verbosity-flag",
"http",
"humantime",
"insta",
"miette",
"ring",
"serde",
"serde_json",
"thiserror 2.0.12",
"tokio",
"tower-http",
"tracing",

View file

@ -1,5 +1,3 @@
cargo-features = ["codegen-backend"]
[package]
description = "An HTTP server using HTTP basic auth to make secure calls to nsupdate"
name = "webnsupdate"
@ -18,24 +16,26 @@ pedantic = { level = "warn", priority = -1 }
[dependencies]
axum = "0.8"
axum-client-ip = "0.7"
axum-client-ip = "1.0"
base64 = "0.22"
clap = { version = "4", features = ["derive", "env"] }
clap-verbosity-flag = { version = "3", default-features = false, features = [
"tracing",
] }
http = "1"
humantime = "2.2.0"
miette = { version = "7", features = ["fancy"] }
ring = { version = "0.17", features = ["std"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.137"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] }
tower-http = { version = "0.6.2", features = ["validate-request"] }
tower-http = { version = "0.6", features = ["validate-request"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
insta = "1"
insta = { version = "=1.42.2", features = ["json"] }
[profile.release]
opt-level = "s"
@ -46,4 +46,3 @@ codegen-units = 1
[profile.dev]
debug = 0
codegen-backend = "cranelift"

View file

@ -3,10 +3,18 @@
imports = [
inputs.treefmt-nix.flakeModule
./package.nix
./module.nix
./tests.nix
];
flake.nixosModules =
let
webnsupdate = ../module.nix;
in
{
default = webnsupdate;
inherit webnsupdate;
};
perSystem =
{ pkgs, ... }:
{

View file

@ -1,208 +0,0 @@
let
module =
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.webnsupdate;
inherit (lib)
mkOption
mkEnableOption
mkPackageOption
types
;
in
{
options.services.webnsupdate = mkOption {
description = "An HTTP server for nsupdate.";
default = { };
type = types.submodule {
options = {
enable = mkEnableOption "webnsupdate";
extraArgs = mkOption {
description = ''
Extra arguments to be passed to the webnsupdate server command.
'';
type = types.listOf types.str;
default = [ ];
example = [ "--ip-source" ];
};
package = mkPackageOption pkgs "webnsupdate" { };
bindIp = mkOption {
description = ''
IP address to bind to.
Setting it to anything other than localhost is very insecure as
`webnsupdate` only supports plain HTTP and should always be behind a
reverse proxy.
'';
type = types.str;
default = "localhost";
example = "0.0.0.0";
};
bindPort = mkOption {
description = "Port to bind to.";
type = types.port;
default = 5353;
};
allowedIPVersion = mkOption {
description = ''The allowed IP versions to accept updates from.'';
type = types.enum [
"both"
"ipv4-only"
"ipv6-only"
];
default = "both";
example = "ipv4-only";
};
passwordFile = mkOption {
description = ''
The file where the password is stored.
This file can be created by running `webnsupdate mkpasswd $USERNAME $PASSWORD`.
'';
type = types.path;
example = "/secrets/webnsupdate.pass";
};
keyFile = mkOption {
description = ''
The TSIG key that `nsupdate` should use.
This file will be passed to `nsupdate` through the `-k` option, so look
at `man 8 nsupdate` for information on the key's format.
'';
type = types.path;
example = "/secrets/webnsupdate.key";
};
ttl = mkOption {
description = "The TTL that should be set on the zone records created by `nsupdate`.";
type = types.ints.positive;
default = 60;
example = 3600;
};
records = mkOption {
description = ''
The fqdn of records that should be updated.
Empty lines will be ignored, but whitespace will not be.
'';
type = types.nullOr types.lines;
default = null;
example = ''
example.com.
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 {
description = "The user to run as.";
type = types.str;
default = "named";
};
group = mkOption {
description = "The group to run as.";
type = types.str;
default = "named";
};
};
};
};
config =
let
recordsFile =
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}";
in
lib.mkIf cfg.enable {
# warnings =
# 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 = {
description = "Web interface for nsupdate.";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"bind.service"
];
preStart = "${cmd} verify";
path = [ pkgs.dig ];
startLimitIntervalSec = 60;
serviceConfig = {
ExecStart = [ cmd ];
Type = "exec";
Restart = "on-failure";
RestartSec = "10s";
# User and group
User = cfg.user;
Group = cfg.group;
# Runtime directory and mode
RuntimeDirectory = "webnsupdate";
RuntimeDirectoryMode = "0750";
# Cache directory and mode
CacheDirectory = "webnsupdate";
CacheDirectoryMode = "0750";
# Logs directory and mode
LogsDirectory = "webnsupdate";
LogsDirectoryMode = "0750";
# State directory and mode
StateDirectory = "webnsupdate";
StateDirectoryMode = "0750";
# New file permissions
UMask = "0027";
# Security
NoNewPrivileges = true;
ProtectHome = true;
};
};
};
};
in
{
flake.nixosModules = {
default = module;
webnsupdate = module;
};
}

View file

@ -9,7 +9,7 @@
lastIPPath = "/var/lib/webnsupdate/last-ip.json";
zoneFile = pkgs.writeText "${testDomain}.zoneinfo" ''
$TTL 60 ; 1 minute
$TTL 600 ; 10 minutes
$ORIGIN ${testDomain}.
@ IN SOA ns1.${testDomain}. admin.${testDomain}. (
1 ; serial
@ -73,20 +73,19 @@
webnsupdate = {
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;
extraArgs = [
"-vvv" # debug messages
"--ip-source=ConnectInfo"
];
records = ''
test1.${testDomain}.
test2.${testDomain}.
test3.${testDomain}.
'';
extraArgs = [ "-vvv" ]; # debug messages
settings = {
address = lib.mkDefault "127.0.0.1:5353";
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}."
];
};
};
};
};
@ -97,7 +96,7 @@
webnsupdate-ipv4-machine
];
config.services.webnsupdate.bindIp = "::1";
config.services.webnsupdate.settings.address = "[::1]:5353";
};
webnsupdate-nginx-machine =
@ -109,342 +108,235 @@
config.services = {
# Use default IP Source
webnsupdate.extraArgs = lib.mkForce [ "-vvv" ]; # debug messages
webnsupdate.settings.ip_source = "RightmostXForwardedFor";
nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts.webnsupdate.locations."/".proxyPass =
"http://${config.services.webnsupdate.bindIp}:${builtins.toString config.services.webnsupdate.bindPort}";
"http://${config.services.webnsupdate.settings.address}";
};
};
};
webnsupdate-ipv4-only-machine = {
imports = [ webnsupdate-nginx-machine ];
config.services.webnsupdate.allowedIPVersion = "ipv4-only";
config.services.webnsupdate.settings.ip_type = "Ipv4Only";
};
webnsupdate-ipv6-only-machine = {
imports = [ webnsupdate-nginx-machine ];
config.services.webnsupdate.allowedIPVersion = "ipv6-only";
config.services.webnsupdate.settings.ip_type = "Ipv6Only";
};
testScript = ''
machine.start(allow_reboot=True)
machine.wait_for_unit("bind.service")
machine.wait_for_unit("webnsupdate.service")
# "A" for IPv4, "AAAA" for IPv6, "ANY" for any
testTemplate =
{
ipv4 ? false,
ipv6 ? false,
nginx ? false,
exclusive ? false,
}:
if exclusive && (ipv4 == ipv6) then
builtins.throw "exclusive means one of ipv4 or ipv6 must be set, but not both"
else
''
IPV4: bool = ${if ipv4 then "True" else "False"}
IPV6: bool = ${if ipv6 then "True" else "False"}
NGINX: bool = ${if nginx then "True" else "False"}
EXCLUSIVE: bool = ${if exclusive then "True" else "False"}
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
# ensure base DNS records area available
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
CURL: str = "curl --fail --no-progress-meter --show-error"
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A test3.${testDomain} AAAA | grep ^test3.${testDomain}")
machine.start(allow_reboot=True)
machine.wait_for_unit("bind.service")
machine.wait_for_unit("webnsupdate.service")
with subtest("update webnsupdate DNS records (invalid auth)"):
machine.fail("curl --fail --silent -u test1:test1 -X GET http://localhost:5353/update")
machine.fail("cat ${lastIPPath}") # no last-ip set yet
STATIC_DOMAINS: list[str] = ["${testDomain}", "ns1.${testDomain}", "nsupdate.${testDomain}"]
DYNAMIC_DOMAINS: list[str] = ["test1.${testDomain}", "test2.${testDomain}", "test3.${testDomain}"]
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A test3.${testDomain} AAAA | grep ^test3.${testDomain}")
def dig_cmd(domain: str, record: str, ip: str | None) -> tuple[str, str]:
match_ip = "" if ip is None else f"\\s\\+600\\s\\+IN\\s\\+{record}\\s\\+{ip}$"
return f"dig @localhost {record} {domain} +noall +answer", f"grep '^{domain}.{match_ip}'"
with subtest("update webnsupdate DNS records (valid auth)"):
machine.succeed("curl --fail --silent -u test:test -X GET http://localhost:5353/update")
def curl_cmd(domain: str, identity: str, path: str, query: dict[str, str]) -> str:
from urllib.parse import urlencode
q= f"?{urlencode(query)}" if query else ""
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):
dig, grep = 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):
dig, grep = 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):
machine.succeed(curl_cmd(domain, "test:test", path, kwargs))
machine.succeed("cat ${lastIPPath}")
def update_records_fail(domain: str="localhost", /, *, identity: str="test:test", path: str="update", **kwargs):
machine.fail(curl_cmd(domain, identity, path, kwargs))
machine.fail("cat ${lastIPPath}")
def invalid_update(domain: str="localhost"):
update_records_fail(domain, identity="bad_user:test")
update_records_fail(domain, identity="test:bad_pass")
# Tests
with subtest("static DNS records are available"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
for domain in STATIC_DOMAINS:
domain_available(domain, "A", "127.0.0.1") # IPv4
domain_available(domain, "AAAA", "::1") # IPv6
with subtest("dynamic DNS records are missing"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
for domain in DYNAMIC_DOMAINS:
domain_missing(domain, "A") # IPv4
domain_missing(domain, "AAAA") # IPv6
with subtest("invalid auth fails to update records"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
invalid_update()
for domain in DYNAMIC_DOMAINS:
domain_missing(domain, "A") # IPv4
domain_missing(domain, "AAAA") # IPv6
if EXCLUSIVE:
with subtest("exclusive IP version fails to update with invalid version"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
if IPV6:
update_records_fail("127.0.0.1")
if IPV4:
update_records_fail("[::1]")
with subtest("valid auth updates records"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
if IPV4:
update_records("127.0.0.1")
if IPV6:
update_records("[::1]")
for domain in DYNAMIC_DOMAINS:
if IPV4:
domain_available(domain, "A", "127.0.0.1")
elif IPV6 and EXCLUSIVE:
domain_missing(domain, "A")
if IPV6:
domain_available(domain, "AAAA", "::1")
elif IPV4 and EXCLUSIVE:
domain_missing(domain, "AAAA")
with subtest("valid auth fritzbox compatible updates records"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
if IPV4 and IPV6:
update_records("127.0.0.1", domain="test", ipv4="1.2.3.4", ipv6="::1234")
elif IPV4:
update_records("127.0.0.1", ipv4="1.2.3.4", ipv6="")
elif IPV6:
update_records("[::1]", ipv4="", ipv6="::1234")
for domain in DYNAMIC_DOMAINS:
if IPV4:
domain_available(domain, "A", "1.2.3.4")
elif IPV6 and EXCLUSIVE:
domain_missing(domain, "A")
if IPV6:
domain_available(domain, "AAAA", "::1234")
elif IPV4 and EXCLUSIVE:
domain_missing(domain, "AAAA")
with subtest("valid auth replaces records"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
if IPV4:
update_records("127.0.0.1")
if IPV6:
update_records("[::1]")
for domain in DYNAMIC_DOMAINS:
if IPV4:
domain_available(domain, "A", "127.0.0.1")
elif IPV6 and EXCLUSIVE:
domain_missing(domain, "A")
if IPV6:
domain_available(domain, "AAAA", "::1")
elif IPV4 and EXCLUSIVE:
domain_missing(domain, "AAAA")
machine.reboot()
machine.succeed("cat ${lastIPPath}")
machine.wait_for_unit("webnsupdate.service")
machine.succeed("cat ${lastIPPath}")
# ensure webnsupdate managed records are available
with subtest("query webnsupdate DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} A test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} A test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} A test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("static DNS records are available after reboot"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
for domain in STATIC_DOMAINS:
domain_available(domain, "A", "127.0.0.1") # IPv4
domain_available(domain, "AAAA", "::1") # IPv6
machine.reboot()
machine.succeed("cat ${lastIPPath}")
machine.wait_for_unit("webnsupdate.service")
machine.succeed("cat ${lastIPPath}")
with subtest("dynamic DNS records are available after reboot"):
print(f"{IPV4=} {IPV6=} {EXCLUSIVE=}")
for domain in DYNAMIC_DOMAINS:
if IPV4:
domain_available(domain, "A", "127.0.0.1")
elif IPV6 and EXCLUSIVE:
domain_missing(domain, "A")
# ensure base DNS records area available after a reboot
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are available after a reboot
with subtest("query webnsupdate DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} A test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} A test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} A test3.${testDomain} AAAA | grep ^test3.${testDomain}")
'';
if IPV6:
domain_available(domain, "AAAA", "::1")
elif IPV4 and EXCLUSIVE:
domain_missing(domain, "AAAA")
'';
in
{
module-ipv4-test = pkgs.testers.runNixOSTest {
module-ipv4-test = pkgs.testers.nixosTest {
name = "webnsupdate-ipv4-module";
nodes.machine = webnsupdate-ipv4-machine;
inherit testScript;
testScript = testTemplate { ipv4 = true; };
};
module-ipv6-test = pkgs.testers.runNixOSTest {
module-ipv6-test = pkgs.testers.nixosTest {
name = "webnsupdate-ipv6-module";
nodes.machine = webnsupdate-ipv6-machine;
inherit testScript;
testScript = testTemplate { ipv6 = true; };
};
module-nginx-test = pkgs.testers.runNixOSTest {
module-nginx-test = pkgs.testers.nixosTest {
name = "webnsupdate-nginx-module";
nodes.machine = webnsupdate-nginx-machine;
testScript = ''
machine.start(allow_reboot=True)
machine.wait_for_unit("bind.service")
machine.wait_for_unit("webnsupdate.service")
# ensure base DNS records area available
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate DNS records (invalid auth)"):
machine.fail("curl --fail --silent -u test1:test1 -X GET http://127.0.0.1/update")
machine.fail("cat ${lastIPPath}") # no last-ip set yet
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate IPv4 DNS records (valid auth)"):
machine.succeed("curl --fail --silent -u test:test -X GET http://127.0.0.1/update")
machine.succeed("cat ${lastIPPath}")
# ensure webnsupdate managed IPv4 records are available
with subtest("query webnsupdate IPv4 DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
# ensure webnsupdate managed IPv6 records are missing
with subtest("query webnsupdate IPv6 DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate IPv6 DNS records (valid auth)"):
machine.succeed("curl --fail --silent -u test:test -X GET http://[::1]/update")
machine.succeed("cat ${lastIPPath}")
# ensure webnsupdate managed IPv6 records are missing
with subtest("query webnsupdate IPv6 DNS records (fail)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
machine.reboot()
machine.succeed("cat ${lastIPPath}")
machine.wait_for_unit("webnsupdate.service")
machine.succeed("cat ${lastIPPath}")
# ensure base DNS records area available after a reboot
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are available after a reboot
with subtest("query webnsupdate DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.succeed("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
'';
testScript = testTemplate {
ipv4 = true;
ipv6 = true;
nginx = true;
};
};
module-ipv4-only-test = pkgs.testers.runNixOSTest {
module-ipv4-only-test = pkgs.testers.nixosTest {
name = "webnsupdate-ipv4-only-module";
nodes.machine = webnsupdate-ipv4-only-machine;
testScript = ''
machine.start(allow_reboot=True)
machine.wait_for_unit("bind.service")
machine.wait_for_unit("webnsupdate.service")
# ensure base DNS records area available
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate DNS records (invalid auth)"):
machine.fail("curl --fail --silent -u test1:test1 -X GET http://127.0.0.1/update")
machine.fail("cat ${lastIPPath}") # no last-ip set yet
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate IPv6 DNS records (valid auth)"):
machine.fail("curl --fail --silent -u test:test -X GET http://[::1]/update")
machine.fail("cat ${lastIPPath}")
# ensure webnsupdate managed IPv6 records are missing
with subtest("query webnsupdate IPv6 DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate IPv4 DNS records (valid auth)"):
machine.succeed("curl --fail --silent -u test:test -X GET http://127.0.0.1/update")
machine.succeed("cat ${lastIPPath}")
# ensure webnsupdate managed IPv4 records are available
with subtest("query webnsupdate IPv4 DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
# ensure webnsupdate managed IPv6 records are missing
with subtest("query webnsupdate IPv6 DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
machine.reboot()
machine.succeed("cat ${lastIPPath}")
machine.wait_for_unit("webnsupdate.service")
machine.succeed("cat ${lastIPPath}")
# ensure base DNS records area available after a reboot
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are available after a reboot
with subtest("query webnsupdate DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
'';
testScript = testTemplate {
ipv4 = true;
nginx = true;
exclusive = true;
};
};
module-ipv6-only-test = pkgs.testers.runNixOSTest {
module-ipv6-only-test = pkgs.testers.nixosTest {
name = "webnsupdate-ipv6-only-module";
nodes.machine = webnsupdate-ipv6-only-machine;
testScript = ''
machine.start(allow_reboot=True)
machine.wait_for_unit("bind.service")
machine.wait_for_unit("webnsupdate.service")
# ensure base DNS records area available
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate DNS records (invalid auth)"):
machine.fail("curl --fail --silent -u test1:test1 -X GET http://127.0.0.1/update")
machine.fail("cat ${lastIPPath}") # no last-ip set yet
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
with subtest("update webnsupdate IPv4 DNS records (valid auth)"):
machine.fail("curl --fail --silent -u test:test -X GET http://127.0.0.1/update")
machine.fail("cat ${lastIPPath}")
# ensure webnsupdate managed IPv4 records are missing
with subtest("query webnsupdate IPv4 DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
with subtest("update webnsupdate IPv6 DNS records (valid auth)"):
machine.succeed("curl --fail --silent -u test:test -X GET http://[::1]/update")
machine.succeed("cat ${lastIPPath}")
# ensure webnsupdate managed IPv6 records are available
with subtest("query webnsupdate IPv6 DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
# ensure webnsupdate managed IPv4 records are missing
with subtest("query webnsupdate IPv4 DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
machine.reboot()
machine.succeed("cat ${lastIPPath}")
machine.wait_for_unit("webnsupdate.service")
machine.succeed("cat ${lastIPPath}")
# ensure base DNS records area available after a reboot
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are available after a reboot
with subtest("query webnsupdate DNS records (succeed)"):
machine.succeed("dig @127.0.0.1 test1.${testDomain} AAAA | grep ^test1.${testDomain}")
machine.succeed("dig @127.0.0.1 test2.${testDomain} AAAA | grep ^test2.${testDomain}")
machine.succeed("dig @127.0.0.1 test3.${testDomain} AAAA | grep ^test3.${testDomain}")
machine.fail("dig @127.0.0.1 test1.${testDomain} A | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} A | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} A | grep ^test3.${testDomain}")
'';
testScript = testTemplate {
ipv6 = true;
nginx = true;
exclusive = true;
};
};
};
};

24
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"crane": {
"locked": {
"lastModified": 1737689766,
"narHash": "sha256-ivVXYaYlShxYoKfSo5+y5930qMKKJ8CLcAoIBPQfJ6s=",
"lastModified": 1743700120,
"narHash": "sha256-8BjG/P0xnuCyVOXlYRwdI1B8nVtyYLf3oDwPSimqREY=",
"owner": "ipetkov",
"repo": "crane",
"rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
"rev": "e316f19ee058e6db50075115783be57ac549c389",
"type": "github"
},
"original": {
@ -22,11 +22,11 @@
]
},
"locked": {
"lastModified": 1736143030,
"narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
@ -37,11 +37,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1737632463,
"narHash": "sha256-38J9QfeGSej341ouwzqf77WIHAScihAKCt8PQJ+NH28=",
"lastModified": 1743583204,
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0aa475546ed21629c4f5bbf90e38c846a99ec9e9",
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
"type": "github"
},
"original": {
@ -82,11 +82,11 @@
]
},
"locked": {
"lastModified": 1737483750,
"narHash": "sha256-5An1wq5U8sNycOBBg3nsDDgpwBmR9liOpDGlhliA6Xo=",
"lastModified": 1743677901,
"narHash": "sha256-eWZln+k+L/VHO69tUTzEmgeDWNQNKIpSUa9nqQgBrSE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "f2cc121df15418d028a59c9737d38e3a90fbaf8f",
"rev": "57dabe2a6255bd6165b2437ff6c2d1f6ee78421a",
"type": "github"
},
"original": {

162
module.nix Normal file
View file

@ -0,0 +1,162 @@
{ lib, pkgs, ... }@args:
let
cfg = args.config.services.webnsupdate;
inherit (lib)
mkOption
mkEnableOption
mkPackageOption
types
;
format = pkgs.formats.json { };
in
{
options.services.webnsupdate = mkOption {
description = "An HTTP server for nsupdate.";
default = { };
type = types.submodule {
options = {
enable = mkEnableOption "webnsupdate";
extraArgs = mkOption {
description = ''
Extra arguments to be passed to the webnsupdate server command.
'';
type = types.listOf types.str;
default = [ ];
example = [ "--ip-source" ];
};
package = mkPackageOption pkgs "webnsupdate" { };
settings = mkOption {
description = "The webnsupdate JSON configuration";
default = { };
type = types.submodule {
freeformType = format.type;
options = {
address = mkOption {
description = ''
IP address and port to bind to.
Setting it to anything other than localhost is very
insecure as `webnsupdate` only supports plain HTTP and
should always be behind a reverse proxy.
'';
type = types.str;
default = "127.0.0.1:5353";
example = "[::1]:5353";
};
ip_type = mkOption {
description = ''The allowed IP versions to accept updates from.'';
type = types.enum [
"Both"
"Ipv4Only"
"Ipv6Only"
];
default = "Both";
example = "Ipv4Only";
};
password_file = mkOption {
description = ''
The file where the password is stored.
This file can be created by running `webnsupdate mkpasswd $USERNAME $PASSWORD`.
'';
type = types.path;
example = "/secrets/webnsupdate.pass";
};
key_file = mkOption {
description = ''
The TSIG key that `nsupdate` should use.
This file will be passed to `nsupdate` through the `-k` option, so look
at `man 8 nsupdate` for information on the key's format.
'';
type = types.path;
example = "/secrets/webnsupdate.key";
};
ttl = mkOption {
description = "The TTL that should be set on the zone records created by `nsupdate`.";
default = "10m";
example = "60s";
type = types.str;
};
records = mkOption {
description = ''
The fqdn of records that should be updated.
Empty lines will be ignored, but whitespace will not be.
'';
type = types.listOf types.str;
default = [ ];
example = [
"example.com."
"example.org."
"ci.example.org."
];
};
};
};
};
user = mkOption {
description = "The user to run as.";
type = types.str;
default = "named";
};
group = mkOption {
description = "The group to run as.";
type = types.str;
default = "named";
};
};
};
};
config =
let
configFile = format.generate "webnsupdate.json" cfg.settings;
args = lib.strings.escapeShellArgs ([ "--config=${configFile}" ] ++ cfg.extraArgs);
cmd = "${lib.getExe cfg.package} ${args}";
in
lib.mkIf cfg.enable {
# FIXME: re-enable once I stop using the patched version of bind
# warnings =
# lib.optional (!config.services.bind.enable) "`webnsupdate` is expected to be used alongside `bind`. This is an unsupported configuration.";
systemd.services.webnsupdate = {
description = "Web interface for nsupdate.";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"bind.service"
];
preStart = "${lib.getExe cfg.package} verify ${configFile}";
path = [ pkgs.dig ];
startLimitIntervalSec = 60;
environment.DATA_DIR = "%S/webnsupdate";
serviceConfig = {
ExecStart = [ cmd ];
Type = "exec";
Restart = "on-failure";
RestartSec = "10s";
# User and group
User = cfg.user;
Group = cfg.group;
# Runtime directory and mode
RuntimeDirectory = "webnsupdate";
RuntimeDirectoryMode = "0750";
# Cache directory and mode
CacheDirectory = "webnsupdate";
CacheDirectoryMode = "0750";
# Logs directory and mode
LogsDirectory = "webnsupdate";
LogsDirectoryMode = "0750";
# State directory and mode
StateDirectory = "webnsupdate";
StateDirectoryMode = "0750";
# New file permissions
UMask = "0027";
# Security
NoNewPrivileges = true;
ProtectHome = true;
};
};
};
}

253
src/config.rs Normal file
View file

@ -0,0 +1,253 @@
use std::{
fs::File,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
};
use axum_client_ip::ClientIpSource;
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",
serialize_with = "humantime_ser",
deserialize_with = "humantime_de"
)]
pub ttl: humantime::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.ClientIpSource.html>
#[serde(default = "default_ip_source")]
pub ip_source: ClientIpSource,
/// 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,
/// The config schema (used for lsp completions)
#[serde(default, rename = "$schema", skip_serializing)]
pub _schema: serde::de::IgnoredAny,
}
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() -> humantime::Duration {
super::DEFAULT_TTL.into()
}
fn default_salt() -> Box<str> {
super::DEFAULT_SALT.into()
}
fn default_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5353)
}
fn default_ip_source() -> ClientIpSource {
ClientIpSource::RightmostXForwardedFor
}
fn default_ip_type() -> IpType {
IpType::Both
}
fn humantime_de<'de, D>(de: D) -> Result<humantime::Duration, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = humantime::Duration;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a duration (e.g. 5s)")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse().map_err(E::custom)
}
}
de.deserialize_str(Visitor)
}
fn humantime_ser<S>(duration: &humantime::Duration, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(&duration.to_string())
}
#[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

@ -5,17 +5,23 @@ use std::{
time::Duration,
};
use axum::{extract::State, routing::get, Router};
use axum_client_ip::{SecureClientIp, SecureClientIpSource};
use axum::{
extract::{Query, State},
routing::get,
Router,
};
use axum_client_ip::ClientIp;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity;
use config::Config;
use http::StatusCode;
use miette::{bail, ensure, Context, IntoDiagnostic, Result};
use tracing::{debug, error, info};
use tracing_subscriber::EnvFilter;
mod auth;
mod config;
mod nsupdate;
mod password;
mod records;
@ -28,120 +34,52 @@ struct Opts {
#[command(flatten)]
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
#[arg(long, default_value = ".")]
#[arg(long, env, default_value = ".")]
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
#[arg(long)]
insecure: bool,
/// Set client IP source
///
/// see: <https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html>
#[clap(long, default_value = "RightmostXForwardedFor")]
ip_source: SecureClientIpSource,
#[clap(flatten)]
config_or_command: ConfigOrCommand,
}
/// Set which IPs to allow updating
#[clap(long, default_value_t = IpType::Both)]
ip_type: IpType,
#[derive(clap::Args, Debug)]
#[group(multiple = false)]
struct ConfigOrCommand {
/// Path to the configuration file
#[arg(long, short)]
config: Option<PathBuf>,
#[clap(subcommand)]
subcommand: Option<Cmd>,
}
#[derive(Debug, Default, Clone, Copy)]
enum IpType {
#[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}'"),
}
impl ConfigOrCommand {
pub fn take(&mut self) -> (Option<PathBuf>, Option<Cmd>) {
(self.config.take(), self.subcommand.take())
}
}
#[derive(Debug, Subcommand)]
enum Cmd {
Mkpasswd(password::Mkpasswd),
/// Verify the records file
Verify,
/// Verify the configuration file
Verify {
/// Path to the configuration file
config: PathBuf,
},
}
impl Cmd {
pub fn process(self, args: &Opts) -> Result<()> {
match self {
Cmd::Mkpasswd(mkpasswd) => mkpasswd.process(args),
Cmd::Verify => records::load(&args.records).map(drop),
Cmd::Verify { config } => config::Config::load(&config) // load config
.and_then(Config::verified) // verify config
.map(drop), // ignore config data
}
}
}
@ -164,7 +102,7 @@ struct AppState<'a> {
last_ips: std::sync::Arc<tokio::sync::Mutex<SavedIPs>>,
/// The IP type for which to allow updates
ip_type: IpType,
ip_type: config::IpType,
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
@ -207,33 +145,38 @@ impl SavedIPs {
}
impl AppState<'static> {
fn from_args(args: &Opts) -> miette::Result<Self> {
fn from_args(args: &Opts, config: &config::Config) -> miette::Result<Self> {
let Opts {
verbosity: _,
address: _,
port: _,
password_file: _,
data_dir,
key_file,
insecure,
subcommand: _,
records,
salt: _,
ttl,
ip_source: _,
ip_type,
config_or_command: _,
} = args;
// Set state
let ttl = Duration::from_secs(*ttl);
let config::Records {
ttl,
records,
client_id: _,
router_domain: _,
ip_source: _,
ip_type,
key_file,
} = &config.records;
// Use last registered IP address if available
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 {
ttl,
// Load DNS records
records: records::load_no_verify(records)?,
ttl: **ttl,
records,
// Load keyfile
key_file: key_file
.as_deref()
@ -281,6 +224,37 @@ fn load_ip(path: &Path) -> Result<Option<SavedIPs>> {
.map(Some)
}
#[derive(Clone, Copy, Debug)]
struct Ipv6Prefix {
prefix: Ipv6Addr,
length: u32,
}
impl std::fmt::Display for Ipv6Prefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { prefix, length } = self;
write!(f, "{prefix}/{length}")
}
}
impl std::str::FromStr for Ipv6Prefix {
type Err = miette::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let (addr, len) = s.split_once('/').wrap_err("missing `/` in ipv6 prefix")?;
Ok(Self {
prefix: addr
.parse()
.into_diagnostic()
.wrap_err("invalid ipv6 address for ipv6 prefix")?,
length: len
.parse()
.into_diagnostic()
.wrap_err("invalid length for ipv6 prefix")?,
})
}
}
#[tracing::instrument(err)]
fn main() -> Result<()> {
// set panic hook to pretty print with miette's formatter
@ -305,34 +279,37 @@ fn main() -> Result<()> {
debug!("{args:?}");
// process subcommand
if let Some(cmd) = args.subcommand.take() {
return cmd.process(&args);
}
let config = match args.config_or_command.take() {
// process subcommand
(None, Some(cmd)) => return cmd.process(&args),
(Some(path), None) => {
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
let state = AppState::from_args(&args)?;
let state = AppState::from_args(&args, &config)?;
let Opts {
verbosity: _,
address: ip,
port,
password_file,
data_dir: _,
key_file: _,
insecure,
subcommand: _,
records: _,
salt,
ttl: _,
ip_source,
ip_type,
config_or_command: _,
} = args;
info!("checking environment");
// Load password hash
let password_hash = password_file
let password_hash = config
.password
.password_file
.map(|path| -> miette::Result<_> {
let path = path.as_path();
let pass = std::fs::read_to_string(path).into_diagnostic()?;
@ -362,12 +339,15 @@ fn main() -> Result<()> {
rt.block_on(async {
// Update DNS record with previous IPs (if available)
let ips = state.last_ips.lock().await.clone();
for ip in ips.ips() {
if !ip_type.valid_for_type(ip) {
continue;
}
match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await {
let mut actions = ips
.ips()
.filter(|ip| config.records.ip_type.valid_for_type(*ip))
.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 {
Ok(status) => {
if !status.success() {
error!("nsupdate failed: code {status}");
@ -387,19 +367,24 @@ fn main() -> Result<()> {
let app = Router::new().route("/update", get(update_records));
// if a password is provided, validate it
let app = if let Some(pass) = password_hash {
app.layer(auth::layer(Box::leak(pass), String::leak(salt)))
app.layer(auth::layer(
Box::leak(pass),
Box::leak(config.password.salt),
))
} else {
app
}
.layer(ip_source.into_extension())
.layer(config.records.ip_source.into_extension())
.with_state(state);
let config::Server { address } = config.server;
// Start services
info!("starting listener on {ip}:{port}");
let listener = tokio::net::TcpListener::bind(SocketAddr::new(ip, port))
info!("starting listener on {address}");
let listener = tokio::net::TcpListener::bind(address)
.await
.into_diagnostic()?;
info!("listening on {ip}:{port}");
info!("listening on {address}");
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
@ -410,20 +395,144 @@ fn main() -> Result<()> {
.wrap_err("failed to run main loop")
}
/// Serde deserialization decorator to map empty Strings to None,
///
/// Adapted from: <https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs>
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
use serde::Deserialize;
let opt = Option::<std::borrow::Cow<'de, str>>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => s.parse::<T>().map_err(serde::de::Error::custom).map(Some),
}
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct FritzBoxUpdateParams {
/// The domain that should be updated
#[allow(unused)]
#[serde(default, deserialize_with = "empty_string_as_none")]
domain: Option<String>,
/// IPv4 address for the domain
#[serde(default, deserialize_with = "empty_string_as_none")]
ipv4: Option<Ipv4Addr>,
/// IPv6 address for the domain
#[serde(default, deserialize_with = "empty_string_as_none")]
ipv6: Option<Ipv6Addr>,
/// IPv6 prefix for the home network
#[allow(unused)]
#[serde(default, deserialize_with = "empty_string_as_none")]
ipv6prefix: Option<Ipv6Prefix>,
/// Whether the networks uses both IPv4 and IPv6
#[allow(unused)]
#[serde(default, deserialize_with = "empty_string_as_none")]
dualstack: Option<String>,
}
impl FritzBoxUpdateParams {
fn has_data(&self) -> bool {
let Self {
domain,
ipv4,
ipv6,
ipv6prefix,
dualstack,
} = self;
domain.is_some()
| ipv4.is_some()
| ipv6.is_some()
| ipv6prefix.is_some()
| dualstack.is_some()
}
}
#[tracing::instrument(skip(state), level = "trace", ret(level = "info"))]
async fn update_records(
State(state): State<AppState<'static>>,
SecureClientIp(ip): SecureClientIp,
ClientIp(ip): ClientIp,
Query(update_params): Query<FritzBoxUpdateParams>,
) -> axum::response::Result<&'static str> {
info!("accepted update from {ip}");
if !state.ip_type.valid_for_type(ip) {
let ip_type = state.ip_type;
tracing::warn!("rejecting update from {ip} as we are running a {ip_type} filter");
return Err((StatusCode::CONFLICT, format!("running in {ip_type} mode")).into());
if !update_params.has_data() {
if !state.ip_type.valid_for_type(ip) {
tracing::warn!(
"rejecting update from {ip} as we are running a {} filter",
state.ip_type
);
return Err((
StatusCode::CONFLICT,
format!("running in {} mode", state.ip_type),
)
.into());
}
return trigger_update(ip, &state).await;
}
match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await {
// FIXME: mark suspicious updates (where IP doesn't match the update_ip) and reject them based
// on policy
let FritzBoxUpdateParams {
domain: _,
ipv4,
ipv6,
ipv6prefix: _,
dualstack: _,
} = update_params;
if ipv4.is_none() && ipv6.is_none() {
return Err((
StatusCode::BAD_REQUEST,
"failed to provide an IP for the update",
)
.into());
}
if let Some(ip) = ipv4 {
let ip = IpAddr::V4(ip);
if state.ip_type.valid_for_type(ip) {
_ = trigger_update(ip, &state).await?;
} else {
tracing::warn!("requested update of IPv4 but we are {}", state.ip_type);
}
}
if let Some(ip) = ipv6 {
let ip = IpAddr::V6(ip);
if state.ip_type.valid_for_type(ip) {
_ = trigger_update(ip, &state).await?;
} else {
tracing::warn!("requested update of IPv6 but we are {}", state.ip_type);
}
}
Ok("Successfully updated IP of records!\n")
}
#[tracing::instrument(skip(state), level = "trace", ret(level = "info"))]
async fn trigger_update(
ip: IpAddr,
state: &AppState<'static>,
) -> axum::response::Result<&'static str> {
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 {
Ok(status) if status.success() => {
let ips = {
// Update state
@ -432,29 +541,169 @@ async fn update_records(
ips.clone()
};
let ip_file = state.ip_file;
tokio::task::spawn_blocking(move || {
info!("updating last ips to {ips:?}");
let data = serde_json::to_vec(&ips).expect("invalid serialization impl");
if let Err(err) = std::fs::write(state.ip_file, data) {
if let Err(err) = std::fs::write(ip_file, data) {
error!("Failed to update last IP: {err}");
}
info!("updated last ips to {ips:?}");
});
Ok("successful update")
Ok("Successfully updated IP of records!\n")
}
Ok(status) => {
error!("nsupdate failed with code {status}");
Err((
StatusCode::INTERNAL_SERVER_ERROR,
"nsupdate failed, check server logs",
"nsupdate failed, check server logs\n",
)
.into())
}
Err(error) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to update records: {error}"),
format!("failed to update records: {error}\n"),
)
.into()),
}
}
#[cfg(test)]
mod parse_query_params {
use axum::extract::Query;
use super::FritzBoxUpdateParams;
#[test]
fn no_params() {
let uri = http::Uri::builder()
.path_and_query("/update")
.build()
.unwrap();
let query: Query<FritzBoxUpdateParams> = Query::try_from_uri(&uri).unwrap();
insta::assert_debug_snapshot!(query, @r#"
Query(
FritzBoxUpdateParams {
domain: None,
ipv4: None,
ipv6: None,
ipv6prefix: None,
dualstack: None,
},
)
"#);
}
#[test]
fn ipv4() {
let uri = http::Uri::builder()
.path_and_query("/update?ipv4=1.2.3.4")
.build()
.unwrap();
let query: Query<FritzBoxUpdateParams> = Query::try_from_uri(&uri).unwrap();
insta::assert_debug_snapshot!(query, @r#"
Query(
FritzBoxUpdateParams {
domain: None,
ipv4: Some(
1.2.3.4,
),
ipv6: None,
ipv6prefix: None,
dualstack: None,
},
)
"#);
}
#[test]
fn ipv6() {
let uri = http::Uri::builder()
.path_and_query("/update?ipv6=%3A%3A1234")
.build()
.unwrap();
let query: Query<FritzBoxUpdateParams> = Query::try_from_uri(&uri).unwrap();
insta::assert_debug_snapshot!(query, @r#"
Query(
FritzBoxUpdateParams {
domain: None,
ipv4: None,
ipv6: Some(
::1234,
),
ipv6prefix: None,
dualstack: None,
},
)
"#);
}
#[test]
fn ipv4_and_ipv6() {
let uri = http::Uri::builder()
.path_and_query("/update?ipv4=1.2.3.4&ipv6=%3A%3A1234")
.build()
.unwrap();
let query: Query<FritzBoxUpdateParams> = Query::try_from_uri(&uri).unwrap();
insta::assert_debug_snapshot!(query, @r#"
Query(
FritzBoxUpdateParams {
domain: None,
ipv4: Some(
1.2.3.4,
),
ipv6: Some(
::1234,
),
ipv6prefix: None,
dualstack: None,
},
)
"#);
}
#[test]
fn ipv4_and_empty_ipv6() {
let uri = http::Uri::builder()
.path_and_query("/update?ipv4=1.2.3.4&ipv6=")
.build()
.unwrap();
let query: Query<FritzBoxUpdateParams> = Query::try_from_uri(&uri).unwrap();
insta::assert_debug_snapshot!(query, @r#"
Query(
FritzBoxUpdateParams {
domain: None,
ipv4: Some(
1.2.3.4,
),
ipv6: None,
ipv6prefix: None,
dualstack: None,
},
)
"#);
}
#[test]
fn empty_ipv4_and_ipv6() {
let uri = http::Uri::builder()
.path_and_query("/update?ipv4=&ipv6=%3A%3A1234")
.build()
.unwrap();
let query: Query<FritzBoxUpdateParams> = Query::try_from_uri(&uri).unwrap();
insta::assert_debug_snapshot!(query, @r#"
Query(
FritzBoxUpdateParams {
domain: None,
ipv4: None,
ipv6: Some(
::1234,
),
ipv6prefix: None,
dualstack: None,
},
)
"#);
}
}

View file

@ -9,12 +9,51 @@ use std::{
use tokio::io::AsyncWriteExt;
use tracing::{debug, warn};
#[tracing::instrument(level = "trace", ret(level = "warn"))]
pub enum Action<'a> {
// Reassign a domain to a different IP
Reassign {
domain: &'a str,
to: IpAddr,
ttl: Duration,
},
}
impl<'a> Action<'a> {
/// Create a set of [`Action`]s reassigning the domains in `records` to the specified
/// [`IpAddr`]
pub fn from_records(
to: IpAddr,
ttl: Duration,
records: &'a [&'a str],
) -> impl IntoIterator<Item = Self> + std::iter::ExactSizeIterator + 'a {
records
.iter()
.map(move |&domain| Action::Reassign { domain, to, ttl })
}
}
impl std::fmt::Display for Action<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::Reassign { domain, to, ttl } => {
let ttl = ttl.as_secs();
let kind = match to {
IpAddr::V4(_) => "A",
IpAddr::V6(_) => "AAAA",
};
// Delete previous record of type `kind`
writeln!(f, "update delete {domain} {ttl} IN {kind}")?;
// Add record with new IP
writeln!(f, "update add {domain} {ttl} IN {kind} {to}")
}
}
}
}
#[tracing::instrument(level = "trace", skip(actions), ret(level = "warn"))]
pub async fn nsupdate(
ip: IpAddr,
ttl: Duration,
key_file: Option<&Path>,
records: &[&str],
actions: impl IntoIterator<Item = Action<'_>>,
) -> std::io::Result<ExitStatus> {
let mut cmd = tokio::process::Command::new("nsupdate");
if let Some(key_file) = key_file {
@ -27,10 +66,13 @@ pub async fn nsupdate(
.inspect_err(|err| warn!("failed to spawn child: {err}"))?;
let mut stdin = child.stdin.take().expect("stdin not present");
debug!("sending update request");
let mut buf = Vec::new();
update_ns_records(&mut buf, actions).unwrap();
stdin
.write_all(update_ns_records(ip, ttl, records).as_bytes())
.write_all(&buf)
.await
.inspect_err(|err| warn!("failed to write to the stdin of nsupdate: {err}"))?;
debug!("closing stdin");
stdin
.shutdown()
@ -43,21 +85,16 @@ pub async fn nsupdate(
.inspect_err(|err| warn!("failed to wait for child: {err}"))
}
fn update_ns_records(ip: IpAddr, ttl: Duration, records: &[&str]) -> String {
use std::fmt::Write;
let ttl_s: u64 = ttl.as_secs();
let rec_type = match ip {
IpAddr::V4(_) => "A",
IpAddr::V6(_) => "AAAA",
};
let mut cmds = String::from("server 127.0.0.1\n");
for &record in records {
writeln!(cmds, "update delete {record} {ttl_s} IN {rec_type}").unwrap();
writeln!(cmds, "update add {record} {ttl_s} IN {rec_type} {ip}").unwrap();
fn update_ns_records<'a>(
mut buf: impl std::io::Write,
actions: impl IntoIterator<Item = Action<'a>>,
) -> std::io::Result<()> {
writeln!(buf, "server 127.0.0.1")?;
for action in actions {
write!(buf, "{action}")?;
}
writeln!(cmds, "send\nquit").unwrap();
cmds
writeln!(buf, "send")?;
writeln!(buf, "quit")
}
#[cfg(test)]
@ -66,17 +103,21 @@ mod test {
use insta::assert_snapshot;
use super::update_ns_records;
use super::{update_ns_records, Action};
use crate::DEFAULT_TTL;
#[test]
#[allow(non_snake_case)]
fn expected_update_string_A() {
assert_snapshot!(update_ns_records(
IpAddr::V4(Ipv4Addr::LOCALHOST),
DEFAULT_TTL,
&["example.com.", "example.org.", "example.net."],
), @r###"
let mut buf = Vec::new();
let actions = Action::from_records(
IpAddr::V4(Ipv4Addr::LOCALHOST),
DEFAULT_TTL,
&["example.com.", "example.org.", "example.net."],
);
update_ns_records(&mut buf, actions).unwrap();
assert_snapshot!(String::from_utf8(buf).unwrap(), @r###"
server 127.0.0.1
update delete example.com. 60 IN A
update add example.com. 60 IN A 127.0.0.1
@ -92,11 +133,15 @@ mod test {
#[test]
#[allow(non_snake_case)]
fn expected_update_string_AAAA() {
assert_snapshot!(update_ns_records(
IpAddr::V6(Ipv6Addr::LOCALHOST),
DEFAULT_TTL,
&["example.com.", "example.org.", "example.net."],
), @r###"
let mut buf = Vec::new();
let actions = Action::from_records(
IpAddr::V6(Ipv6Addr::LOCALHOST),
DEFAULT_TTL,
&["example.com.", "example.org.", "example.net."],
);
update_ns_records(&mut buf, actions).unwrap();
assert_snapshot!(String::from_utf8(buf).unwrap(), @r###"
server 127.0.0.1
update delete example.com. 60 IN AAAA
update add example.com. 60 IN AAAA ::1

View file

@ -4,7 +4,7 @@
//! records
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use std::path::PathBuf;
use base64::prelude::*;
use miette::{Context, IntoDiagnostic, Result};
@ -20,11 +20,18 @@ pub struct Mkpasswd {
/// The password
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 {
pub fn process(self, args: &crate::Opts) -> Result<()> {
mkpasswd(self, args.password_file.as_deref(), &args.salt)
pub fn process(self, _args: &crate::Opts) -> Result<()> {
mkpasswd(self)
}
}
@ -45,13 +52,16 @@ pub fn hash_identity(username: &str, password: &str, salt: &str) -> Digest {
}
pub fn mkpasswd(
Mkpasswd { username, password }: Mkpasswd,
password_file: Option<&Path>,
salt: &str,
Mkpasswd {
username,
password,
salt,
password_file,
}: Mkpasswd,
) -> 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 Some(path) = password_file else {
let Some(path) = password_file.as_deref() else {
println!("{encoded}");
return Ok(());
};

View file

@ -1,52 +1,9 @@
//! Deal with the DNS records
use std::path::Path;
use miette::{ensure, miette, LabeledSpan, Result};
use miette::{ensure, miette, Context, IntoDiagnostic, LabeledSpan, NamedSource, Result};
/// 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(())
pub fn validate_record_str(record: &str) -> Result<()> {
validate_line(0, record).map_err(|err| err.with_source_code(String::from(record)))
}
fn validate_line(offset: usize, line: &str) -> Result<()> {
@ -156,7 +113,7 @@ fn validate_octet(offset: usize, octet: u8) -> Result<()> {
#[cfg(test)]
mod test {
use crate::records::verify;
use crate::records::validate_record_str;
macro_rules! assert_miette_snapshot {
($diag:expr) => {{
@ -180,104 +137,51 @@ mod test {
#[test]
fn valid_records() -> miette::Result<()> {
verify(
"\
example.com.\n\
example.org.\n\
example.net.\n\
subdomain.example.com.\n\
",
std::path::Path::new("test_records_valid"),
)
for record in [
"example.com.",
"example.org.",
"example.net.",
"subdomain.example.com.",
] {
validate_record_str(record)?;
}
Ok(())
}
#[test]
fn hostname_too_long() {
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();
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();
assert_miette_snapshot!(err);
}
#[test]
fn not_fqd() {
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();
let err = validate_record_str("example.net").unwrap_err();
assert_miette_snapshot!(err);
}
#[test]
fn empty_label() {
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();
let err = validate_record_str("name..example.org.").unwrap_err();
assert_miette_snapshot!(err);
}
#[test]
fn label_too_long() {
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();
let err = validate_record_str("name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.").unwrap_err();
assert_miette_snapshot!(err);
}
#[test]
fn invalid_ascii() {
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();
let err = validate_record_str("name.this-is-not-ascii-ß.example.org.").unwrap_err();
assert_miette_snapshot!(err);
}
#[test]
fn invalid_octet() {
let err = verify(
"\
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();
let err =
validate_record_str("name.this-character:-is-not-allowed.example.org.").unwrap_err();
assert_miette_snapshot!(err);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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