chore: cargo update #9

Merged
jalil merged 4 commits from cargo-update into main 2024-10-28 10:45:56 +01:00
12 changed files with 532 additions and 314 deletions

110
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -28,9 +28,9 @@ dependencies = [
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.15" version = "0.6.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@ -43,36 +43,36 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.8" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [ dependencies = [
"utf8parse", "utf8parse",
] ]
[[package]] [[package]]
name = "anstyle-query" name = "anstyle-query"
version = "1.1.1" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.4" version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -208,15 +208,15 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.7.2" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.30" version = "1.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -237,6 +237,16 @@ dependencies = [
"clap_derive", "clap_derive",
] ]
[[package]]
name = "clap-verbosity-flag"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e099138e1807662ff75e2cebe4ae2287add879245574489f9b1588eb5e5564ed"
dependencies = [
"clap",
"log",
]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.20" version = "4.5.20"
@ -269,9 +279,9 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]] [[package]]
name = "console" name = "console"
@ -436,9 +446,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.4.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -507,9 +517,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.159" version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
@ -659,9 +669,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -671,9 +681,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.87" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -689,9 +699,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -767,9 +777,9 @@ dependencies = [
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.17" 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 = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]] [[package]]
name = "ryu" name = "ryu"
@ -779,18 +789,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.210" version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.210" version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -799,9 +809,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.128" version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -918,9 +928,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.79" version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -962,18 +972,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.64" version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.64" version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -992,9 +1002,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.40.0" version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -1159,6 +1169,7 @@ dependencies = [
"axum-client-ip", "axum-client-ip",
"base64 0.22.1", "base64 0.22.1",
"clap", "clap",
"clap-verbosity-flag",
"http", "http",
"insta", "insta",
"miette", "miette",
@ -1208,6 +1219,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.48.5" version = "0.48.5"

View file

@ -7,27 +7,28 @@ version = "0.3.0-dev"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
axum = "0.7.7" axum = "0.7"
axum-auth = { version = "0.7.0", default-features = false, features = [ axum-auth = { version = "0.7", default-features = false, features = [
"auth-basic", "auth-basic",
] } ] }
axum-client-ip = "0.6.1" axum-client-ip = "0.6"
base64 = "0.22.1" base64 = "0.22"
clap = { version = "4.5.20", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"] }
http = "1.1.0" clap-verbosity-flag = "2"
miette = { version = "7.2.0", features = ["fancy"] } http = "1"
ring = { version = "0.17.8", features = ["std"] } miette = { version = "7", features = ["fancy"] }
tokio = { version = "1.40.0", features = [ ring = { version = "0.17", features = ["std"] }
tokio = { version = "1", features = [
"macros", "macros",
"rt", "rt",
"process", "process",
"io-util", "io-util",
] } ] }
tracing = "0.1.40" tracing = "0.1"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies] [dev-dependencies]
insta = "1.40.0" insta = "1"
[profile.dev] [profile.dev]
debug = 0 debug = 0

33
flake-modules/default.nix Normal file
View file

@ -0,0 +1,33 @@
{ inputs, ... }:
{
imports = [
inputs.treefmt-nix.flakeModule
./package.nix
./overlay.nix
./module.nix
./tests.nix
];
perSystem =
{ pkgs, ... }:
{
# Setup formatters
treefmt = {
projectRootFile = "flake.nix";
programs = {
nixfmt.enable = true;
rustfmt.enable = true;
statix.enable = true;
typos.enable = true;
};
};
devShells.default = pkgs.mkShellNoCC {
packages = [
pkgs.cargo-insta
pkgs.cargo-udeps
pkgs.mold
];
};
};
}

196
flake-modules/module.nix Normal file
View file

@ -0,0 +1,196 @@
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;
};
}

View file

@ -0,0 +1,5 @@
{
flake = {
overlays.default = _final: prev: { webnsupdate = prev.callPackage ../default.nix { }; };
};
}

24
flake-modules/package.nix Normal file
View file

@ -0,0 +1,24 @@
{
perSystem =
{ pkgs, ... }:
{
packages =
let
webnsupdate = pkgs.callPackage ../default.nix { };
in
{
inherit 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
'';
};
};
};
}

144
flake-modules/tests.nix Normal file
View file

@ -0,0 +1,144 @@
{ self, ... }:
{
perSystem =
{ pkgs, self', ... }:
{
checks =
let
testDomain = "webnstest.example";
dynamicZonesDir = "/var/lib/named/zones";
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}.
$ORIGIN ${testDomain}.
${testDomain}. IN A 127.0.0.1
${testDomain}. IN AAAA ::1
ns1 IN A 127.0.0.1
ns1 IN AAAA ::1
nsupdate IN A 127.0.0.1
nsupdate IN AAAA ::1
'';
webnsupdate-machine = {
imports = [ self.nixosModules.webnsupdate ];
config = {
environment.systemPackages = [
pkgs.dig
pkgs.curl
];
services = {
webnsupdate = {
enable = true;
bindIp = "127.0.0.1";
keyFile = "/etc/bind/rndc.key";
# test:test (user:password)
passwordFile = pkgs.writeText "webnsupdate.pass" "FQoNmuU1BKfg8qsU96F6bK5ykp2b0SLe3ZpB3nbtfZA";
package = self'.packages.webnsupdate;
extraArgs = [
"-vvv" # debug messages
"--ip-source=ConnectInfo"
];
records = ''
test1.${testDomain}.
test2.${testDomain}.
test3.${testDomain}.
'';
};
bind = {
enable = true;
zones.${testDomain} = {
master = true;
file = "${dynamicZonesDir}/${testDomain}";
extraConfig = ''
allow-update { key rndc-key; };
'';
};
};
};
systemd.services.bind.preStart = ''
# shellcheck disable=SC2211,SC1127
rm -f ${dynamicZonesDir}/* # reset dynamic zones
${pkgs.coreutils}/bin/mkdir -m 0755 -p ${dynamicZonesDir}
chown "named" ${dynamicZonesDir}
chown "named" /var/lib/named
# copy dynamic zone's file to the dynamic zones dir
cp ${zoneFile} ${dynamicZonesDir}/${testDomain}
'';
};
};
in
{
module-test = pkgs.testers.runNixOSTest {
name = "webnsupdate-module";
nodes.machine = webnsupdate-machine;
testScript = ''
machine.start(allow_reboot=True)
machine.wait_for_unit("webnsupdate.service")
# ensure base DNS records area available
with subtest("query base DNS records"):
machine.succeed("dig @127.0.0.1 ${testDomain} | grep ^${testDomain}")
machine.succeed("dig @127.0.0.1 ns1.${testDomain} | grep ^ns1.${testDomain}")
machine.succeed("dig @127.0.0.1 nsupdate.${testDomain} | grep ^nsupdate.${testDomain}")
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} | grep ^test3.${testDomain}")
with subtest("update webnsupdate DNS records (invalid auth)"):
machine.fail("curl --fail --silent -u test1:test1 -X GET http://localhost:5353/update")
machine.fail("cat /var/lib/webnsupdate/last-ip") # no last-ip set yet
# ensure webnsupdate managed records are missing
with subtest("query webnsupdate DNS records (fail)"):
machine.fail("dig @127.0.0.1 test1.${testDomain} | grep ^test1.${testDomain}")
machine.fail("dig @127.0.0.1 test2.${testDomain} | grep ^test2.${testDomain}")
machine.fail("dig @127.0.0.1 test3.${testDomain} | grep ^test3.${testDomain}")
with subtest("update webnsupdate DNS records (valid auth)"):
machine.succeed("curl --fail --silent -u test:test -X GET http://localhost:5353/update")
machine.succeed("cat /var/lib/webnsupdate/last-ip")
# ensure webnsupdate managed records are available
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}")
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}")
'';
};
};
};
}

View file

@ -39,7 +39,8 @@
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"systems": "systems" "systems": "systems",
"treefmt-nix": "treefmt-nix"
} }
}, },
"systems": { "systems": {
@ -56,6 +57,26 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1729613947,
"narHash": "sha256-XGOvuIPW1XRfPgHtGYXd5MAmJzZtOuwlfKDgxX5KT3s=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "aac86347fb5063960eccb19493e0cadcdb4205ca",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -7,73 +7,16 @@
url = "github:hercules-ci/flake-parts"; url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs"; inputs.nixpkgs-lib.follows = "nixpkgs";
}; };
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = outputs =
inputs: inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } { inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ ./flake-modules ];
systems = import inputs.systems; systems = import inputs.systems;
perSystem =
{
lib,
pkgs,
self',
...
}:
{
packages =
let
webnsupdate = pkgs.callPackage ./default.nix { };
in
{
inherit 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
'';
};
};
formatter = pkgs.nixfmt-rfc-style;
checks = {
fmtRust = pkgs.callPackage ./run-cmd.nix {
src = inputs.self;
name = "fmt-rust";
extraNativeBuildInputs = [ pkgs.rustfmt ];
cmd = "${lib.getExe pkgs.cargo} fmt --all --check --verbose";
};
fmtNix = pkgs.callPackage ./run-cmd.nix {
src = inputs.self;
name = "fmt-nix";
cmd = "${lib.getExe self'.formatter} --check .";
};
lintNix = pkgs.callPackage ./run-cmd.nix {
src = inputs.self;
name = "lint-nix";
cmd = "${lib.getExe pkgs.statix} check .";
};
};
devShells.default = pkgs.mkShell {
packages = [
pkgs.cargo-insta
pkgs.cargo-udeps
pkgs.mold
];
};
};
flake = {
overlays.default = final: prev: { webnsupdate = final.callPackage ./default.nix { }; };
nixosModules.default = ./module.nix;
};
}; };
} }

View file

@ -1,177 +0,0 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.webnsupdate;
inherit (lib) mkOption mkEnableOption 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" ];
};
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)
]
++ cfg.extraArgs
);
cmd = "${lib.getExe pkgs.webnsupdate} ${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";
# New file permissions
UMask = "0027";
# Security
NoNewPrivileges = true;
ProtectHome = true;
};
};
};
}

View file

@ -1,16 +0,0 @@
{
stdenvNoCC,
src,
name,
cmd,
extraBuildInputs ? [ ],
extraNativeBuildInputs ? [ ],
}:
stdenvNoCC.mkDerivation {
name = "${name}-src";
inherit src;
buildInputs = extraBuildInputs;
nativeBuildInputs = extraNativeBuildInputs;
buildPhase = cmd;
installPhase = "mkdir $out";
}

View file

@ -12,6 +12,7 @@ use axum_auth::AuthBasic;
use axum_client_ip::{SecureClientIp, SecureClientIpSource}; use axum_client_ip::{SecureClientIp, SecureClientIpSource};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity;
use http::StatusCode; use http::StatusCode;
use miette::{bail, ensure, Context, IntoDiagnostic, Result}; use miette::{bail, ensure, Context, IntoDiagnostic, Result};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
@ -26,6 +27,9 @@ const DEFAULT_SALT: &str = "UpdateMyDNS";
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct Opts { struct Opts {
#[command(flatten)]
verbosity: Verbosity,
/// Ip address of the server /// Ip address of the server
#[arg(long, default_value = "127.0.0.1")] #[arg(long, default_value = "127.0.0.1")]
address: IpAddr, address: IpAddr,
@ -121,6 +125,7 @@ struct AppState<'a> {
} }
fn load_ip(path: &Path) -> Result<Option<IpAddr>> { fn load_ip(path: &Path) -> Result<Option<IpAddr>> {
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,
Err(err) => { Err(err) => {
@ -146,13 +151,28 @@ fn main() -> Result<()> {
// parse cli arguments // parse cli arguments
let mut args = Opts::parse(); let mut args = Opts::parse();
debug!("{args:?}");
// configure logger // configure logger
let subscriber = tracing_subscriber::FmtSubscriber::builder() let subscriber = tracing_subscriber::FmtSubscriber::builder()
.without_time() .without_time()
.with_env_filter( .with_env_filter(
EnvFilter::builder() EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into()) .with_default_directive(
if args.verbosity.is_present() {
match args.verbosity.log_level_filter() {
clap_verbosity_flag::LevelFilter::Off => LevelFilter::OFF,
clap_verbosity_flag::LevelFilter::Error => LevelFilter::ERROR,
clap_verbosity_flag::LevelFilter::Warn => LevelFilter::WARN,
clap_verbosity_flag::LevelFilter::Info => LevelFilter::INFO,
clap_verbosity_flag::LevelFilter::Debug => LevelFilter::DEBUG,
clap_verbosity_flag::LevelFilter::Trace => LevelFilter::TRACE,
}
} else {
LevelFilter::WARN
}
.into(),
)
.from_env_lossy(), .from_env_lossy(),
) )
.finish(); .finish();
@ -166,6 +186,7 @@ fn main() -> Result<()> {
} }
let Opts { let Opts {
verbosity: _,
address: ip, address: ip,
port, port,
password_file, password_file,
@ -287,6 +308,7 @@ async fn update_records(
AuthBasic((username, pass)): AuthBasic, AuthBasic((username, pass)): AuthBasic,
SecureClientIp(ip): SecureClientIp, SecureClientIp(ip): SecureClientIp,
) -> axum::response::Result<&'static str> { ) -> axum::response::Result<&'static str> {
debug!("received update request from {ip}");
let Some(pass) = pass else { let Some(pass) = pass else {
return Err((StatusCode::UNAUTHORIZED, Json::from("no password provided")).into()); return Err((StatusCode::UNAUTHORIZED, Json::from("no password provided")).into());
}; };
@ -309,9 +331,11 @@ async fn update_records(
match nsupdate(ip, state.ttl, state.key_file, state.records).await { match nsupdate(ip, state.ttl, state.key_file, state.records).await {
Ok(status) if status.success() => { Ok(status) if status.success() => {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
info!("updating last ip to {ip}");
if let Err(err) = std::fs::write(state.ip_file, format!("{ip}")) { if let Err(err) = std::fs::write(state.ip_file, format!("{ip}")) {
error!("Failed to update last IP: {err}"); error!("Failed to update last IP: {err}");
} }
info!("updated last ip to {ip}");
}); });
Ok("successful update") Ok("successful update")
} }