Compare commits
3 commits
217bacb958
...
bfb923e841
Author | SHA1 | Date | |
---|---|---|---|
bfb923e841 | |||
f207cbe859 | |||
c589fb40c3 |
8 changed files with 232 additions and 240 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -409,6 +409,12 @@ version = "1.0.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.6.0"
|
||||
|
@ -749,9 +755,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
|
||||
checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
|
@ -1177,6 +1183,7 @@ dependencies = [
|
|||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"http",
|
||||
"humantime",
|
||||
"insta",
|
||||
"miette",
|
||||
"ring",
|
||||
|
|
|
@ -23,6 +23,7 @@ clap-verbosity-flag = { version = "3", default-features = false, features = [
|
|||
"tracing",
|
||||
] }
|
||||
http = "1"
|
||||
humantime = "2.2.0"
|
||||
miette = { version = "7", features = ["fancy"] }
|
||||
ring = { version = "0.17", features = ["std"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
|
@ -3,10 +3,18 @@
|
|||
imports = [
|
||||
inputs.treefmt-nix.flakeModule
|
||||
./package.nix
|
||||
./module.nix
|
||||
./tests.nix
|
||||
];
|
||||
|
||||
flake.nixosModules =
|
||||
let
|
||||
webnsupdate = ../module.nix;
|
||||
in
|
||||
{
|
||||
default = webnsupdate;
|
||||
inherit webnsupdate;
|
||||
};
|
||||
|
||||
perSystem =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
|
|
|
@ -1,222 +0,0 @@
|
|||
let
|
||||
module =
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.webnsupdate;
|
||||
inherit (lib)
|
||||
mkOption
|
||||
mkEnableOption
|
||||
mkPackageOption
|
||||
types
|
||||
;
|
||||
format = pkgs.formats.json { };
|
||||
in
|
||||
{
|
||||
# imports = [
|
||||
# (lib.mkRenamedOptionModule
|
||||
# [ "services" "webnsupdate" "passwordFile" ]
|
||||
# [ "services" "webnsupdate" "settings" "password_file" ]
|
||||
# )
|
||||
# (lib.mkRenamedOptionModule
|
||||
# [ "services" "webnsupdate" "keyFile" ]
|
||||
# [ "services" "webnsupdate" "settings" "key_file" ]
|
||||
# )
|
||||
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "allowedIPVersion" ] ''
|
||||
# This option was replaced with 'services.webnsupdate.settings.ip_type' which defaults to Both.
|
||||
# '')
|
||||
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "bindIp" ] ''
|
||||
# This option was replaced with 'services.webnsupdate.settings.address' which defaults to 127.0.0.1:5353.
|
||||
# '')
|
||||
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "bindPort" ] ''
|
||||
# This option was replaced with 'services.webnsupdate.settings.address' which defaults to 127.0.0.1:5353.
|
||||
# '')
|
||||
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "records" ] ''
|
||||
# This option was replaced with 'services.webnsupdate.settings.records' which defaults to [].
|
||||
# '')
|
||||
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "recordsFile" ] ''
|
||||
# This option was replaced with 'services.webnsupdate.settings.records' which defaults to [].
|
||||
# '')
|
||||
# (lib.mkRemovedOptionModule [ "services" "webnsupdate" "ttl" ] ''
|
||||
# This option was replaced with 'services.webnsupdate.settings.ttl' which defaults to 600s.
|
||||
# '')
|
||||
# ];
|
||||
|
||||
options.services.webnsupdate = mkOption {
|
||||
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 = {
|
||||
secs = 600;
|
||||
};
|
||||
example = {
|
||||
secs = 600;
|
||||
nanos = 50000;
|
||||
};
|
||||
type = types.submodule {
|
||||
options = {
|
||||
secs = mkOption {
|
||||
description = "The TTL (in seconds) that should be set on the zone records created by `nsupdate`.";
|
||||
example = 3600;
|
||||
};
|
||||
nanos = mkOption {
|
||||
description = "The TTL (in nanoseconds) that should be set on the zone records created by `nsupdate`.";
|
||||
default = 0;
|
||||
example = 50000;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
flake.nixosModules = {
|
||||
default = module;
|
||||
webnsupdate = module;
|
||||
};
|
||||
}
|
18
flake.lock
generated
18
flake.lock
generated
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1741481578,
|
||||
"narHash": "sha256-JBTSyJFQdO3V8cgcL08VaBUByEU6P5kXbTJN6R0PFQo=",
|
||||
"lastModified": 1742317686,
|
||||
"narHash": "sha256-ScJYnUykEDhYeCepoAWBbZWx2fpQ8ottyvOyGry7HqE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "bb1c9567c43e4434f54e9481eb4b8e8e0d50f0b5",
|
||||
"rev": "66cb0013f9a99d710b167ad13cbd8cc4e64f2ddb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -37,11 +37,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1741851582,
|
||||
"narHash": "sha256-cPfs8qMccim2RBgtKGF+x9IBCduRvd/N5F4nYpU0TVE=",
|
||||
"lastModified": 1742069588,
|
||||
"narHash": "sha256-C7jVfohcGzdZRF6DO+ybyG/sqpo1h6bZi9T56sxLy+k=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6607cf789e541e7873d40d3a8f7815ea92204f32",
|
||||
"rev": "c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -82,11 +82,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1739829690,
|
||||
"narHash": "sha256-mL1szCeIsjh6Khn3nH2cYtwO5YXG6gBiTw1A30iGeDU=",
|
||||
"lastModified": 1742303424,
|
||||
"narHash": "sha256-2R7cGdcA2npQQcIWu2cTlU63veTzwVZe78BliIuJT00=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "3d0579f5cc93436052d94b73925b48973a104204",
|
||||
"rev": "b3b938ab8ba2e8a0ce9ee9b30ccfa5e903ae5753",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -2,7 +2,6 @@ use std::{
|
|||
fs::File,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use axum_client_ip::SecureClientIpSource;
|
||||
|
@ -75,8 +74,12 @@ pub struct Password {
|
|||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Records {
|
||||
/// Time To Live (in seconds) to set on the DNS records
|
||||
#[serde(default = "default_ttl")]
|
||||
pub ttl: Duration,
|
||||
#[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")]
|
||||
|
@ -124,6 +127,10 @@ pub struct Config {
|
|||
/// 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 {
|
||||
|
@ -179,8 +186,8 @@ pub struct Invalid {
|
|||
|
||||
// --- Default Values (sadly serde doesn't have a way to specify a constant as a default value) ---
|
||||
|
||||
fn default_ttl() -> Duration {
|
||||
super::DEFAULT_TTL
|
||||
fn default_ttl() -> humantime::Duration {
|
||||
super::DEFAULT_TTL.into()
|
||||
}
|
||||
|
||||
fn default_salt() -> Box<str> {
|
||||
|
@ -199,6 +206,35 @@ 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();
|
||||
|
|
|
@ -175,7 +175,7 @@ impl AppState<'static> {
|
|||
);
|
||||
|
||||
let state = AppState {
|
||||
ttl: *ttl,
|
||||
ttl: **ttl,
|
||||
records,
|
||||
// Load keyfile
|
||||
key_file: key_file
|
||||
|
|
Loading…
Add table
Reference in a new issue