wip: add config file to webnsupdate
All checks were successful
/ build (push) Successful in 1m7s
/ check (clippy) (push) Successful in 15s
/ check (module-ipv4-only-test) (push) Successful in 32s
/ check (module-ipv4-test) (push) Successful in 30s
/ check (module-ipv6-only-test) (push) Successful in 32s
/ check (module-ipv6-test) (push) Successful in 30s
/ check (module-nginx-test) (push) Successful in 32s
/ check (nextest) (push) Successful in 3s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 7s
All checks were successful
/ build (push) Successful in 1m7s
/ check (clippy) (push) Successful in 15s
/ check (module-ipv4-only-test) (push) Successful in 32s
/ check (module-ipv4-test) (push) Successful in 30s
/ check (module-ipv6-only-test) (push) Successful in 32s
/ check (module-ipv6-test) (push) Successful in 30s
/ check (module-nginx-test) (push) Successful in 32s
/ check (nextest) (push) Successful in 3s
/ check (treefmt) (push) Successful in 3s
/ report-size (push) Successful in 7s
This commit is contained in:
parent
29f7315f67
commit
8bfa36b837
15 changed files with 543 additions and 426 deletions
|
@ -14,8 +14,38 @@ let
|
|||
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 = { };
|
||||
|
@ -31,82 +61,92 @@ let
|
|||
example = [ "--ip-source" ];
|
||||
};
|
||||
package = mkPackageOption pkgs "webnsupdate" { };
|
||||
bindIp = mkOption {
|
||||
description = ''
|
||||
IP address to bind to.
|
||||
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 = "localhost";
|
||||
example = "0.0.0.0";
|
||||
};
|
||||
bindPort = mkOption {
|
||||
description = "Port to bind to.";
|
||||
type = types.port;
|
||||
default = 5353;
|
||||
};
|
||||
allowedIPVersion = mkOption {
|
||||
description = ''The allowed IP versions to accept updates from.'';
|
||||
type = types.enum [
|
||||
"both"
|
||||
"ipv4-only"
|
||||
"ipv6-only"
|
||||
];
|
||||
default = "both";
|
||||
example = "ipv4-only";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
description = ''
|
||||
The file where the password is stored.
|
||||
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";
|
||||
};
|
||||
keyFile = mkOption {
|
||||
description = ''
|
||||
The TSIG key that `nsupdate` should use.
|
||||
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`.";
|
||||
type = types.ints.positive;
|
||||
default = 60;
|
||||
example = 3600;
|
||||
};
|
||||
records = mkOption {
|
||||
description = ''
|
||||
The fqdn of records that should be updated.
|
||||
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.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";
|
||||
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.";
|
||||
|
@ -124,41 +164,14 @@ let
|
|||
|
||||
config =
|
||||
let
|
||||
recordsFile =
|
||||
if cfg.recordsFile != null then cfg.recordsFile else pkgs.writeText "webnsrecords" cfg.records;
|
||||
args = lib.strings.escapeShellArgs (
|
||||
[
|
||||
"--records"
|
||||
recordsFile
|
||||
"--key-file"
|
||||
cfg.keyFile
|
||||
"--password-file"
|
||||
cfg.passwordFile
|
||||
"--address"
|
||||
cfg.bindIp
|
||||
"--ip-type"
|
||||
cfg.allowedIPVersion
|
||||
"--port"
|
||||
(builtins.toString cfg.bindPort)
|
||||
"--ttl"
|
||||
(builtins.toString cfg.ttl)
|
||||
"--data-dir=%S/webnsupdate"
|
||||
]
|
||||
++ cfg.extraArgs
|
||||
);
|
||||
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.";
|
||||
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.";
|
||||
|
@ -167,9 +180,10 @@ let
|
|||
"network.target"
|
||||
"bind.service"
|
||||
];
|
||||
preStart = "${cmd} verify";
|
||||
preStart = "${lib.getExe cfg.package} verify ${configFile}";
|
||||
path = [ pkgs.dig ];
|
||||
startLimitIntervalSec = 60;
|
||||
environment.DATA_DIR = "%S/webnsupdate";
|
||||
serviceConfig = {
|
||||
ExecStart = [ cmd ];
|
||||
Type = "exec";
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
lastIPPath = "/var/lib/webnsupdate/last-ip.json";
|
||||
|
||||
zoneFile = pkgs.writeText "${testDomain}.zoneinfo" ''
|
||||
$TTL 60 ; 1 minute
|
||||
$TTL 600 ; 10 minutes
|
||||
$ORIGIN ${testDomain}.
|
||||
@ IN SOA ns1.${testDomain}. admin.${testDomain}. (
|
||||
1 ; serial
|
||||
|
@ -73,20 +73,19 @@
|
|||
|
||||
webnsupdate = {
|
||||
enable = true;
|
||||
bindIp = lib.mkDefault "127.0.0.1";
|
||||
keyFile = "/etc/bind/rndc.key";
|
||||
# test:test (user:password)
|
||||
passwordFile = pkgs.writeText "webnsupdate.pass" "FQoNmuU1BKfg8qsU96F6bK5ykp2b0SLe3ZpB3nbtfZA";
|
||||
package = self'.packages.webnsupdate;
|
||||
extraArgs = [
|
||||
"-vvv" # debug messages
|
||||
"--ip-source=ConnectInfo"
|
||||
];
|
||||
records = ''
|
||||
test1.${testDomain}.
|
||||
test2.${testDomain}.
|
||||
test3.${testDomain}.
|
||||
'';
|
||||
extraArgs = [ "-vvv" ]; # debug messages
|
||||
settings = {
|
||||
address = lib.mkDefault "127.0.0.1:5353";
|
||||
key_file = "/etc/bind/rndc.key";
|
||||
password_file = pkgs.writeText "webnsupdate.pass" "FQoNmuU1BKfg8qsU96F6bK5ykp2b0SLe3ZpB3nbtfZA"; # test:test
|
||||
ip_source = lib.mkDefault "ConnectInfo";
|
||||
records = [
|
||||
"test1.${testDomain}."
|
||||
"test2.${testDomain}."
|
||||
"test3.${testDomain}."
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -97,7 +96,7 @@
|
|||
webnsupdate-ipv4-machine
|
||||
];
|
||||
|
||||
config.services.webnsupdate.bindIp = "::1";
|
||||
config.services.webnsupdate.settings.address = "[::1]:5353";
|
||||
};
|
||||
|
||||
webnsupdate-nginx-machine =
|
||||
|
@ -109,26 +108,26 @@
|
|||
|
||||
config.services = {
|
||||
# Use default IP Source
|
||||
webnsupdate.extraArgs = lib.mkForce [ "-vvv" ]; # debug messages
|
||||
webnsupdate.settings.ip_source = "RightmostXForwardedFor";
|
||||
|
||||
nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
|
||||
virtualHosts.webnsupdate.locations."/".proxyPass =
|
||||
"http://${config.services.webnsupdate.bindIp}:${builtins.toString config.services.webnsupdate.bindPort}";
|
||||
"http://${config.services.webnsupdate.settings.address}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
webnsupdate-ipv4-only-machine = {
|
||||
imports = [ webnsupdate-nginx-machine ];
|
||||
config.services.webnsupdate.allowedIPVersion = "ipv4-only";
|
||||
config.services.webnsupdate.settings.ip_type = "Ipv4Only";
|
||||
};
|
||||
|
||||
webnsupdate-ipv6-only-machine = {
|
||||
imports = [ webnsupdate-nginx-machine ];
|
||||
config.services.webnsupdate.allowedIPVersion = "ipv6-only";
|
||||
config.services.webnsupdate.settings.ip_type = "Ipv6Only";
|
||||
};
|
||||
|
||||
# "A" for IPv4, "AAAA" for IPv6, "ANY" for any
|
||||
|
@ -158,9 +157,9 @@
|
|||
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) -> str:
|
||||
match_ip = "" if ip is None else f"\\s\\+60\\s\\+IN\\s\\+{record}\\s\\+{ip}$"
|
||||
return f"dig @localhost {record} {domain} +noall +answer | grep '^{domain}.{match_ip}'"
|
||||
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
|
||||
|
@ -168,10 +167,16 @@
|
|||
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):
|
||||
machine.succeed(dig_cmd(domain, record, ip))
|
||||
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):
|
||||
machine.fail(dig_cmd(domain, record, ip))
|
||||
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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue