diff --git a/nixos/boxes/home/routing-common/dns.nix b/nixos/boxes/home/routing-common/dns.nix index d4a142d..89fec53 100644 --- a/nixos/boxes/home/routing-common/dns.nix +++ b/nixos/boxes/home/routing-common/dns.nix @@ -2,6 +2,7 @@ index: { lib, pkgs, config, assignments, allAssignments, ... }: let inherit (builtins) attrNames elemAt; inherit (lib.my) net; + inherit (lib.my.c) pubDomain; inherit (lib.my.c.home) prefixes vips routers; name = elemAt routers index; @@ -22,6 +23,7 @@ in owner = "pdns-recursor"; group = "pdns-recursor"; }; + "home/ddclient-cloudflare.key" = {}; }; pdns.recursor = { @@ -63,9 +65,36 @@ in }; }; - systemd.services = { - # Add AF_NETLINK to allow pulling IP from network interfaces - pdns.serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + systemd = { + services = { + # Add AF_NETLINK to allow pulling IP from network interfaces + pdns.serviceConfig.RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + ddns-update = { + description = "DNS update script"; + after = [ "network.target" ]; + path = [ + (pkgs.python3.withPackages (ps: [ ps.cloudflare ])) + pkgs.ldns + ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = + ''${./dns_update.py} -k ${config.age.secrets."home/ddclient-cloudflare.key".path} '' + + ''${pubDomain} ns${toString (index + 1)}.${config.networking.domain}''; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; + timers = { + ddns-update = { + description = "Periodically update DNS"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5min"; + OnUnitInactiveSec = "5min"; + }; + }; + }; }; environment.systemPackages = with pkgs; [ diff --git a/nixos/boxes/home/routing-common/dns_update.py b/nixos/boxes/home/routing-common/dns_update.py new file mode 100755 index 0000000..f06278e --- /dev/null +++ b/nixos/boxes/home/routing-common/dns_update.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import argparse +import subprocess + +import CloudFlare + +def main(): + parser = argparse.ArgumentParser(description='Cloudflare DNS update script') + parser.add_argument('-k', '--api-token-file', help='Cloudflare API token file') + parser.add_argument('zone', help='Cloudflare Zone') + parser.add_argument('record', help='Cloudflare record name') + args = parser.parse_args() + + address = subprocess.check_output( + ['drill', '-Q', '-p5353', '@127.0.0.1', args.record, 'A'], + encoding='utf8').strip() + + cf_token = None + if args.api_token_file: + with open(args.api_token_file) as f: + cf_token = f.readline().strip() + + cf = CloudFlare.CloudFlare(token=cf_token) + zones = cf.zones.get(params={'name': args.zone}) + assert zones, f'Zone {args.zone} not found' + records = cf.zones.dns_records.get(zones[0]['id'], params={'name': args.record}) + assert records, f'Record {args.record} not found in zone {args.zone}' + + print(f'Updating {args.record} -> {address}') + cf.zones.dns_records.patch( + zones[0]['id'], records[0]['id'], + data={'type': 'A', 'name': args.record, 'content': address}) + +if __name__ == '__main__': + main() diff --git a/secrets/home/ddclient-cloudflare.key.age b/secrets/home/ddclient-cloudflare.key.age new file mode 100644 index 0000000..3a85daa --- /dev/null +++ b/secrets/home/ddclient-cloudflare.key.age @@ -0,0 +1,13 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFpOcUlvZyBNalB5 +RUZRNE1CTUJsbW1kSkxBSWVIcG1RUnBKd1gvcnRQVkZCUXFOQmhvClJUN2ltbnNk +T1grdVJSTzIyNTBTTGVEckVGQXdYNHdwOU5NbW1md3lGM0kKLT4gc3NoLWVkMjU1 +MTkgcytxUmZnIHZ4bFZSS0huWFBDbUhNcTd2MFhvV0lOY1l3d3ZXNU4vT3dwMmlI +emhoV0kKcDF4M0FPK0JpclI5Q3Q5WGxpZWVYbHVWbkNWdTArclZsN09XK3VJSXc1 +awotPiBYMjU1MTkgRjRCNVZmcXVnQnJ4KzZoM1ZkdWxYUkJTM1JuK3ZlRWJYdkFR +WXpFSmR4NApTbU5qR3ZuN0ZmbzIvMTFsMkdNSGJXSVlrVmZPdnZvcHFiZW45SW9I +endJCi0+IDEoIjlcJi1ncmVhc2UgJUE4IWl5ODkgfGVdLihEfT4gWCAreSduPS4K +bkI2Wm9LRGJXdW11aDl2VgotLS0gTENqYjZEUUZaWVZEcWQvWW5yTzJEdHRLeDJm +QUl5aytXdDE5QVMwVHZVSQo+aDbaGNOrz+hTSUQ4IAjDC9EfNwrlXDZtBqw8HkRv +1/Rr737scjrM7Bgt9zuKn6CB0zdeHTW5u685V2hCW/3aTy1eppWMWj3r +-----END AGE ENCRYPTED FILE-----