From f428c678a35760bb7e6a549d8e2f555837b5af05 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sun, 11 Aug 2024 18:49:28 +0200 Subject: [PATCH] clanModule: Init dyndns module to update domains pointing to dynamic IPs --- clanModules/dyndns/README.md | 6 ++ clanModules/dyndns/default.nix | 173 +++++++++++++++++++++++++++++++++ clanModules/flake-module.nix | 1 + docs/mkdocs.yml | 1 + 4 files changed, 181 insertions(+) create mode 100644 clanModules/dyndns/README.md create mode 100644 clanModules/dyndns/default.nix diff --git a/clanModules/dyndns/README.md b/clanModules/dyndns/README.md new file mode 100644 index 000000000..b739c5efb --- /dev/null +++ b/clanModules/dyndns/README.md @@ -0,0 +1,6 @@ +--- +description = "A dynamic DNS service to update domain IPs" +--- + + + diff --git a/clanModules/dyndns/default.nix b/clanModules/dyndns/default.nix new file mode 100644 index 000000000..3ef3759fa --- /dev/null +++ b/clanModules/dyndns/default.nix @@ -0,0 +1,173 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + name = "dyndns"; + cfg = config.clan.${name}; + + # We dedup secrets if they have the same provider + base domain + secret_id = opt: "${name}-${opt.provider}-${opt.domain}"; + secret_path = + opt: config.clan.core.facts.services."${secret_id opt}".secret."${secret_id opt}".path; + + /* + We go from: + {home.gchq.icu:{value:{domain:gchq.icu,host:home, provider:namecheap}}} + To: + {settings: [{domain: gchq.icu, host: home, provider: namecheap, password: dyndns-namecheap-gchq.icu}]} + */ + service_config = { + settings = builtins.catAttrs "value" ( + builtins.attrValues ( + lib.mapAttrs (_: opt: { + value = opt // { + password = secret_id opt; + }; + }) cfg.settings + ) + ); + }; + + secret_generator = _: opt: { + name = secret_id opt; + value = { + secret.${secret_id opt} = { }; + generator.prompt = "Dyndns passphrase for ${secret_id opt}"; + generator.script = '' + echo "$prompt_value" > $secrets/${secret_id opt} + ''; + }; + }; +in +{ + options.clan.${name} = { + + user = lib.mkOption { + type = lib.types.str; + default = name; + description = "User to run the service as"; + }; + group = lib.mkOption { + type = lib.types.str; + default = name; + description = "Group to run the service as"; + }; + + settings = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { ... }: + { + options = { + provider = lib.mkOption { + type = lib.types.str; + description = "The dyndns provider to use"; + }; + domain = lib.mkOption { + type = lib.types.str; + description = "The top level domain to update. For example 'example.com'. + If you want to update a subdomain, add the 'subdomain' option"; + }; + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "The subdomain to update of the tld"; + }; + }; + } + ) + ); + default = [ ]; + description = "Wifi networks to predefine"; + }; + }; + + imports = [ + (lib.mkRemovedOptionModule [ + "clan" + "dyndns" + "enable" + ] "Just define clan.dyndns.settings to enable it") + ]; + + config = lib.mkMerge [ + (lib.mkIf (cfg.settings != { }) { + clan.core.facts.services = lib.mapAttrs' secret_generator cfg.settings; + + users.groups.${cfg.group} = { }; + users.users.${cfg.user} = { + group = cfg.group; + isSystemUser = true; + description = "User for ${name} service"; + home = "/var/lib/${name}"; + createHome = true; + }; + + systemd.services.${name} = { + path = [ ]; + description = "Dynamic DNS updater"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + MYCONFIG = "${builtins.toJSON service_config}"; + }; + + serviceConfig = + let + pyscript = pkgs.writers.writePyPy3Bin "test.py" { libraries = [ ]; } '' + import json + from pathlib import Path + import os + + cred_dir = Path(os.getenv("CREDENTIALS_DIRECTORY")) + config_str = os.getenv("MYCONFIG") + + + def get_credential(name): + secret_p = cred_dir / name + with open(secret_p, 'r') as f: + return f.read().strip() + + + config = json.loads(config_str) + print(f"Config: {config}") + for attrset in config["settings"]: + attrset['password'] = get_credential(attrset['password']) + + # create directory data if it does not exist + data_dir = Path('data') + data_dir.mkdir(mode=0o770, exist_ok=True) + + # Write the config with secrets back + config_path = data_dir / 'config.json' + with open(config_path, 'w') as f: + f.write(json.dumps(config, indent=4)) + + # Set file permissions to read and write + # only by the user and group + config_path.chmod(0o660) + + # Set file permissions to read + # and write only by the user and group + for file in data_dir.iterdir(): + file.chmod(0o660) + ''; + in + { + ExecStartPre = lib.getExe pyscript; + ExecStart = lib.getExe pkgs.ddns-updater; + LoadCredential = lib.mapAttrsToList (_: opt: "${secret_id opt}:${secret_path opt}") cfg.settings; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = "/var/lib/${name}"; + + Restart = "always"; + RestartSec = 60; + }; + }; + }) + ]; +} diff --git a/clanModules/flake-module.nix b/clanModules/flake-module.nix index 3f13fc971..8e21351e3 100644 --- a/clanModules/flake-module.nix +++ b/clanModules/flake-module.nix @@ -2,6 +2,7 @@ { flake.clanModules = { iwd = ./iwd; + dyndns = ./dyndns; borgbackup = ./borgbackup; borgbackup-static = ./borgbackup-static; deltachat = ./deltachat; diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index dd1fea679..0537b0654 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -60,6 +60,7 @@ nav: - reference/clanModules/borgbackup.md - reference/clanModules/deltachat.md - reference/clanModules/iwd.md + - reference/clanModules/dyndns.md - reference/clanModules/ergochat.md - reference/clanModules/localbackup.md - reference/clanModules/localsend.md