Compare commits
143 commits
Author | SHA1 | Date | |
---|---|---|---|
c3cb6f6298 | |||
b600518091 | |||
55d7d3eb5a | |||
c7fdb1acc5 | |||
5efb1f4344 | |||
ca8b9f00a0 | |||
c270a24de8 | |||
0f4ff679d2 | |||
e28cf9be3f | |||
1dac706aeb | |||
29916c0841 | |||
3c9587bd4c | |||
7dcd2c6a4c | |||
5a98e8205f | |||
2183c81d70 | |||
4ab2f709a9 | |||
269a37c920 | |||
6ac1133486 | |||
c5c51645ba | |||
fe58c295ff | |||
f356463079 | |||
caa7c1165f | |||
7dca0efdf2 | |||
5f2ec1089f | |||
ba2babb0ae | |||
f9da4aecd4 | |||
2d00dd3818 | |||
2daf620a4a | |||
316f2bf576 | |||
3d660314cf | |||
f207cbe859 | |||
c589fb40c3 | |||
08ea3271c1 | |||
8787adae30 | |||
130c949723 | |||
e236aa424b | |||
baeb98a2e6 | |||
9b41d7d2a5 | |||
33f8b1570d | |||
06995416d2 | |||
f8848e669e | |||
72843b8f52 | |||
4cdffc20bc | |||
cf66c77136 | |||
5fc53886f2 | |||
881983dd6c | |||
632250d544 | |||
c71a8b418c | |||
873dd980ff | |||
3e1140ffe5 | |||
6c78e8d78e | |||
3ed76c094c | |||
f16c3b9138 | |||
42482574ac | |||
855963bc85 | |||
7cc182b23e | |||
bc62fd7c1d | |||
cdef8078cc | |||
a6f0785dc3 | |||
5d4c0fdb70 | |||
2a2f9ef06c | |||
ef6e955b90 | |||
b581e2adf1 | |||
48034ec6e5 | |||
593bee9024 | |||
09345f2193 | |||
71d1e43ef2 | |||
528aad1d8e | |||
cb7e4d554b | |||
01f53b2bf0 | |||
60662ff1f0 | |||
eaed7b2302 | |||
8a04c2726f | |||
1a88dbaeb2 | |||
c41008f800 | |||
3c18f07a2a | |||
0a5348097d | |||
bdb27d7cb1 | |||
41c30372fb | |||
e99bc52de2 | |||
338e296683 | |||
29f7315f67 | |||
738fa8accf | |||
172076eaad | |||
b775f8e811 | |||
48c2e5be4d | |||
d56af9ecfe | |||
72aa4f365e | |||
d8630aa8cb | |||
5655d7de67 | |||
d2f6c3cd66 | |||
13c9c544a7 | |||
70ed898f1d | |||
2f97008475 | |||
09bd450a46 | |||
a6bb8bf817 | |||
a39fa354e4 | |||
26566fd612 | |||
09be5627c3 | |||
dff29cab77 | |||
c8407a8eb4 | |||
77cb03576d | |||
a47dd0bfac | |||
34ce8a69f6 | |||
ea428d1aef | |||
98aa3c2a97 | |||
0fc17d9150 | |||
e72e3777b8 | |||
2473e6edbc | |||
989ed2a080 | |||
2e8d20f89d | |||
ec27e31336 | |||
e5f7d94f77 | |||
a2735b46b5 | |||
542336867a | |||
70162c83f6 | |||
0fd9a87907 | |||
880d462e80 | |||
40a9d600c9 | |||
8bf62f3ce2 | |||
883f6e6ae7 | |||
4863ebc6df | |||
36b4d55ea8 | |||
faf1f637ab | |||
9b2880c141 | |||
0685c2601a | |||
5e16700652 | |||
ccc1ccba97 | |||
a515c5d8df | |||
7fdf322c73 | |||
5d6fd054ee | |||
2b953c4b75 | |||
c038b68ecb | |||
dcba690961 | |||
99e887513d | |||
4490dfac05 | |||
22ab037b6f | |||
e4451beebf | |||
6a1feb2612 | |||
2ba6277778 | |||
69fde96f67 | |||
f6084449fa | |||
e9d5b87ecc |
27 changed files with 1736 additions and 887 deletions
14
.editorconfig
Normal file
14
.editorconfig
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{nix,toml,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
|
@ -1,26 +1,39 @@
|
||||||
on: [push]
|
on: [push]
|
||||||
jobs:
|
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:
|
build:
|
||||||
runs-on: nixos
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
- uses: https://git.salame.cl/actions/checkout@v4
|
- uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
- run: nix --version
|
- name: Build Package
|
||||||
- run: nix build --print-build-logs .#
|
run: |
|
||||||
check:
|
nix --version
|
||||||
|
nix build --print-build-logs .#
|
||||||
|
test:
|
||||||
needs: build # we use the built binaries in the checks
|
needs: build # we use the built binaries in the checks
|
||||||
runs-on: nixos
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
- uses: https://git.salame.cl/actions/checkout@v4
|
- uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
- run: nix --version
|
- name: Run tests
|
||||||
- run: nix flake check --keep-going --verbose --print-build-logs
|
run: |
|
||||||
|
nix --version
|
||||||
|
nix-fast-build --max-jobs 2 --no-nom --skip-cached --no-link \
|
||||||
|
--flake ".#checks.$(nix eval --raw --impure --expr builtins.currentSystem)"
|
||||||
report-size:
|
report-size:
|
||||||
runs-on: nixos
|
runs-on: nixos
|
||||||
needs: build
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- uses: https://git.salame.cl/actions/checkout@v4
|
- uses: https://git.salame.cl/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
- run: nix --version
|
- run: nix --version
|
||||||
- name: Generate size report
|
- 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:
|
with:
|
||||||
comment-on-pr: ${{ github.ref_name != 'main' }}
|
comment-on-pr: ${{ github.ref_name != 'main' }}
|
||||||
generate-artifact: ${{ github.ref_name == 'main' }}
|
generate-artifact: ${{ github.ref_name == 'main' }}
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
# 03:42 on Saturdays
|
|
||||||
- cron: '42 3 * * 6'
|
|
||||||
env:
|
|
||||||
PR_TITLE: Weekly `cargo update` of dependencies
|
|
||||||
PR_MESSAGE: |
|
|
||||||
Automation to keep dependencies in `Cargo.lock` current.
|
|
||||||
|
|
||||||
The following is the output from `cargo update`:
|
|
||||||
COMMIT_MESSAGE: "chore(deps): cargo update"
|
|
||||||
jobs:
|
|
||||||
update-cargo:
|
|
||||||
runs-on: nixos
|
|
||||||
env:
|
|
||||||
BRANCH_NAME: cargo-update
|
|
||||||
steps:
|
|
||||||
- uses: https://git.salame.cl/actions/checkout@v4
|
|
||||||
- run: nix --version
|
|
||||||
- run: nix run .#cargo-update
|
|
||||||
- name: craft PR body and commit message
|
|
||||||
run: |
|
|
||||||
set -xeuo pipefail
|
|
||||||
|
|
||||||
echo "${COMMIT_MESSAGE}" > commit.txt
|
|
||||||
printf '\n\n' >> commit.txt
|
|
||||||
cat cargo_update.log >> commit.txt
|
|
||||||
|
|
||||||
echo "${PR_MESSAGE}" > body.md
|
|
||||||
echo '```txt' >> body.md
|
|
||||||
cat cargo_update.log >> body.md
|
|
||||||
echo '```' >> body.md
|
|
||||||
- name: commit
|
|
||||||
run: |
|
|
||||||
set -xeuo pipefail
|
|
||||||
|
|
||||||
git config user.name forgejo-actions
|
|
||||||
git config user.email forgejo-actions@salame.cl
|
|
||||||
git switch --force-create "$BRANCH_NAME"
|
|
||||||
git add ./Cargo.lock
|
|
||||||
DIFF="$(git diff --staged)"
|
|
||||||
if [[ "$DIFF" == "" ]]; then
|
|
||||||
echo "Cargo.lock was not changed, bailing out and not making a PR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
git commit --no-verify --file=commit.txt
|
|
||||||
- name: push
|
|
||||||
run: |
|
|
||||||
set -xeuo pipefail
|
|
||||||
git push --no-verify --force --set-upstream origin "$BRANCH_NAME"
|
|
||||||
- name: open new pull request
|
|
||||||
env:
|
|
||||||
# We have to use a Personal Access Token (PAT) here.
|
|
||||||
# PRs opened from a workflow using the standard `GITHUB_TOKEN` in GitHub Actions
|
|
||||||
# do not automatically trigger more workflows:
|
|
||||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: token $GITHUB_TOKEN" \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d "$(
|
|
||||||
echo '{}' |
|
|
||||||
jq --arg body "$(cat body.md)" \
|
|
||||||
--arg title "$COMMIT_MESSAGE" \
|
|
||||||
--arg head "$BRANCH_NAME" \
|
|
||||||
'{"body": $body, "title": $title, "head": $head, "base": "main"}'
|
|
||||||
)" \
|
|
||||||
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls"
|
|
44
.renovaterc.json
Normal file
44
.renovaterc.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"assignees": [
|
||||||
|
"jalil"
|
||||||
|
],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeStrategy": "auto",
|
||||||
|
"automergeType": "pr",
|
||||||
|
"commitBodyTable": true,
|
||||||
|
"dependencyDashboard": true,
|
||||||
|
"extends": [
|
||||||
|
"config:best-practices"
|
||||||
|
],
|
||||||
|
"prCreation": "immediate",
|
||||||
|
"cargo": {
|
||||||
|
"commitMessageTopic": "Rust crate {{depName}}",
|
||||||
|
"fileMatch": [
|
||||||
|
"(^|/)Cargo\\.toml$"
|
||||||
|
],
|
||||||
|
"versioning": "cargo",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"nix": {
|
||||||
|
"fileMatch": [
|
||||||
|
"(^|/)flake\\.nix$"
|
||||||
|
],
|
||||||
|
"commitMessageTopic": "nixpkgs",
|
||||||
|
"commitMessageExtra": "to {{newValue}}",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true,
|
||||||
|
"recreateWhen": "always",
|
||||||
|
"rebaseWhen": "behind-base-branch",
|
||||||
|
"branchTopic": "lock-file-maintenance",
|
||||||
|
"commitMessageAction": "Lock file maintenance",
|
||||||
|
"schedule": [
|
||||||
|
"* 22 * * *"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"automergeSchedule": [
|
||||||
|
"* 23 * * *"
|
||||||
|
]
|
||||||
|
}
|
49
CHANGELOG.md
49
CHANGELOG.md
|
@ -2,6 +2,55 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.3.6] - 2025-01-26
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(webnsupdate)* Allow running in IPv4/6 only mode
|
||||||
|
- *(module)* Add option for setting --ip-type
|
||||||
|
- *(flake)* Add tests for new allowedIPVersion option
|
||||||
|
|
||||||
|
## [0.3.5] - 2025-01-23
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(renovate)* Enable lockFileMaintenance
|
||||||
|
- *(webnsupdate)* Add handling for multiple IPs
|
||||||
|
- Tune compilation for size
|
||||||
|
- *(tests)* Add nginx integration test
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(flake)* Switch to github ref
|
||||||
|
- *(renovate)* Switch automergeStrategy to auto
|
||||||
|
- *(ci)* Remove update workflow
|
||||||
|
- *(typos)* Typos caught more typos :3
|
||||||
|
- *(renovate)* Branch creation before automerge
|
||||||
|
- *(renovaterc)* Invalid cron syntax
|
||||||
|
- *(deps)* Update rust crate clap to v4.5.24
|
||||||
|
- *(deps)* Update rust crate tokio to v1.43.0
|
||||||
|
- *(deps)* Update rust crate clap to v4.5.25
|
||||||
|
- *(deps)* Update rust crate clap to v4.5.26
|
||||||
|
- *(flake)* Switch overlay to callPackage
|
||||||
|
- *(deps)* Update rust crate clap to v4.5.27
|
||||||
|
- *(deps)* Update rust crate axum to v0.8.2
|
||||||
|
- *(module)* Test both IPv4 and IPv6
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- Setup renovate to manage dependencies
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update to axum 0.8
|
||||||
|
- Parallelize checks
|
||||||
|
|
||||||
|
## [0.3.4] - 2024-12-26
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(main)* Add more logging and default to info
|
||||||
|
|
||||||
## [0.3.3] - 2024-12-22
|
## [0.3.3] - 2024-12-22
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
302
Cargo.lock
generated
302
Cargo.lock
generated
|
@ -67,34 +67,24 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "anstyle-wincon"
|
||||||
version = "3.0.6"
|
version = "3.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
|
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
"once_cell",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-trait"
|
|
||||||
version = "0.1.83"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.7.9"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
@ -122,9 +112,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-client-ip"
|
name = "axum-client-ip"
|
||||||
version = "0.6.1"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac"
|
checksum = "b9329923fe6c30624095e63cb6c25796b32ffbf5d1da8c3a95d1054c301db92a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"forwarded-header-value",
|
"forwarded-header-value",
|
||||||
|
@ -133,13 +123,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.4.5"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-core",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
@ -184,21 +173,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.9.0"
|
version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.5"
|
version = "1.2.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
|
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
@ -211,9 +200,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.23"
|
version = "4.5.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
|
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
@ -231,9 +220,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.23"
|
version = "4.5.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
|
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
@ -243,9 +232,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.18"
|
version = "4.5.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -267,9 +256,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.10"
|
version = "0.15.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b"
|
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -285,9 +274,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.10"
|
version = "0.3.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
@ -315,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nonempty",
|
"nonempty",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -376,9 +365,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.2.0"
|
version = "1.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
|
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
|
@ -397,12 +386,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body-util"
|
name = "http-body-util"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-core",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
@ -410,9 +399,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.9.5"
|
version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpdate"
|
name = "httpdate"
|
||||||
|
@ -421,10 +410,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "humantime"
|
||||||
version = "1.5.2"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -441,9 +436,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -457,13 +452,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.41.1"
|
version = "1.42.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8"
|
checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"lazy_static",
|
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
|
"once_cell",
|
||||||
|
"pin-project",
|
||||||
|
"serde",
|
||||||
"similar",
|
"similar",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -481,9 +478,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.14"
|
version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
|
@ -493,9 +490,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.169"
|
version = "0.2.171"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
|
@ -505,15 +502,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
|
@ -526,9 +523,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
|
@ -538,9 +535,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miette"
|
name = "miette"
|
||||||
version = "7.4.0"
|
version = "7.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64"
|
checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"backtrace-ext",
|
"backtrace-ext",
|
||||||
|
@ -552,15 +549,15 @@ dependencies = [
|
||||||
"supports-unicode",
|
"supports-unicode",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"unicode-width",
|
"unicode-width 0.1.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miette-derive"
|
name = "miette-derive"
|
||||||
version = "7.4.0"
|
version = "7.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67"
|
checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -575,9 +572,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.2"
|
version = "0.8.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
]
|
]
|
||||||
|
@ -611,18 +608,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.5"
|
version = "0.36.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
|
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.20.2"
|
version = "1.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
|
@ -632,9 +629,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "owo-colors"
|
||||||
version = "4.1.0"
|
version = "4.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
|
checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
|
@ -643,10 +640,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project"
|
||||||
version = "0.2.15"
|
version = "1.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
|
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-internal"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-utils"
|
name = "pin-utils"
|
||||||
|
@ -656,18 +673,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.92"
|
version = "1.0.94"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.37"
|
version = "1.0.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
@ -718,15 +735,14 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"libc",
|
"libc",
|
||||||
"spin",
|
|
||||||
"untrusted",
|
"untrusted",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
@ -739,9 +755,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.42"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
|
@ -752,30 +768,30 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.18"
|
version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
|
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.216"
|
version = "1.0.219"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.216"
|
version = "1.0.219"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -784,9 +800,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.133"
|
version = "1.0.140"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -796,9 +812,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_path_to_error"
|
name = "serde_path_to_error"
|
||||||
version = "0.1.16"
|
version = "0.1.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
|
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -842,32 +858,26 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "similar"
|
name = "similar"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.9.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
@ -897,9 +907,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.90"
|
version = "2.0.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -914,9 +924,9 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
|
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
@ -924,12 +934,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.16.1"
|
version = "0.16.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
"unicode-width",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -938,7 +948,16 @@ version = "1.0.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"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]]
|
[[package]]
|
||||||
|
@ -952,6 +971,17 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.8"
|
version = "1.1.8"
|
||||||
|
@ -964,9 +994,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.42.0"
|
version = "1.44.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -981,9 +1011,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1097,9 +1127,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.14"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-linebreak"
|
name = "unicode-linebreak"
|
||||||
|
@ -1113,6 +1143,12 @@ version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -1127,9 +1163,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
|
@ -1139,7 +1175,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webnsupdate"
|
name = "webnsupdate"
|
||||||
version = "0.3.3"
|
version = "0.3.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-client-ip",
|
"axum-client-ip",
|
||||||
|
@ -1147,9 +1183,13 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap-verbosity-flag",
|
"clap-verbosity-flag",
|
||||||
"http",
|
"http",
|
||||||
|
"humantime",
|
||||||
"insta",
|
"insta",
|
||||||
"miette",
|
"miette",
|
||||||
"ring",
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
19
Cargo.toml
19
Cargo.toml
|
@ -1,10 +1,9 @@
|
||||||
cargo-features = ["codegen-backend"]
|
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
description = "An HTTP server using HTTP basic auth to make secure calls to nsupdate"
|
description = "An HTTP server using HTTP basic auth to make secure calls to nsupdate"
|
||||||
name = "webnsupdate"
|
name = "webnsupdate"
|
||||||
version = "0.3.3"
|
version = "0.3.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
license-file = "LICENSE"
|
license-file = "LICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["dns", "dyndns", "dynamic-ip"]
|
keywords = ["dns", "dyndns", "dynamic-ip"]
|
||||||
|
@ -17,30 +16,34 @@ multiple_crate_versions = "allow"
|
||||||
pedantic = { level = "warn", priority = -1 }
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7"
|
axum = "0.8"
|
||||||
axum-client-ip = "0.6"
|
axum-client-ip = "1.0"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
clap = { version = "4", features = ["derive", "env"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
clap-verbosity-flag = { version = "3", default-features = false, features = [
|
clap-verbosity-flag = { version = "3", default-features = false, features = [
|
||||||
"tracing",
|
"tracing",
|
||||||
] }
|
] }
|
||||||
http = "1"
|
http = "1"
|
||||||
|
humantime = "2.2.0"
|
||||||
miette = { version = "7", features = ["fancy"] }
|
miette = { version = "7", features = ["fancy"] }
|
||||||
ring = { version = "0.17", features = ["std"] }
|
ring = { version = "0.17", features = ["std"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] }
|
tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] }
|
||||||
tower-http = { version = "0.6.2", features = ["validate-request"] }
|
tower-http = { version = "0.6", features = ["validate-request"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1"
|
insta = { version = "=1.42.2", features = ["json"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
lto = true
|
lto = true
|
||||||
strip = true
|
strip = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
codegen-backend = "cranelift"
|
|
||||||
|
|
37
default.nix
Normal file
37
default.nix
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
pkgs ?
|
||||||
|
(builtins.getFlake (builtins.toString ./.)).inputs.nixpkgs.legacyPackages.${builtins.currentSystem},
|
||||||
|
lib ? pkgs.lib,
|
||||||
|
crane ? (builtins.getFlake (builtins.toString ./.)).inputs.crane,
|
||||||
|
pkgSrc ? ./.,
|
||||||
|
mold ? pkgs.mold,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
craneLib = crane.mkLib pkgs;
|
||||||
|
src = craneLib.cleanCargoSource pkgSrc;
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
inherit src;
|
||||||
|
strictDeps = true;
|
||||||
|
|
||||||
|
doCheck = false; # tests will be run in the `checks` derivation
|
||||||
|
NEXTEST_HIDE_PROGRESS_BAR = 1;
|
||||||
|
NEXTEST_FAILURE_OUTPUT = "immediate-final";
|
||||||
|
|
||||||
|
nativeBuildInputs = [ mold ];
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
license = lib.licenses.mit;
|
||||||
|
homepage = "https://github.com/jalil-salame/webnsupdate";
|
||||||
|
mainProgram = "webnsupdate";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
in
|
||||||
|
craneLib.buildPackage (
|
||||||
|
lib.mergeAttrsList [
|
||||||
|
commonArgs
|
||||||
|
{ inherit cargoArtifacts; }
|
||||||
|
]
|
||||||
|
)
|
|
@ -3,10 +3,18 @@
|
||||||
imports = [
|
imports = [
|
||||||
inputs.treefmt-nix.flakeModule
|
inputs.treefmt-nix.flakeModule
|
||||||
./package.nix
|
./package.nix
|
||||||
./module.nix
|
|
||||||
./tests.nix
|
./tests.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
flake.nixosModules =
|
||||||
|
let
|
||||||
|
webnsupdate = ../module.nix;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = webnsupdate;
|
||||||
|
inherit webnsupdate;
|
||||||
|
};
|
||||||
|
|
||||||
perSystem =
|
perSystem =
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,196 +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;
|
|
||||||
};
|
|
||||||
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
|
|
||||||
"--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 unsopported 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;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,13 +1,11 @@
|
||||||
{ withSystem, inputs, ... }:
|
{ inputs, ... }:
|
||||||
{
|
{
|
||||||
flake.overlays.default =
|
flake.overlays.default = final: prev: {
|
||||||
final: prev:
|
webnsupdate = prev.callPackage ../default.nix {
|
||||||
withSystem prev.stdenv.hostPlatform.system (
|
inherit (inputs) crane;
|
||||||
{ self', ... }:
|
pkgSrc = inputs.self;
|
||||||
{
|
};
|
||||||
inherit (self'.packages) webnsupdate;
|
};
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
perSystem =
|
perSystem =
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
|
@ -33,29 +31,22 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
webnsupdate = craneLib.buildPackage (
|
withArtifacts = lib.mergeAttrsList [
|
||||||
lib.mergeAttrsList [
|
commonArgs
|
||||||
commonArgs
|
{ inherit cargoArtifacts; }
|
||||||
{ inherit cargoArtifacts; }
|
];
|
||||||
]
|
webnsupdate = pkgs.callPackage ../default.nix {
|
||||||
);
|
inherit (inputs) crane;
|
||||||
|
pkgSrc = inputs.self;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
checks = {
|
checks = {
|
||||||
|
nextest = craneLib.cargoNextest withArtifacts;
|
||||||
clippy = craneLib.cargoClippy (
|
clippy = craneLib.cargoClippy (
|
||||||
lib.mergeAttrsList [
|
lib.mergeAttrsList [
|
||||||
commonArgs
|
withArtifacts
|
||||||
{
|
{ cargoClippyExtraArgs = "--all-targets -- --deny warnings"; }
|
||||||
inherit cargoArtifacts;
|
|
||||||
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
nextest = craneLib.cargoNextest (
|
|
||||||
lib.mergeAttrsList [
|
|
||||||
commonArgs
|
|
||||||
{ inherit cargoArtifacts; }
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -64,16 +55,6 @@
|
||||||
inherit webnsupdate;
|
inherit webnsupdate;
|
||||||
inherit (pkgs) git-cliff;
|
inherit (pkgs) git-cliff;
|
||||||
default = webnsupdate;
|
default = webnsupdate;
|
||||||
cargo-update = pkgs.writeShellApplication {
|
|
||||||
name = "cargo-update-lockfile";
|
|
||||||
runtimeInputs = with pkgs; [
|
|
||||||
cargo
|
|
||||||
gnused
|
|
||||||
];
|
|
||||||
text = ''
|
|
||||||
CARGO_TERM_COLOR=never cargo update 2>&1 | sed '/crates.io index/d' | tee -a cargo_update.log
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,138 +6,337 @@
|
||||||
checks =
|
checks =
|
||||||
let
|
let
|
||||||
testDomain = "webnstest.example";
|
testDomain = "webnstest.example";
|
||||||
dynamicZonesDir = "/var/lib/named/zones";
|
lastIPPath = "/var/lib/webnsupdate/last-ip.json";
|
||||||
zoneFile = pkgs.writeText "${testDomain}.zoneinfo" ''
|
|
||||||
$ORIGIN .
|
|
||||||
$TTL 60 ; 1 minute
|
|
||||||
${testDomain} IN SOA ns1.${testDomain}. admin.${testDomain}. (
|
|
||||||
1 ; serial
|
|
||||||
21600 ; refresh (6 hours)
|
|
||||||
3600 ; retry (1 hour)
|
|
||||||
604800 ; expire (1 week)
|
|
||||||
86400) ; negative caching TTL (1 day)
|
|
||||||
|
|
||||||
IN NS ns1.${testDomain}.
|
zoneFile = pkgs.writeText "${testDomain}.zoneinfo" ''
|
||||||
|
$TTL 600 ; 10 minutes
|
||||||
$ORIGIN ${testDomain}.
|
$ORIGIN ${testDomain}.
|
||||||
${testDomain}. IN A 127.0.0.1
|
@ IN SOA ns1.${testDomain}. admin.${testDomain}. (
|
||||||
${testDomain}. IN AAAA ::1
|
1 ; serial
|
||||||
ns1 IN A 127.0.0.1
|
6h ; refresh
|
||||||
ns1 IN AAAA ::1
|
1h ; retry
|
||||||
nsupdate IN A 127.0.0.1
|
1w ; expire
|
||||||
nsupdate IN AAAA ::1
|
1d) ; negative caching TTL
|
||||||
|
|
||||||
|
IN NS ns1.${testDomain}.
|
||||||
|
@ IN A 127.0.0.1
|
||||||
|
ns1 IN A 127.0.0.1
|
||||||
|
nsupdate IN A 127.0.0.1
|
||||||
|
@ IN AAAA ::1
|
||||||
|
ns1 IN AAAA ::1
|
||||||
|
nsupdate IN AAAA ::1
|
||||||
'';
|
'';
|
||||||
|
|
||||||
webnsupdate-machine = {
|
bindDynamicZone =
|
||||||
imports = [ self.nixosModules.webnsupdate ];
|
{ config, ... }:
|
||||||
|
let
|
||||||
config = {
|
bindCfg = config.services.bind;
|
||||||
environment.systemPackages = [
|
bindData = bindCfg.directory;
|
||||||
pkgs.dig
|
dynamicZonesDir = "${bindData}/zones";
|
||||||
pkgs.curl
|
in
|
||||||
];
|
{
|
||||||
|
services.bind.zones.${testDomain} = {
|
||||||
services = {
|
master = true;
|
||||||
webnsupdate = {
|
file = "${dynamicZonesDir}/${testDomain}";
|
||||||
enable = true;
|
extraConfig = ''
|
||||||
bindIp = "127.0.0.1";
|
allow-update { key rndc-key; };
|
||||||
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}.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
bind = {
|
|
||||||
enable = true;
|
|
||||||
zones.${testDomain} = {
|
|
||||||
master = true;
|
|
||||||
file = "${dynamicZonesDir}/${testDomain}";
|
|
||||||
extraConfig = ''
|
|
||||||
allow-update { key rndc-key; };
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.bind.preStart = ''
|
systemd.services.bind.preStart = ''
|
||||||
# shellcheck disable=SC2211,SC1127
|
# shellcheck disable=SC2211,SC1127
|
||||||
rm -f ${dynamicZonesDir}/* # reset dynamic zones
|
rm -f ${dynamicZonesDir}/* # reset dynamic zones
|
||||||
|
|
||||||
${pkgs.coreutils}/bin/mkdir -m 0755 -p ${dynamicZonesDir}
|
# create a dynamic zones dir
|
||||||
chown "named" ${dynamicZonesDir}
|
mkdir -m 0755 -p ${dynamicZonesDir}
|
||||||
chown "named" /var/lib/named
|
|
||||||
|
|
||||||
# copy dynamic zone's file to the dynamic zones dir
|
# copy dynamic zone's file to the dynamic zones dir
|
||||||
cp ${zoneFile} ${dynamicZonesDir}/${testDomain}
|
cp ${zoneFile} ${dynamicZonesDir}/${testDomain}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
webnsupdate-ipv4-machine =
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
bindDynamicZone
|
||||||
|
self.nixosModules.webnsupdate
|
||||||
|
];
|
||||||
|
|
||||||
|
config = {
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.dig
|
||||||
|
pkgs.curl
|
||||||
|
];
|
||||||
|
|
||||||
|
services = {
|
||||||
|
bind.enable = true;
|
||||||
|
|
||||||
|
webnsupdate = {
|
||||||
|
enable = true;
|
||||||
|
package = self'.packages.webnsupdate;
|
||||||
|
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}."
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
webnsupdate-ipv6-machine = {
|
||||||
|
imports = [
|
||||||
|
webnsupdate-ipv4-machine
|
||||||
|
];
|
||||||
|
|
||||||
|
config.services.webnsupdate.settings.address = "[::1]:5353";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
webnsupdate-nginx-machine =
|
||||||
|
{ lib, config, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
webnsupdate-ipv4-machine
|
||||||
|
];
|
||||||
|
|
||||||
|
config.services = {
|
||||||
|
# Use default IP Source
|
||||||
|
webnsupdate.settings.ip_source = "RightmostXForwardedFor";
|
||||||
|
|
||||||
|
nginx = {
|
||||||
|
enable = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
|
||||||
|
virtualHosts.webnsupdate.locations."/".proxyPass =
|
||||||
|
"http://${config.services.webnsupdate.settings.address}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
webnsupdate-ipv4-only-machine = {
|
||||||
|
imports = [ webnsupdate-nginx-machine ];
|
||||||
|
config.services.webnsupdate.settings.ip_type = "Ipv4Only";
|
||||||
|
};
|
||||||
|
|
||||||
|
webnsupdate-ipv6-only-machine = {
|
||||||
|
imports = [ webnsupdate-nginx-machine ];
|
||||||
|
config.services.webnsupdate.settings.ip_type = "Ipv6Only";
|
||||||
|
};
|
||||||
|
|
||||||
|
# "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=}")
|
||||||
|
|
||||||
|
CURL: str = "curl --fail --no-progress-meter --show-error"
|
||||||
|
|
||||||
|
machine.start(allow_reboot=True)
|
||||||
|
machine.wait_for_unit("bind.service")
|
||||||
|
machine.wait_for_unit("webnsupdate.service")
|
||||||
|
|
||||||
|
STATIC_DOMAINS: list[str] = ["${testDomain}", "ns1.${testDomain}", "nsupdate.${testDomain}"]
|
||||||
|
DYNAMIC_DOMAINS: list[str] = ["test1.${testDomain}", "test2.${testDomain}", "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}'"
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
if IPV6:
|
||||||
|
domain_available(domain, "AAAA", "::1")
|
||||||
|
elif IPV4 and EXCLUSIVE:
|
||||||
|
domain_missing(domain, "AAAA")
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
module-test = pkgs.testers.runNixOSTest {
|
module-ipv4-test = pkgs.testers.nixosTest {
|
||||||
name = "webnsupdate-module";
|
name = "webnsupdate-ipv4-module";
|
||||||
nodes.machine = webnsupdate-machine;
|
nodes.machine = webnsupdate-ipv4-machine;
|
||||||
testScript = ''
|
testScript = testTemplate { ipv4 = true; };
|
||||||
machine.start(allow_reboot=True)
|
};
|
||||||
machine.wait_for_unit("webnsupdate.service")
|
module-ipv6-test = pkgs.testers.nixosTest {
|
||||||
|
name = "webnsupdate-ipv6-module";
|
||||||
# ensure base DNS records area available
|
nodes.machine = webnsupdate-ipv6-machine;
|
||||||
with subtest("query base DNS records"):
|
testScript = testTemplate { ipv6 = true; };
|
||||||
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
|
};
|
||||||
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
|
module-nginx-test = pkgs.testers.nixosTest {
|
||||||
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
|
name = "webnsupdate-nginx-module";
|
||||||
|
nodes.machine = webnsupdate-nginx-machine;
|
||||||
# ensure webnsupdate managed records are missing
|
testScript = testTemplate {
|
||||||
with subtest("query webnsupdate DNS records (fail)"):
|
ipv4 = true;
|
||||||
machine.fail("dig @127.0.0.1 test1.${testDomain} | grep ^test1.${testDomain}")
|
ipv6 = true;
|
||||||
machine.fail("dig @127.0.0.1 test2.${testDomain} | grep ^test2.${testDomain}")
|
nginx = true;
|
||||||
machine.fail("dig @127.0.0.1 test3.${testDomain} | grep ^test3.${testDomain}")
|
};
|
||||||
|
};
|
||||||
with subtest("update webnsupdate DNS records (invalid auth)"):
|
module-ipv4-only-test = pkgs.testers.nixosTest {
|
||||||
machine.fail("curl --fail --silent -u test1:test1 -X GET http://localhost:5353/update")
|
name = "webnsupdate-ipv4-only-module";
|
||||||
machine.fail("cat /var/lib/webnsupdate/last-ip") # no last-ip set yet
|
nodes.machine = webnsupdate-ipv4-only-machine;
|
||||||
|
testScript = testTemplate {
|
||||||
# ensure webnsupdate managed records are missing
|
ipv4 = true;
|
||||||
with subtest("query webnsupdate DNS records (fail)"):
|
nginx = true;
|
||||||
machine.fail("dig @127.0.0.1 test1.${testDomain} | grep ^test1.${testDomain}")
|
exclusive = true;
|
||||||
machine.fail("dig @127.0.0.1 test2.${testDomain} | grep ^test2.${testDomain}")
|
};
|
||||||
machine.fail("dig @127.0.0.1 test3.${testDomain} | grep ^test3.${testDomain}")
|
};
|
||||||
|
module-ipv6-only-test = pkgs.testers.nixosTest {
|
||||||
with subtest("update webnsupdate DNS records (valid auth)"):
|
name = "webnsupdate-ipv6-only-module";
|
||||||
machine.succeed("curl --fail --silent -u test:test -X GET http://localhost:5353/update")
|
nodes.machine = webnsupdate-ipv6-only-machine;
|
||||||
machine.succeed("cat /var/lib/webnsupdate/last-ip")
|
testScript = testTemplate {
|
||||||
|
ipv6 = true;
|
||||||
# ensure webnsupdate managed records are available
|
nginx = true;
|
||||||
with subtest("query webnsupdate DNS records (succeed)"):
|
exclusive = true;
|
||||||
machine.succeed("dig @127.0.0.1 test1.${testDomain} | grep ^test1.${testDomain}")
|
};
|
||||||
machine.succeed("dig @127.0.0.1 test2.${testDomain} | grep ^test2.${testDomain}")
|
|
||||||
machine.succeed("dig @127.0.0.1 test3.${testDomain} | grep ^test3.${testDomain}")
|
|
||||||
|
|
||||||
machine.reboot()
|
|
||||||
machine.succeed("cat /var/lib/webnsupdate/last-ip")
|
|
||||||
machine.wait_for_unit("webnsupdate.service")
|
|
||||||
machine.succeed("cat /var/lib/webnsupdate/last-ip")
|
|
||||||
|
|
||||||
# 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} | grep ^test1.${testDomain}")
|
|
||||||
machine.succeed("dig @127.0.0.1 test2.${testDomain} | grep ^test2.${testDomain}")
|
|
||||||
machine.succeed("dig @127.0.0.1 test3.${testDomain} | grep ^test3.${testDomain}")
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
29
flake.lock
generated
29
flake.lock
generated
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
"crane": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1734808813,
|
"lastModified": 1743908961,
|
||||||
"narHash": "sha256-3aH/0Y6ajIlfy7j52FGZ+s4icVX0oHhqBzRdlOeztqg=",
|
"narHash": "sha256-e1idZdpnnHWuosI3KsBgAgrhMR05T2oqskXCmNzGPq0=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "72e2d02dbac80c8c86bf6bf3e785536acf8ee926",
|
"rev": "80ceeec0dc94ef967c371dcdc56adb280328f591",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -22,11 +22,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1733312601,
|
"lastModified": 1743550720,
|
||||||
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
|
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
|
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -37,17 +37,18 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1734424634,
|
"lastModified": 1744098102,
|
||||||
"narHash": "sha256-cHar1vqHOOyC7f1+tVycPoWTfKIaqkoe1Q6TnKzuti4=",
|
"narHash": "sha256-tzCdyIJj9AjysC3OuKA+tMD/kDEDAF9mICPDU7ix0JA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d3c42f187194c26d9f0309a8ecc469d6c878ce33",
|
"rev": "c8cd81426f45942bb2906d5ed2fe21d2f19d95b7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixos-unstable",
|
||||||
"type": "indirect"
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
|
@ -81,11 +82,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1734704479,
|
"lastModified": 1743748085,
|
||||||
"narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=",
|
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f",
|
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
url = "github:hercules-ci/flake-parts";
|
url = "github:hercules-ci/flake-parts";
|
||||||
inputs.nixpkgs-lib.follows = "nixpkgs";
|
inputs.nixpkgs-lib.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
systems.url = "github:nix-systems/default";
|
systems.url = "github:nix-systems/default";
|
||||||
treefmt-nix = {
|
treefmt-nix = {
|
||||||
url = "github:numtide/treefmt-nix";
|
url = "github:numtide/treefmt-nix";
|
||||||
|
|
2
justfile
Normal file
2
justfile
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
changelog version:
|
||||||
|
git cliff --unreleased --prepend=CHANGELOG.md --tag='{{ version }}'
|
162
module.nix
Normal file
162
module.nix
Normal 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
253
src/config.rs
Normal 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"
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
}
|
611
src/main.rs
611
src/main.rs
|
@ -1,21 +1,27 @@
|
||||||
use std::{
|
use std::{
|
||||||
io::ErrorKind,
|
io::ErrorKind,
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{extract::State, routing::get, Router};
|
use axum::{
|
||||||
use axum_client_ip::{SecureClientIp, SecureClientIpSource};
|
extract::{Query, State},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use axum_client_ip::ClientIp;
|
||||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_verbosity_flag::Verbosity;
|
use clap_verbosity_flag::Verbosity;
|
||||||
|
use config::Config;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use miette::{bail, ensure, Context, IntoDiagnostic, Result};
|
use miette::{bail, ensure, Context, IntoDiagnostic, Result};
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod config;
|
||||||
mod nsupdate;
|
mod nsupdate;
|
||||||
mod password;
|
mod password;
|
||||||
mod records;
|
mod records;
|
||||||
|
@ -26,77 +32,54 @@ const DEFAULT_SALT: &str = "UpdateMyDNS";
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
struct Opts {
|
struct Opts {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
verbosity: Verbosity<clap_verbosity_flag::WarnLevel>,
|
verbosity: Verbosity<clap_verbosity_flag::InfoLevel>,
|
||||||
|
|
||||||
/// Ip address of the server
|
|
||||||
#[arg(long, default_value = "127.0.0.1")]
|
|
||||||
address: IpAddr,
|
|
||||||
|
|
||||||
/// Port of the server
|
|
||||||
#[arg(long, default_value_t = 5353)]
|
|
||||||
port: u16,
|
|
||||||
|
|
||||||
/// File containing password to match against
|
|
||||||
///
|
|
||||||
/// Should be of the format `username:password` and contain a single password
|
|
||||||
#[arg(long)]
|
|
||||||
password_file: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Salt to get more unique hashed passwords and prevent table based attacks
|
|
||||||
#[arg(long, default_value = DEFAULT_SALT)]
|
|
||||||
salt: String,
|
|
||||||
|
|
||||||
/// Time To Live (in seconds) to set on the DNS records
|
|
||||||
#[arg(long, default_value_t = DEFAULT_TTL.as_secs())]
|
|
||||||
ttl: u64,
|
|
||||||
|
|
||||||
/// Data directory
|
/// Data directory
|
||||||
#[arg(long, default_value = ".")]
|
#[arg(long, env, default_value = ".")]
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
|
|
||||||
/// File containing the records that should be updated when an update request is made
|
|
||||||
///
|
|
||||||
/// There should be one record per line:
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// example.com.
|
|
||||||
/// mail.example.com.
|
|
||||||
/// ```
|
|
||||||
#[arg(long)]
|
|
||||||
records: PathBuf,
|
|
||||||
|
|
||||||
/// Keyfile `nsupdate` should use
|
|
||||||
///
|
|
||||||
/// If specified, then `webnsupdate` must have read access to the file
|
|
||||||
#[arg(long)]
|
|
||||||
key_file: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Allow not setting a password
|
/// Allow not setting a password
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
insecure: bool,
|
insecure: bool,
|
||||||
|
|
||||||
/// Set client IP source
|
#[clap(flatten)]
|
||||||
///
|
config_or_command: ConfigOrCommand,
|
||||||
/// see: <https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html>
|
}
|
||||||
#[clap(long, default_value = "RightmostXForwardedFor")]
|
|
||||||
ip_source: SecureClientIpSource,
|
#[derive(clap::Args, Debug)]
|
||||||
|
#[group(multiple = false)]
|
||||||
|
struct ConfigOrCommand {
|
||||||
|
/// Path to the configuration file
|
||||||
|
#[arg(long, short)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
subcommand: Option<Cmd>,
|
subcommand: Option<Cmd>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConfigOrCommand {
|
||||||
|
pub fn take(&mut self) -> (Option<PathBuf>, Option<Cmd>) {
|
||||||
|
(self.config.take(), self.subcommand.take())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
Mkpasswd(password::Mkpasswd),
|
Mkpasswd(password::Mkpasswd),
|
||||||
/// Verify the records file
|
/// Verify the configuration file
|
||||||
Verify,
|
Verify {
|
||||||
|
/// Path to the configuration file
|
||||||
|
config: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
pub fn process(self, args: &Opts) -> Result<()> {
|
pub fn process(self, args: &Opts) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Cmd::Mkpasswd(mkpasswd) => mkpasswd.process(args),
|
Cmd::Mkpasswd(mkpasswd) => mkpasswd.process(args),
|
||||||
Cmd::Verify => 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,35 +97,86 @@ struct AppState<'a> {
|
||||||
|
|
||||||
/// The file where the last IP is stored
|
/// The file where the last IP is stored
|
||||||
ip_file: &'a Path,
|
ip_file: &'a Path,
|
||||||
|
|
||||||
|
/// Last recorded IPs
|
||||||
|
last_ips: std::sync::Arc<tokio::sync::Mutex<SavedIPs>>,
|
||||||
|
|
||||||
|
/// The IP type for which to allow updates
|
||||||
|
ip_type: config::IpType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct SavedIPs {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
ipv4: Option<Ipv4Addr>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
ipv6: Option<Ipv6Addr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SavedIPs {
|
||||||
|
fn update(&mut self, ip: IpAddr) {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ipv4_addr) => self.ipv4 = Some(ipv4_addr),
|
||||||
|
IpAddr::V6(ipv6_addr) => self.ipv6 = Some(ipv6_addr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ips(&self) -> impl Iterator<Item = IpAddr> {
|
||||||
|
self.ipv4
|
||||||
|
.map(IpAddr::V4)
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.ipv6.map(IpAddr::V6))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_str(data: &str) -> miette::Result<Self> {
|
||||||
|
match data.parse::<IpAddr>() {
|
||||||
|
// Old format
|
||||||
|
Ok(IpAddr::V4(ipv4)) => Ok(Self {
|
||||||
|
ipv4: Some(ipv4),
|
||||||
|
ipv6: None,
|
||||||
|
}),
|
||||||
|
Ok(IpAddr::V6(ipv6)) => Ok(Self {
|
||||||
|
ipv4: None,
|
||||||
|
ipv6: Some(ipv6),
|
||||||
|
}),
|
||||||
|
Err(_) => serde_json::from_str(data).into_diagnostic(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState<'static> {
|
impl AppState<'static> {
|
||||||
fn from_args(args: &Opts) -> miette::Result<Self> {
|
fn from_args(args: &Opts, config: &config::Config) -> miette::Result<Self> {
|
||||||
let Opts {
|
let Opts {
|
||||||
verbosity: _,
|
verbosity: _,
|
||||||
address: _,
|
|
||||||
port: _,
|
|
||||||
password_file: _,
|
|
||||||
data_dir,
|
data_dir,
|
||||||
key_file,
|
|
||||||
insecure,
|
insecure,
|
||||||
subcommand: _,
|
config_or_command: _,
|
||||||
records,
|
|
||||||
salt: _,
|
|
||||||
ttl,
|
|
||||||
ip_source: _,
|
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
// Set state
|
let config::Records {
|
||||||
let ttl = Duration::from_secs(*ttl);
|
ttl,
|
||||||
|
records,
|
||||||
|
client_id: _,
|
||||||
|
router_domain: _,
|
||||||
|
ip_source: _,
|
||||||
|
ip_type,
|
||||||
|
key_file,
|
||||||
|
} = &config.records;
|
||||||
|
|
||||||
// Use last registered IP address if available
|
// Use last registered IP address if available
|
||||||
let ip_file = data_dir.join("last-ip");
|
let ip_file = Box::leak(data_dir.join("last-ip.json").into_boxed_path());
|
||||||
|
|
||||||
|
// Leak DNS records
|
||||||
|
let records: &[&str] = &*Vec::leak(
|
||||||
|
records
|
||||||
|
.iter()
|
||||||
|
.map(|record| &*Box::leak(record.clone()))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
ttl,
|
ttl: **ttl,
|
||||||
// Load DNS records
|
records,
|
||||||
records: records::load_no_verify(records)?,
|
|
||||||
// Load keyfile
|
// Load keyfile
|
||||||
key_file: key_file
|
key_file: key_file
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
@ -155,7 +189,11 @@ impl AppState<'static> {
|
||||||
Ok(&*Box::leak(path.into()))
|
Ok(&*Box::leak(path.into()))
|
||||||
})
|
})
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
ip_file: Box::leak(ip_file.into_boxed_path()),
|
ip_file,
|
||||||
|
ip_type: *ip_type,
|
||||||
|
last_ips: std::sync::Arc::new(tokio::sync::Mutex::new(
|
||||||
|
load_ip(ip_file)?.unwrap_or_default(),
|
||||||
|
)),
|
||||||
};
|
};
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
|
@ -167,7 +205,7 @@ impl AppState<'static> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_ip(path: &Path) -> Result<Option<IpAddr>> {
|
fn load_ip(path: &Path) -> Result<Option<SavedIPs>> {
|
||||||
debug!("loading last IP from {}", path.display());
|
debug!("loading last IP from {}", path.display());
|
||||||
let data = match std::fs::read_to_string(path) {
|
let data = match std::fs::read_to_string(path) {
|
||||||
Ok(ip) => ip,
|
Ok(ip) => ip,
|
||||||
|
@ -181,13 +219,43 @@ fn load_ip(path: &Path) -> Result<Option<IpAddr>> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some(
|
SavedIPs::from_str(&data)
|
||||||
data.parse()
|
.wrap_err_with(|| format!("failed to load last ip address from {}", path.display()))
|
||||||
.into_diagnostic()
|
.map(Some)
|
||||||
.wrap_err("failed to parse last ip address")?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<()> {
|
fn main() -> Result<()> {
|
||||||
// set panic hook to pretty print with miette's formatter
|
// set panic hook to pretty print with miette's formatter
|
||||||
miette::set_panic_hook();
|
miette::set_panic_hook();
|
||||||
|
@ -204,39 +272,44 @@ fn main() -> Result<()> {
|
||||||
.from_env_lossy(),
|
.from_env_lossy(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
tracing::subscriber::set_global_default(subscriber)
|
tracing::subscriber::set_global_default(subscriber)
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.wrap_err("setting global tracing subscriber")?;
|
.wrap_err("failed to set global tracing subscriber")?;
|
||||||
|
|
||||||
debug!("{args:?}");
|
debug!("{args:?}");
|
||||||
|
|
||||||
// process subcommand
|
let config = match args.config_or_command.take() {
|
||||||
if let Some(cmd) = args.subcommand.take() {
|
// process subcommand
|
||||||
return cmd.process(&args);
|
(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
|
// Initialize state
|
||||||
let state = AppState::from_args(&args)?;
|
let state = AppState::from_args(&args, &config)?;
|
||||||
|
|
||||||
let Opts {
|
let Opts {
|
||||||
verbosity: _,
|
verbosity: _,
|
||||||
address: ip,
|
|
||||||
port,
|
|
||||||
password_file,
|
|
||||||
data_dir: _,
|
data_dir: _,
|
||||||
key_file: _,
|
|
||||||
insecure,
|
insecure,
|
||||||
subcommand: _,
|
config_or_command: _,
|
||||||
records: _,
|
|
||||||
salt,
|
|
||||||
ttl: _,
|
|
||||||
ip_source,
|
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
info!("checking environment");
|
info!("checking environment");
|
||||||
|
|
||||||
// Load password hash
|
// Load password hash
|
||||||
let password_hash = password_file
|
let password_hash = config
|
||||||
|
.password
|
||||||
|
.password_file
|
||||||
.map(|path| -> miette::Result<_> {
|
.map(|path| -> miette::Result<_> {
|
||||||
let path = path.as_path();
|
let path = path.as_path();
|
||||||
let pass = std::fs::read_to_string(path).into_diagnostic()?;
|
let pass = std::fs::read_to_string(path).into_diagnostic()?;
|
||||||
|
@ -249,7 +322,8 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
Ok(pass)
|
Ok(pass)
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()
|
||||||
|
.wrap_err("failed to load password hash")?;
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
password_hash.is_some() || insecure,
|
password_hash.is_some() || insecure,
|
||||||
|
@ -263,46 +337,54 @@ fn main() -> Result<()> {
|
||||||
.wrap_err("failed to start the tokio runtime")?;
|
.wrap_err("failed to start the tokio runtime")?;
|
||||||
|
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
// Load previous IP and update DNS record to point to it (if available)
|
// Update DNS record with previous IPs (if available)
|
||||||
match load_ip(state.ip_file) {
|
let ips = state.last_ips.lock().await.clone();
|
||||||
Ok(Some(ip)) => {
|
|
||||||
match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await {
|
let mut actions = ips
|
||||||
Ok(status) => {
|
.ips()
|
||||||
if !status.success() {
|
.filter(|ip| config.records.ip_type.valid_for_type(*ip))
|
||||||
error!("nsupdate failed: code {status}");
|
.flat_map(|ip| nsupdate::Action::from_records(ip, state.ttl, state.records))
|
||||||
bail!("nsupdate returned with code {status}");
|
.peekable();
|
||||||
}
|
|
||||||
}
|
if actions.peek().is_some() {
|
||||||
Err(err) => {
|
match nsupdate::nsupdate(state.key_file, actions).await {
|
||||||
error!("Failed to update records with previous IP: {err}");
|
Ok(status) => {
|
||||||
return Err(err)
|
if !status.success() {
|
||||||
.into_diagnostic()
|
error!("nsupdate failed: code {status}");
|
||||||
.wrap_err("failed to update records with previous IP");
|
bail!("nsupdate returned with code {status}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to update records with previous IP: {err}");
|
||||||
|
return Err(err)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to update records with previous IP");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => info!("No previous IP address set"),
|
}
|
||||||
|
|
||||||
Err(err) => error!("Failed to load last ip address: {err}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create services
|
// Create services
|
||||||
let app = Router::new().route("/update", get(update_records));
|
let app = Router::new().route("/update", get(update_records));
|
||||||
// if a password is provided, validate it
|
// if a password is provided, validate it
|
||||||
let app = if let Some(pass) = password_hash {
|
let app = if let Some(pass) = password_hash {
|
||||||
app.layer(auth::layer(Box::leak(pass), String::leak(salt)))
|
app.layer(auth::layer(
|
||||||
|
Box::leak(pass),
|
||||||
|
Box::leak(config.password.salt),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
.layer(ip_source.into_extension())
|
.layer(config.records.ip_source.into_extension())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
let config::Server { address } = config.server;
|
||||||
|
|
||||||
// Start services
|
// Start services
|
||||||
info!("starting listener on {ip}:{port}");
|
info!("starting listener on {address}");
|
||||||
let listener = tokio::net::TcpListener::bind(SocketAddr::new(ip, port))
|
let listener = tokio::net::TcpListener::bind(address)
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
info!("listening on {ip}:{port}");
|
info!("listening on {address}");
|
||||||
axum::serve(
|
axum::serve(
|
||||||
listener,
|
listener,
|
||||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||||
|
@ -310,37 +392,318 @@ fn main() -> Result<()> {
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
})
|
})
|
||||||
|
.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"))]
|
#[tracing::instrument(skip(state), level = "trace", ret(level = "info"))]
|
||||||
async fn update_records(
|
async fn update_records(
|
||||||
State(state): State<AppState<'static>>,
|
State(state): State<AppState<'static>>,
|
||||||
SecureClientIp(ip): SecureClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
|
Query(update_params): Query<FritzBoxUpdateParams>,
|
||||||
) -> axum::response::Result<&'static str> {
|
) -> axum::response::Result<&'static str> {
|
||||||
info!("accepted update from {ip}");
|
info!("accepted update from {ip}");
|
||||||
match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await {
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() => {
|
Ok(status) if status.success() => {
|
||||||
|
let ips = {
|
||||||
|
// Update state
|
||||||
|
let mut ips = state.last_ips.lock().await;
|
||||||
|
ips.update(ip);
|
||||||
|
ips.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let ip_file = state.ip_file;
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
info!("updating last ip to {ip}");
|
info!("updating last ips to {ips:?}");
|
||||||
if let Err(err) = std::fs::write(state.ip_file, format!("{ip}")) {
|
let data = serde_json::to_vec(&ips).expect("invalid serialization impl");
|
||||||
|
if let Err(err) = std::fs::write(ip_file, data) {
|
||||||
error!("Failed to update last IP: {err}");
|
error!("Failed to update last IP: {err}");
|
||||||
}
|
}
|
||||||
info!("updated last ip to {ip}");
|
info!("updated last ips to {ips:?}");
|
||||||
});
|
});
|
||||||
Ok("successful update")
|
|
||||||
|
Ok("Successfully updated IP of records!\n")
|
||||||
}
|
}
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
error!("nsupdate failed with code {status}");
|
error!("nsupdate failed with code {status}");
|
||||||
Err((
|
Err((
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"nsupdate failed, check server logs",
|
"nsupdate failed, check server logs\n",
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
Err(error) => Err((
|
Err(error) => Err((
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("failed to update records: {error}"),
|
format!("failed to update records: {error}\n"),
|
||||||
)
|
)
|
||||||
.into()),
|
.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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
105
src/nsupdate.rs
105
src/nsupdate.rs
|
@ -9,12 +9,51 @@ use std::{
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tracing::{debug, warn};
|
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(
|
pub async fn nsupdate(
|
||||||
ip: IpAddr,
|
|
||||||
ttl: Duration,
|
|
||||||
key_file: Option<&Path>,
|
key_file: Option<&Path>,
|
||||||
records: &[&str],
|
actions: impl IntoIterator<Item = Action<'_>>,
|
||||||
) -> std::io::Result<ExitStatus> {
|
) -> std::io::Result<ExitStatus> {
|
||||||
let mut cmd = tokio::process::Command::new("nsupdate");
|
let mut cmd = tokio::process::Command::new("nsupdate");
|
||||||
if let Some(key_file) = key_file {
|
if let Some(key_file) = key_file {
|
||||||
|
@ -27,10 +66,13 @@ pub async fn nsupdate(
|
||||||
.inspect_err(|err| warn!("failed to spawn child: {err}"))?;
|
.inspect_err(|err| warn!("failed to spawn child: {err}"))?;
|
||||||
let mut stdin = child.stdin.take().expect("stdin not present");
|
let mut stdin = child.stdin.take().expect("stdin not present");
|
||||||
debug!("sending update request");
|
debug!("sending update request");
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
update_ns_records(&mut buf, actions).unwrap();
|
||||||
stdin
|
stdin
|
||||||
.write_all(update_ns_records(ip, ttl, records).as_bytes())
|
.write_all(&buf)
|
||||||
.await
|
.await
|
||||||
.inspect_err(|err| warn!("failed to write to the stdin of nsupdate: {err}"))?;
|
.inspect_err(|err| warn!("failed to write to the stdin of nsupdate: {err}"))?;
|
||||||
|
|
||||||
debug!("closing stdin");
|
debug!("closing stdin");
|
||||||
stdin
|
stdin
|
||||||
.shutdown()
|
.shutdown()
|
||||||
|
@ -43,21 +85,16 @@ pub async fn nsupdate(
|
||||||
.inspect_err(|err| warn!("failed to wait for child: {err}"))
|
.inspect_err(|err| warn!("failed to wait for child: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_ns_records(ip: IpAddr, ttl: Duration, records: &[&str]) -> String {
|
fn update_ns_records<'a>(
|
||||||
use std::fmt::Write;
|
mut buf: impl std::io::Write,
|
||||||
let ttl_s: u64 = ttl.as_secs();
|
actions: impl IntoIterator<Item = Action<'a>>,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
let rec_type = match ip {
|
writeln!(buf, "server 127.0.0.1")?;
|
||||||
IpAddr::V4(_) => "A",
|
for action in actions {
|
||||||
IpAddr::V6(_) => "AAAA",
|
write!(buf, "{action}")?;
|
||||||
};
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
writeln!(cmds, "send\nquit").unwrap();
|
writeln!(buf, "send")?;
|
||||||
cmds
|
writeln!(buf, "quit")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -66,17 +103,21 @@ mod test {
|
||||||
|
|
||||||
use insta::assert_snapshot;
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
use super::update_ns_records;
|
use super::{update_ns_records, Action};
|
||||||
use crate::DEFAULT_TTL;
|
use crate::DEFAULT_TTL;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn expected_update_string_A() {
|
fn expected_update_string_A() {
|
||||||
assert_snapshot!(update_ns_records(
|
let mut buf = Vec::new();
|
||||||
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
let actions = Action::from_records(
|
||||||
DEFAULT_TTL,
|
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||||
&["example.com.", "example.org.", "example.net."],
|
DEFAULT_TTL,
|
||||||
), @r###"
|
&["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
|
server 127.0.0.1
|
||||||
update delete example.com. 60 IN A
|
update delete example.com. 60 IN A
|
||||||
update add example.com. 60 IN A 127.0.0.1
|
update add example.com. 60 IN A 127.0.0.1
|
||||||
|
@ -92,11 +133,15 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn expected_update_string_AAAA() {
|
fn expected_update_string_AAAA() {
|
||||||
assert_snapshot!(update_ns_records(
|
let mut buf = Vec::new();
|
||||||
IpAddr::V6(Ipv6Addr::LOCALHOST),
|
let actions = Action::from_records(
|
||||||
DEFAULT_TTL,
|
IpAddr::V6(Ipv6Addr::LOCALHOST),
|
||||||
&["example.com.", "example.org.", "example.net."],
|
DEFAULT_TTL,
|
||||||
), @r###"
|
&["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
|
server 127.0.0.1
|
||||||
update delete example.com. 60 IN AAAA
|
update delete example.com. 60 IN AAAA
|
||||||
update add example.com. 60 IN AAAA ::1
|
update add example.com. 60 IN AAAA ::1
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
//! records
|
//! records
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::path::Path;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use miette::{Context, IntoDiagnostic, Result};
|
use miette::{Context, IntoDiagnostic, Result};
|
||||||
|
@ -20,11 +20,18 @@ pub struct Mkpasswd {
|
||||||
|
|
||||||
/// The password
|
/// The password
|
||||||
password: String,
|
password: String,
|
||||||
|
|
||||||
|
/// An application specific value
|
||||||
|
#[arg(long, default_value = crate::DEFAULT_SALT)]
|
||||||
|
salt: String,
|
||||||
|
|
||||||
|
/// The file to write the password to
|
||||||
|
password_file: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mkpasswd {
|
impl Mkpasswd {
|
||||||
pub fn process(self, args: &crate::Opts) -> Result<()> {
|
pub fn process(self, _args: &crate::Opts) -> Result<()> {
|
||||||
mkpasswd(self, args.password_file.as_deref(), &args.salt)
|
mkpasswd(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,13 +52,16 @@ pub fn hash_identity(username: &str, password: &str, salt: &str) -> Digest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mkpasswd(
|
pub fn mkpasswd(
|
||||||
Mkpasswd { username, password }: Mkpasswd,
|
Mkpasswd {
|
||||||
password_file: Option<&Path>,
|
username,
|
||||||
salt: &str,
|
password,
|
||||||
|
salt,
|
||||||
|
password_file,
|
||||||
|
}: Mkpasswd,
|
||||||
) -> miette::Result<()> {
|
) -> miette::Result<()> {
|
||||||
let hash = hash_identity(&username, &password, salt);
|
let hash = hash_identity(&username, &password, &salt);
|
||||||
let encoded = BASE64_URL_SAFE_NO_PAD.encode(hash.as_ref());
|
let encoded = BASE64_URL_SAFE_NO_PAD.encode(hash.as_ref());
|
||||||
let Some(path) = password_file else {
|
let Some(path) = password_file.as_deref() else {
|
||||||
println!("{encoded}");
|
println!("{encoded}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
136
src/records.rs
136
src/records.rs
|
@ -1,52 +1,9 @@
|
||||||
//! Deal with the DNS records
|
//! Deal with the DNS records
|
||||||
|
|
||||||
use std::path::Path;
|
use miette::{ensure, miette, LabeledSpan, Result};
|
||||||
|
|
||||||
use miette::{ensure, miette, Context, IntoDiagnostic, LabeledSpan, NamedSource, Result};
|
pub fn validate_record_str(record: &str) -> Result<()> {
|
||||||
|
validate_line(0, record).map_err(|err| err.with_source_code(String::from(record)))
|
||||||
/// Loads and verifies the records from a file
|
|
||||||
pub fn load(path: &Path) -> Result<()> {
|
|
||||||
let records = std::fs::read_to_string(path)
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err_with(|| format!("failed to read records from {}", path.display()))?;
|
|
||||||
|
|
||||||
verify(&records, path)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load records without verifying them
|
|
||||||
pub fn load_no_verify(path: &Path) -> Result<&'static [&'static str]> {
|
|
||||||
let records = std::fs::read_to_string(path)
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err_with(|| format!("failed to read records from {}", path.display()))?;
|
|
||||||
|
|
||||||
if let Err(err) = verify(&records, path) {
|
|
||||||
tracing::error!("Failed to verify records: {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// leak memory: we only do this here and it prevents a bunch of allocations
|
|
||||||
let records: &str = records.leak();
|
|
||||||
let records: Box<[&str]> = records.lines().collect();
|
|
||||||
|
|
||||||
Ok(Box::leak(records))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies that a list of records is valid
|
|
||||||
pub fn verify(data: &str, path: &Path) -> Result<()> {
|
|
||||||
let mut offset = 0usize;
|
|
||||||
for line in data.lines() {
|
|
||||||
validate_line(offset, line).map_err(|err| {
|
|
||||||
err.with_source_code(NamedSource::new(
|
|
||||||
path.display().to_string(),
|
|
||||||
data.to_string(),
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
offset += line.len() + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_line(offset: usize, line: &str) -> Result<()> {
|
fn validate_line(offset: usize, line: &str) -> Result<()> {
|
||||||
|
@ -156,7 +113,7 @@ fn validate_octet(offset: usize, octet: u8) -> Result<()> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::records::verify;
|
use crate::records::validate_record_str;
|
||||||
|
|
||||||
macro_rules! assert_miette_snapshot {
|
macro_rules! assert_miette_snapshot {
|
||||||
($diag:expr) => {{
|
($diag:expr) => {{
|
||||||
|
@ -180,104 +137,51 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_records() -> miette::Result<()> {
|
fn valid_records() -> miette::Result<()> {
|
||||||
verify(
|
for record in [
|
||||||
"\
|
"example.com.",
|
||||||
example.com.\n\
|
"example.org.",
|
||||||
example.org.\n\
|
"example.net.",
|
||||||
example.net.\n\
|
"subdomain.example.com.",
|
||||||
subdomain.example.com.\n\
|
] {
|
||||||
",
|
validate_record_str(record)?;
|
||||||
std::path::Path::new("test_records_valid"),
|
}
|
||||||
)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hostname_too_long() {
|
fn hostname_too_long() {
|
||||||
let err = verify(
|
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();
|
||||||
"\
|
|
||||||
example.com.\n\
|
|
||||||
example.org.\n\
|
|
||||||
example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net.\n\
|
|
||||||
subdomain.example.com.\n\
|
|
||||||
",
|
|
||||||
std::path::Path::new("test_records_invalid"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
assert_miette_snapshot!(err);
|
assert_miette_snapshot!(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn not_fqd() {
|
fn not_fqd() {
|
||||||
let err = verify(
|
let err = validate_record_str("example.net").unwrap_err();
|
||||||
"\
|
|
||||||
example.com.\n\
|
|
||||||
example.org.\n\
|
|
||||||
example.net\n\
|
|
||||||
subdomain.example.com.\n\
|
|
||||||
",
|
|
||||||
std::path::Path::new("test_records_invalid"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
assert_miette_snapshot!(err);
|
assert_miette_snapshot!(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_label() {
|
fn empty_label() {
|
||||||
let err = verify(
|
let err = validate_record_str("name..example.org.").unwrap_err();
|
||||||
"\
|
|
||||||
example.com.\n\
|
|
||||||
name..example.org.\n\
|
|
||||||
example.net.\n\
|
|
||||||
subdomain.example.com.\n\
|
|
||||||
",
|
|
||||||
std::path::Path::new("test_records_invalid"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
assert_miette_snapshot!(err);
|
assert_miette_snapshot!(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn label_too_long() {
|
fn label_too_long() {
|
||||||
let err = verify(
|
let err = validate_record_str("name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.").unwrap_err();
|
||||||
"\
|
|
||||||
example.com.\n\
|
|
||||||
name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.\n\
|
|
||||||
example.net.\n\
|
|
||||||
subdomain.example.com.\n\
|
|
||||||
",
|
|
||||||
std::path::Path::new("test_records_invalid"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
assert_miette_snapshot!(err);
|
assert_miette_snapshot!(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_ascii() {
|
fn invalid_ascii() {
|
||||||
let err = verify(
|
let err = validate_record_str("name.this-is-not-ascii-ß.example.org.").unwrap_err();
|
||||||
"\
|
|
||||||
example.com.\n\
|
|
||||||
name.this-is-not-aßcii.example.org.\n\
|
|
||||||
example.net.\n\
|
|
||||||
subdomain.example.com.\n\
|
|
||||||
",
|
|
||||||
std::path::Path::new("test_records_invalid"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
assert_miette_snapshot!(err);
|
assert_miette_snapshot!(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_octet() {
|
fn invalid_octet() {
|
||||||
let err = verify(
|
let err =
|
||||||
"\
|
validate_record_str("name.this-character:-is-not-allowed.example.org.").unwrap_err();
|
||||||
example.com.\n\
|
|
||||||
name.this-character:-is-not-allowed.example.org.\n\
|
|
||||||
example.net.\n\
|
|
||||||
subdomain.example.com.\n\
|
|
||||||
",
|
|
||||||
std::path::Path::new("test_records_invalid"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
assert_miette_snapshot!(err);
|
assert_miette_snapshot!(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,9 @@ expression: out
|
||||||
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
||||||
|
|
||||||
× empty label
|
× empty label
|
||||||
╭─[test_records_invalid:2:6]
|
╭────
|
||||||
1 │ example.com.
|
1 │ name..example.org.
|
||||||
2 │ name..example.org.
|
|
||||||
· ▲
|
· ▲
|
||||||
· ╰── label
|
· ╰── label
|
||||||
3 │ example.net.
|
|
||||||
╰────
|
╰────
|
||||||
help: each label should have at least one character
|
help: each label should have at least one character
|
||||||
|
|
|
@ -6,11 +6,9 @@ expression: out
|
||||||
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
||||||
|
|
||||||
× hostname too long (260 octets)
|
× hostname too long (260 octets)
|
||||||
╭─[test_records_invalid:3:1]
|
╭────
|
||||||
2 │ example.org.
|
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.
|
||||||
3 │ example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.example.net.
|
|
||||||
· ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
· ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
· ╰── this line
|
· ╰── this line
|
||||||
4 │ subdomain.example.com.
|
|
||||||
╰────
|
╰────
|
||||||
help: fully qualified domain names can be at most 255 characters long
|
help: fully qualified domain names can be at most 255 characters long
|
||||||
|
|
|
@ -6,11 +6,9 @@ expression: out
|
||||||
]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\
|
]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\
|
||||||
|
|
||||||
× invalid octet: '\xc3'
|
× invalid octet: '\xc3'
|
||||||
╭─[test_records_invalid:2:19]
|
╭────
|
||||||
1 │ example.com.
|
1 │ name.this-is-not-ascii-ß.example.org.
|
||||||
2 │ name.this-is-not-aßcii.example.org.
|
· ┬
|
||||||
· ┬
|
· ╰── octet
|
||||||
· ╰── octet
|
|
||||||
3 │ example.net.
|
|
||||||
╰────
|
╰────
|
||||||
help: we only accept ascii characters
|
help: we only accept ascii characters
|
||||||
|
|
|
@ -6,11 +6,9 @@ expression: out
|
||||||
]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\
|
]8;;https://en.wikipedia.org/wiki/Hostname#Syntax\(link)]8;;\
|
||||||
|
|
||||||
× invalid octet: ':'
|
× invalid octet: ':'
|
||||||
╭─[test_records_invalid:2:20]
|
╭────
|
||||||
1 │ example.com.
|
1 │ name.this-character:-is-not-allowed.example.org.
|
||||||
2 │ name.this-character:-is-not-allowed.example.org.
|
|
||||||
· ┬
|
· ┬
|
||||||
· ╰── octet
|
· ╰── octet
|
||||||
3 │ example.net.
|
|
||||||
╰────
|
╰────
|
||||||
help: hostnames are only allowed to contain characters in [a-zA-Z0-9_-]
|
help: hostnames are only allowed to contain characters in [a-zA-Z0-9_-]
|
||||||
|
|
|
@ -6,11 +6,9 @@ expression: out
|
||||||
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
||||||
|
|
||||||
× label too long (78 octets)
|
× label too long (78 octets)
|
||||||
╭─[test_records_invalid:2:6]
|
╭────
|
||||||
1 │ example.com.
|
1 │ name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.
|
||||||
2 │ name.an-entremely-long-label-that-should-not-exist-because-it-goes-against-the-spec.example.org.
|
|
||||||
· ───────────────────────────────────────┬──────────────────────────────────────
|
· ───────────────────────────────────────┬──────────────────────────────────────
|
||||||
· ╰── label
|
· ╰── label
|
||||||
3 │ example.net.
|
|
||||||
╰────
|
╰────
|
||||||
help: labels should be at most 63 octets
|
help: labels should be at most 63 octets
|
||||||
|
|
|
@ -6,11 +6,9 @@ expression: out
|
||||||
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
]8;;https://en.wikipedia.org/wiki/Fully_qualified_domain_name\(link)]8;;\
|
||||||
|
|
||||||
× not a fully qualified domain name
|
× not a fully qualified domain name
|
||||||
╭─[test_records_invalid:3:11]
|
╭────
|
||||||
2 │ example.org.
|
1 │ example.net
|
||||||
3 │ example.net
|
|
||||||
· ┬
|
· ┬
|
||||||
· ╰── last character
|
· ╰── last character
|
||||||
4 │ subdomain.example.com.
|
|
||||||
╰────
|
╰────
|
||||||
help: hostname should be a fully qualified domain name (end with a '.')
|
help: hostname should be a fully qualified domain name (end with a '.')
|
||||||
|
|
Loading…
Add table
Reference in a new issue