From 96fdda678fb2a0ae90314e26092854fe4171b60a Mon Sep 17 00:00:00 2001 From: Peter Lehmann Date: Sun, 2 Feb 2025 15:09:25 +0100 Subject: [PATCH] Move dyndns into separate module --- flake.nix | 1 + modules/dyndns/default.nix | 64 ++++++++++++++++++++++++++++++++++ modules/dyndns/dyndns.sh | 52 +++++++++++++++++++++++++++ nodes/heptifili/default.nix | 2 +- nodes/heptifili/dyndns.nix | 55 ----------------------------- nodes/heptifili/networking.nix | 7 ++++ secrets/common.yaml | 10 +++--- 7 files changed, 130 insertions(+), 61 deletions(-) create mode 100644 modules/dyndns/default.nix create mode 100644 modules/dyndns/dyndns.sh delete mode 100644 nodes/heptifili/dyndns.nix diff --git a/flake.nix b/flake.nix index 265a370..82d5b49 100644 --- a/flake.nix +++ b/flake.nix @@ -147,6 +147,7 @@ nixosModules = { common = ./modules/common; + dyndns = ./modules/dyndns; immich = ./modules/immich.nix; monitoring = ./modules/monitoring; netbox = ./modules/netbox.nix; diff --git a/modules/dyndns/default.nix b/modules/dyndns/default.nix new file mode 100644 index 0000000..c1a1d0f --- /dev/null +++ b/modules/dyndns/default.nix @@ -0,0 +1,64 @@ +{ inputs +, config +, pkgs +, lib +, ... +}: +let + cfg = config.dyndns; +in +{ + options.dyndns = { + enable = lib.mkOption { + default = false; + }; + IPv4 = lib.mkOption { + default = false; + }; + IPv6 = lib.mkOption { + default = false; + }; + hostname = lib.mkOption { + default = ""; + }; + zone = lib.mkOption { + default = ""; + }; + }; + + config = lib.mkIf cfg.enable { + sops.secrets."dyndns/environment" = { + sopsFile = "${inputs.self}/secrets/common.yaml"; + }; + + systemd.timers."dyndns" = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "1m"; + OnUnitActiveSec = "1m"; + Unit = "dyndns.service"; + }; + }; + + systemd.services."dyndns" = { + serviceConfig = + let + script = pkgs.writeShellApplication { + name = "dyndns"; + runtimeInputs = with pkgs; [ curl jq ]; + bashOptions = [ + "errexit" + "pipefail" + ]; + text = builtins.readFile ./dyndns.sh; + }; + in + { + EnvironmentFile = config.sops.secrets."dyndns/environment".path; + ExecStart = "${lib.getExe script} ${if cfg.IPv4 then "-4" else ""} ${if cfg.IPv6 then "-6" else ""} -h ${cfg.hostname} -z ${cfg.zone}"; + Type = "oneshot"; + User = "root"; + }; + }; + }; +} diff --git a/modules/dyndns/dyndns.sh b/modules/dyndns/dyndns.sh new file mode 100644 index 0000000..8f2e10a --- /dev/null +++ b/modules/dyndns/dyndns.sh @@ -0,0 +1,52 @@ +FLAG_IPV4="" +FLAG_IPV6="" + +while getopts 46h:z: opt +do + case $opt in + 4) FLAG_IPV4=true;; + 6) FLAG_IPV6=true;; + h) declare -r HOSTNAME=${OPTARG};; + z) declare -r ZONE=${OPTARG};; + *) exit 1;; + esac +done + +[ -n "$HOSTNAME" ] || [ -n "$ZONE" ] || (echo 'Argument missing please set hostname and domain using -h and -z flag'; exit 1) +[ -n "$FLAG_IPV4" ] || [ -n "$FLAG_IPV6" ] || (echo 'No IP version selected. Please set IP version using -4 and/or -6'; exit 1) + +ZONE_ID=$(curl -s "https://dns.hetzner.com/api/v1/zones" -H "Auth-API-Token: $HETZNER_API_KEY" | jq -r --arg ZONE "$ZONE" '.zones.[] | select(.name==$ZONE) | .id') + +if [ -n "$FLAG_IPV4" ]; then + RECORDID_V4=$(curl -s "https://dns.hetzner.com/api/v1/records?zone_id=$ZONE_ID" -H "Auth-API-Token: $HETZNER_API_KEY" | jq -r --arg HOSTNAME "$HOSTNAME" '.records.[] | select(.name==$HOSTNAME) | select(.type=="A") | .id') || (err=$?; printf "Get recordid A failed with %s" "$err"; exit 1) + CURRENT_V4=$(curl -s4 https://ip.hetzner.com) || (err=$?; printf "Get current IPv4 failed with %s" "$err"; exit 1) + DNS_V4=$(curl -s "https://dns.hetzner.com/api/v1/records/$RECORDID_V4" -H "Auth-API-Token: $HETZNER_API_KEY" | jq ".record.value") || (err=$?; printf "Get A record failed with %s" "$err"; exit 1) + if [ "$CURRENT_V4" = "$DNS_V4" ] + then + echo "IPv4 already up to date" + else + echo "$DNS_V4 => $CURRENT_V4" + curl -s -X "PUT" "https://dns.hetzner.com/api/v1/records/$RECORDID_V4" \ + -H 'Content-Type: application/json' \ + -H "Auth-API-Token: $HETZNER_API_KEY" \ + -d $'{"value": "'"$CURRENT_V4"'", "ttl": 60, "type": "A", "name": "'"$HOSTNAME"'", "zone_id": "'"$ZONE_ID"'"}' \ + || (err=$?; printf "Updating A record failed with %s" "$err"; exit 1) + fi +fi + +if [ -n "$FLAG_IPV6" ]; then + RECORDID_V6=$(curl -s "https://dns.hetzner.com/api/v1/records?zone_id=$ZONE_ID" -H "Auth-API-Token: $HETZNER_API_KEY" | jq -r --arg HOSTNAME "$HOSTNAME" '.records.[] | select(.name==$HOSTNAME) | select(.type=="AAAA") | .id') || (err=$?; printf "Get recordid AAAA failed with %s" "$err"; exit 1) + CURRENT_V6=$(curl -s6 https://ip.hetzner.com) || (err=$?; printf "Get current IPv6 failed with %s" "$err"; exit 1) + DNS_V6=$(curl -s "https://dns.hetzner.com/api/v1/records/$RECORDID_V6" -H "Auth-API-Token: $HETZNER_API_KEY" | jq ".record.value") || (err=$?; printf "Get AAAA record failed with %s" "$err"; exit 1) + if [ "$CURRENT_V6" = "$DNS_V6" ] + then + echo "IPv6 already up to date" + else + echo "$DNS_V6 => $CURRENT_V6" + curl -s -X "PUT" "https://dns.hetzner.com/api/v1/records/$RECORDID_V6" \ + -H 'Content-Type: application/json' \ + -H "Auth-API-Token: $HETZNER_API_KEY" \ + -d $'{"value": "'"$CURRENT_V6"'", "ttl": 60, "type": "A", "name": "'"$HOSTNAME"'", "zone_id": "'"$ZONE_ID"'"}' \ + || (err=$?; printf "Updating A record failed with %s" "$err"; exit 1) + fi +fi diff --git a/nodes/heptifili/default.nix b/nodes/heptifili/default.nix index e373b8c..9cbe199 100644 --- a/nodes/heptifili/default.nix +++ b/nodes/heptifili/default.nix @@ -3,10 +3,10 @@ }: { imports = [ + inputs.self.nixosModules.dyndns inputs.self.nixosModules.syncthing inputs.self.nixosModules.restic-server ./disko.nix - ./dyndns.nix ./hardware-configuration.nix ./networking.nix ]; diff --git a/nodes/heptifili/dyndns.nix b/nodes/heptifili/dyndns.nix deleted file mode 100644 index 3bfaa67..0000000 --- a/nodes/heptifili/dyndns.nix +++ /dev/null @@ -1,55 +0,0 @@ -{ inputs -, config -, pkgs -, ... -}: -{ - sops.secrets."dyndns/hetzner_api_key" = { - sopsFile = "${inputs.self}/secrets/common.yaml"; - }; - - systemd.timers."dyndns" = { - wantedBy = [ "timers.target" ]; - timerConfig = { - OnBootSec = "1m"; - OnUnitActiveSec = "1m"; - Unit = "dyndns.service"; - }; - }; - - systemd.services."dyndns" = { - script = '' - zone_id=7DbNys3Lx4MWjg4eXEvMG4 - # Get record ids - # recordid_ipv4=$(${pkgs.curl}/bin/curl -s "https://dns.hetzner.com/api/v1/records?zone_id=$zone_id" -H "Auth-API-Token: $(cat ${config.sops.secrets."dyndns/hetzner_api_key".path})" | ${pkgs.jq}/bin/jq '.records.[] | select(.name=="ip.heptifili") | select(.type=="A") | .id' | tr -d '"') || (err=$?; printf "Get recordid A failed with %s" "$err"; exit 1) - recordid_ipv6=$(${pkgs.curl}/bin/curl -s "https://dns.hetzner.com/api/v1/records?zone_id=$zone_id" -H "Auth-API-Token: $(cat ${config.sops.secrets."dyndns/hetzner_api_key".path})" | ${pkgs.jq}/bin/jq '.records.[] | select(.name=="ip.heptifili") | select(.type=="AAAA") | .id' | tr -d '"') || (err=$?; printf "Get recordid AAAA failed with %s" "$err"; exit 1) - # Get in use IP-addresses - # current_ipv4=$(${pkgs.curl}/bin/curl -s4 https://ip.hetzner.com) || (err=$?; printf "Get current IPv4 failed with %s" "$err"; exit 1) - current_ipv6=$(${pkgs.curl}/bin/curl -s6 https://ip.hetzner.com) || (err=$?; printf "Get current IPv6 failed with %s" "$err"; exit 1) - # Get IP-addresses set in DNS - # dns_ipv4=$(${pkgs.curl}/bin/curl -s "https://dns.hetzner.com/api/v1/records/$recordid_ipv4" -H "Auth-API-Token: $(cat ${config.sops.secrets."dyndns/hetzner_api_key".path})" | ${pkgs.jq}/bin/jq ".record.value" | tr -d '"') || (err=$?; printf "Get A record failed with %s" "$err"; exit 1) - dns_ipv6=$(${pkgs.curl}/bin/curl -s "https://dns.hetzner.com/api/v1/records/$recordid_ipv6" -H "Auth-API-Token: $(cat ${config.sops.secrets."dyndns/hetzner_api_key".path})" | ${pkgs.jq}/bin/jq ".record.value" | tr -d '"') || (err=$?; printf "Get AAAA record failed with %s" "$err"; exit 1) - - # if [ $current_ipv4 = $dns_ipv4 ] - # then - # echo "IPv4 already up to date" - # else - # echo "$dns_ipv4 => $current_ipv4" - # ${pkgs.curl}/bin/curl -s -X "PUT" "https://dns.hetzner.com/api/v1/records/$recordid_ipv4" -H 'Content-Type: application/json' -H "Auth-API-Token: $(cat ${config.sops.secrets."dyndns/hetzner_api_key".path})" -d $'{"value": "'$current_ipv4'", "ttl": 60, "type": "A", "name": "'ip.heptifili'", "zone_id": "'$zone_id'"}' || (err=$?; printf "Updating A record failed with %s" "$err"; exit 1) - # fi - - if [ $current_ipv6 = $dns_ipv6 ] - then - echo "IPv6 already up to date" - else - echo "$dns_ipv6 => $current_ipv6" - ${pkgs.curl}/bin/curl -s -X "PUT" "https://dns.hetzner.com/api/v1/records/$recordid_ipv6" -H 'Content-Type: application/json' -H "Auth-API-Token: $(cat ${config.sops.secrets."dyndns/hetzner_api_key".path})" -d $'{"value": "'$current_ipv6'", "ttl": 60, "type": "AAAA", "name": "'ip.heptifili'", "zone_id": "'$zone_id'"}' || (err=$?; printf "Updating AAAA record failed with %s" "$err"; exit 1) - fi - exit 0 - ''; - serviceConfig = { - Type = "oneshot"; - User = "root"; - }; - }; -} diff --git a/nodes/heptifili/networking.nix b/nodes/heptifili/networking.nix index d22dd46..91cdef7 100644 --- a/nodes/heptifili/networking.nix +++ b/nodes/heptifili/networking.nix @@ -13,6 +13,13 @@ in physicalConnections = [ (mkConnectionRev "Fritz!Box" "*") ]; }; + dyndns = { + enable = true; + IPv6 = true; + hostname = "ip.heptifili"; + zone = "xnee.net"; + }; + networking = { domains = { enable = true; diff --git a/secrets/common.yaml b/secrets/common.yaml index 068713b..1b80a19 100644 --- a/secrets/common.yaml +++ b/secrets/common.yaml @@ -6,9 +6,9 @@ backup: hetzner-s3: ENC[AES256_GCM,data:ATrXrFnhuguwqkwf7rYh3QmVY5tWpz0imaLdVIYwROX6MunZsX5WhKOoWsH5aXnBaxOw4a0PQ2l6c8E67dXnF4i1qEMF0nXb+LX2jCH0ZWhDVLrjSXC5QL8kvDpVBxb1Cu990VdYgHYa,iv:oAVASEl9Cc/TTEH/ShaAeLcjwYKBOJjyJN8EsrrqGSg=,tag:inZoJR7Hbvz2ArjyBiOEag==,type:str] heptifili: ENC[AES256_GCM,data:8ZFwfxOPZfZTTZu4ZfQBMFLAj9tKSFSCOw/be7/EipboTJvEobCwVETvDQcDUnS76LVYEqRndY0lrYVR2d2+PvXaPWSMCEIePpH9XVX/EiqNsv8Ls8XKVQGkOSiOPLzb7ct2QmN5/3++hEpap6WT+AognGLcAsGaMEZijUZkxtLOXUZfbA02U1zS3FvQeasB350iL2zl9SknVQAmtD5hFwmDu+23scRCS5u0jOth/YMRyez1hRKoUXBl,iv:LZtBGbtH98wHBiQGWqjLY8n1f3dXW/4H05+ONIfATwA=,tag:k06uBxDFIKSTqyNYFrFHXQ==,type:str] acme: - environment: ENC[AES256_GCM,data:i2hyFlAb1qANvqMyDsMZsGgJWbaYRkzPZqAGH6bfSJSKaf8oPVUe5zMSy+IiDI7idw==,iv:DmPZPemgw/e7hfCTRfQhWKoJJiJQcVMh+PVKcLu10Vw=,tag:6tg4cnZKBg2EasQ/Hekyfg==,type:str] + environment: ENC[AES256_GCM,data:wI1/TLI9DEriFBkZs2pacgOGph9bEm9g5W5wsI2YHzlGFyj4szfOTnq2/LpLykJO5Gx8,iv:n24nXZJ1frM6Pu0zyMNtj6suuIbkHsO5Io8WsETwGyw=,tag:e7KIT1klon2Ab9pW+rHvww==,type:str] dyndns: - hetzner_api_key: ENC[AES256_GCM,data:Sk22uZ14QCX8B3yzNQ5WbI8Qgj3Y5B+v8Lyy4R/XqeY=,iv:WXt9iddmNYGCwNkSfbEKwa7L1ZgVHedwqVuUJhnrm+4=,tag:cb4jSwRzhbM2iuLnRmmgIA==,type:str] + environment: ENC[AES256_GCM,data:Yb5UBt4AvYDc5T/KS5UFKkLwWZjmCaUukiwpcJkH8PNlzbNSyCB9JIioSTJXoPsaDc6y,iv:A9pMyNadzquRmYPv12WGp2UUhAwwPDWs1AJ3K2EnI7o=,tag:odopcgXWz0+QfLOKd9Cc0Q==,type:str] tailscale: authkey: ENC[AES256_GCM,data:W7GhMn7lqpt9dYI1L0Oq+dVGebu9xgmDuo422/YaY7UQTSvwgsE7WpsUs8hqvf0ukFCf73SG1jNC0jgyuQ==,iv:KTbGN3tvl1CiKgxNGmizW1TWVZzOl5PgZ2RDgQIKZnc=,tag:Y6uv1iCXwuebXUzZNXTzsA==,type:str] sops: @@ -80,8 +80,8 @@ sops: T0x3YkxyQUlGdEpPRnR4UVBzYy9FOTQKRyLlelSHBSwTZ5Ue6ADXCgD0nnVIwVSz b8zYn1j5JSYpxKt9jt7qC8vEI3zHgAMhHzMwO03dc4TjdkApFaAu6A== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-01-02T19:16:05Z" - mac: ENC[AES256_GCM,data:BAN5CT6YEq/Uvuqu9LtjOQwsdcY1ac30C6RLGHimyvGwFMW85TXG+63ThBv6SQdRDIke1WfZv4jj/U/cxIeIvMclqaS5YcDYcmqR/MvCl0LF6CJaMM2dtGgpuDiXKb4BUj9KvhfRZVyRtaRd3mvrh3XTp/GRKkwg6oT3GTSP3cU=,iv:2Q0vMfbJEl4XY6IwEnpyFwo02o/qWm7o8C4hduV/QSk=,tag:yh3QlJlTvAAoAtPPMdsTJg==,type:str] + lastmodified: "2025-02-02T14:06:00Z" + mac: ENC[AES256_GCM,data:A6/YnsASL/PqOCCSFxQ1NVP3LLc9mBN8bDbwBd4V8Cloebl7xUoCF6taWCHCf0yN/zC3E2m7TOFuUjQn/szeddCm60USuPUE9n5Nh7DnTmHFlo/um5tMwXk98a50yDFIz397fx97ji/63dWPKtMOnxDOEFu20ELR/AaibD1BCmk=,iv:ASIzas9DL2Ideq8uwWbTNOwnQh0p5f5OIAhR8WSSwws=,tag:EN4E3CIm39QE01rryfbT4g==,type:str] pgp: [] unencrypted_suffix: _unencrypted - version: 3.9.2 + version: 3.9.3