Created
November 13, 2025 17:40
-
-
Save emmabastas/f39fab00f2b5fd0aa1f62270a23293d4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Autmatically update you dynamic DNS with njal.la in nix. | |
| This file is intended to be imported as a nixos module | |
| ```nix | |
| { | |
| import = [ <path-to-this-file> ]; | |
| services.njallaDns = { | |
| enable = true; | |
| domain = "mydomain.com"; | |
| keyFile = "/secrets/keyfile" | |
| frequency = "*:0/20"; # defaults to "*:0/5" | |
| }; | |
| } | |
| ``` | |
| At some point I think it should be added to the nixpkgs repo, but it needs some more polish maybe. | |
| I've found this module to be perfectly good when you're only interested in one njal.la domain. | |
| This module is based on: | |
| - https://github.com/NixOS/nixpkgs/blob/nixos-25.05/nixos/modules/services/misc/duckdns.nix | |
| - https://github.com/NixOS/nixpkgs/blob/nixos-25.05/nixos/modules/services/networking/cloudflare-dyndns.nix | |
| TODO: Right now this module can only handle a single domain. duckdns and cloudflare-dyndns generalise by accepting a list of domains. However this is not possible for Njalla since every domain also has a unique key associated. I think the best option is to generalize this module to take a list of configurations, one for each domain, complete with it's own `enable` option, and so on. | |
| IDEA: Can we reduce network traffic and CPU usage by only sending requests to Njalla API when our IP actually does change? Imagine two options | |
| - `cacheIp = { type = lib.types.bool; }` | |
| - `cachePath = { type = lib.types.path; }` | |
| We cache our current IP and if it hasn't changed skip making an http request. | |
| */ | |
| { | |
| config, | |
| pkgs, | |
| lib, | |
| ... | |
| }: | |
| let | |
| cfg = config.services.njallaDns; | |
| # Actual script used to update dynamic DNS record. | |
| njallaUpdate = pkgs.writeShellScriptBin "njallaUpdate" '' | |
| KEY=$1 | |
| DOMAIN=$2 | |
| RESPONSE=$(\ | |
| curl --connect-timeout 60 \ | |
| --no-progress-meter \ | |
| --write-out "\\n%{http_code}" \ | |
| "https://njal.la/update/?h=$DOMAIN&k=$KEY&auto&quiet" \ | |
| ) | |
| BODY=$(echo "$RESPONSE" | head -n -1) | |
| STATUS=$(echo "$RESPONSE" | tail -n 1) | |
| if [[ "$STATUS" = "200" ]]; then | |
| echo "IP was successfully updated at $(date)." | |
| else | |
| echo -e "Something went wrong, status code: $STATUS. Body:\n$BODY" | |
| exit 1 | |
| fi | |
| ''; | |
| # The following options are appended to the systemd configuration. They don't | |
| # affect the behavior of the systemd service and are only here to harden it. | |
| # If you are modifying the systemd script in the future and things don't work | |
| # for mysterious reasons (you cannot read some file that obviously exists for | |
| # instance) then you might want to look over these options. | |
| systemdHardeningOptions = { | |
| DynamicUser = true; | |
| PrivateTmp = true; | |
| PrivateDevices = true; | |
| PrivateUsers = true; | |
| DevicePolicy = "closed"; | |
| RemoveIPC = true; | |
| NoNewPrivileges= true; | |
| CapabilityBoundingSet = ""; | |
| ProtectClock = true; | |
| ProtectProc = "noaccess"; | |
| ProtectHome = true; | |
| ProtectControlGroups = true; | |
| ProtectKernelModules = true; | |
| ProtectKernelTunables = true; | |
| ProtectHostname = true; | |
| ProtectKernelLogs = true; | |
| RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; | |
| RestrictNamespaces = true; | |
| RestrictRealtime = true; | |
| RestrictSUIDSGID = true; | |
| MemoryDenyWriteExecute = true; | |
| LockPersonality = true; | |
| SystemCallFilter = "@system-service"; | |
| UMask = "377"; | |
| }; | |
| in | |
| { | |
| options.services.njallaDns = { | |
| enable = lib.mkEnableOption "Njalla Dynamic DNS Client"; | |
| keyFile = lib.mkOption { | |
| default = null; | |
| type = lib.types.path; | |
| description = '' | |
| The path to a file containing the key | |
| used to update the dynamic DNS record. | |
| See https://njal.la/docs/ddns/ | |
| ''; | |
| }; | |
| domain = lib.mkOption { | |
| default = null; | |
| type = lib.types.nullOr (lib.types.str); | |
| example = [ "foo.bar.com" ]; | |
| description = '' | |
| The domain to whose dynamic DNS record is to be updated | |
| ''; | |
| }; | |
| frequency = lib.mkOption { | |
| type = lib.types.nullOr lib.types.str; | |
| default = "*:0/5"; | |
| description = '' | |
| Update the dynamic DNS record with the given frequency (see | |
| {manpage}`systemd.time(7)` for the format). | |
| If null, do not run automatically. | |
| ''; | |
| }; | |
| curlPackage = lib.mkOption { | |
| default = pkgs.curl; | |
| type = lib.types.package; | |
| description = '' | |
| The curl package to use when making http requests to Njalla | |
| ''; | |
| }; | |
| }; | |
| config = lib.mkIf cfg.enable { | |
| assertions = [ | |
| { | |
| assertion = cfg.domain != null; | |
| message = "services.njallaDns.domain has to be defined"; | |
| } | |
| { | |
| assertion = cfg.keyFile != null; | |
| message = "services.njallaDns.keyFile has to be defined"; | |
| } | |
| ]; | |
| systemd.services.njallaDns = { | |
| description = "Njalla Dynamic DNS Client"; | |
| after = [ "network.target" ]; | |
| wantedBy = [ "multi-user.target" ]; | |
| path = [ | |
| cfg.curlPackage | |
| ]; | |
| serviceConfig = { | |
| Type = "simple"; | |
| LoadCredential = [ | |
| "NJALLA_KEY_FILE:${cfg.keyFile}" | |
| ]; | |
| } // systemdHardeningOptions; | |
| script = '' | |
| exec ${lib.getExe njallaUpdate} $(systemd-creds cat NJALLA_KEY_FILE) ${cfg.domain} | |
| ''; | |
| } | |
| // lib.optionalAttrs (cfg.frequency != null) { | |
| startAt = cfg.frequency; | |
| }; | |
| }; | |
| meta.maintainers = with lib.maintainers; [ emmabastas ]; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment