1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 cfg = config.services.cloudflare-dyndns;
9in
10{
11 options = {
12 services.cloudflare-dyndns = {
13 enable = lib.mkEnableOption "Cloudflare Dynamic DNS Client";
14
15 package = lib.mkPackageOption pkgs "cloudflare-dyndns" { };
16
17 apiTokenFile = lib.mkOption {
18 type = lib.types.pathWith {
19 absolute = true;
20 inStore = false;
21 };
22
23 description = ''
24 The path to a file containing the CloudFlare API token.
25 '';
26 };
27
28 domains = lib.mkOption {
29 type = lib.types.listOf lib.types.str;
30 default = [ ];
31 description = ''
32 List of domain names to update records for.
33 '';
34 };
35
36 frequency = lib.mkOption {
37 type = lib.types.nullOr lib.types.str;
38 default = "*:0/5";
39 description = ''
40 Run cloudflare-dyndns with the given frequency (see
41 {manpage}`systemd.time(7)` for the format).
42 If null, do not run automatically.
43 '';
44 };
45
46 proxied = lib.mkOption {
47 type = lib.types.bool;
48 default = false;
49 description = ''
50 Whether this is a DNS-only record, or also being proxied through CloudFlare.
51 '';
52 };
53
54 ipv4 = lib.mkOption {
55 type = lib.types.bool;
56 default = true;
57 description = ''
58 Whether to enable setting IPv4 A records.
59 '';
60 };
61
62 ipv6 = lib.mkOption {
63 type = lib.types.bool;
64 default = false;
65 description = ''
66 Whether to enable setting IPv6 AAAA records.
67 '';
68 };
69
70 deleteMissing = lib.mkOption {
71 type = lib.types.bool;
72 default = false;
73 description = ''
74 Whether to delete the record when no IP address is found.
75 '';
76 };
77 };
78 };
79
80 config = lib.mkIf cfg.enable {
81 systemd.services.cloudflare-dyndns =
82 {
83 description = "CloudFlare Dynamic DNS Client";
84 after = [ "network.target" ];
85 wantedBy = [ "multi-user.target" ];
86
87 environment = {
88 CLOUDFLARE_DOMAINS = toString cfg.domains;
89 };
90
91 serviceConfig = {
92 Type = "simple";
93 DynamicUser = true;
94 StateDirectory = "cloudflare-dyndns";
95 Environment = [ "XDG_CACHE_HOME=%S/cloudflare-dyndns/.cache" ];
96 LoadCredential = [
97 "apiToken:${cfg.apiTokenFile}"
98 ];
99 };
100
101 script =
102 let
103 args =
104 [ "--cache-file /var/lib/cloudflare-dyndns/ip.cache" ]
105 ++ (if cfg.ipv4 then [ "-4" ] else [ "-no-4" ])
106 ++ (if cfg.ipv6 then [ "-6" ] else [ "-no-6" ])
107 ++ lib.optional cfg.deleteMissing "--delete-missing"
108 ++ lib.optional cfg.proxied "--proxied";
109 in
110 ''
111 export CLOUDFLARE_API_TOKEN_FILE=''${CREDENTIALS_DIRECTORY}/apiToken
112
113 # Added 2025-03-10: `cfg.apiTokenFile` used to be passed as an
114 # `EnvironmentFile` to the service, which required it to be of
115 # the form "CLOUDFLARE_API_TOKEN=" rather than just the secret.
116 # If we detect this legacy usage, error out.
117 token=$(< "''${CLOUDFLARE_API_TOKEN_FILE}")
118 if [[ $token == CLOUDFLARE_API_TOKEN* ]]; then
119 echo "Error: your api token starts with 'CLOUDFLARE_API_TOKEN='. Remove that, and instead specify just the token." >&2
120 exit 1
121 fi
122
123 exec ${lib.getExe cfg.package} ${toString args}
124 '';
125 }
126 // lib.optionalAttrs (cfg.frequency != null) {
127 startAt = cfg.frequency;
128 };
129 };
130}