1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 cfg = config.services.ddclient;
9 boolToStr = bool: if bool then "yes" else "no";
10 dataDir = "/var/lib/ddclient";
11 StateDirectory = builtins.baseNameOf dataDir;
12 RuntimeDirectory = StateDirectory;
13
14 configFile' = pkgs.writeText "ddclient.conf" ''
15 # This file can be used as a template for configFile or is automatically generated by Nix options.
16 cache=${dataDir}/ddclient.cache
17 foreground=YES
18 ${lib.optionalString (cfg.use != "") "use=${cfg.use}"}
19 ${lib.optionalString (cfg.use == "" && cfg.usev4 != "") "usev4=${cfg.usev4}"}
20 ${lib.optionalString (cfg.use == "" && cfg.usev6 != "") "usev6=${cfg.usev6}"}
21 ${lib.optionalString (cfg.username != "") "login=${cfg.username}"}
22 ${
23 if cfg.protocol == "nsupdate" then
24 "/run/${RuntimeDirectory}/ddclient.key"
25 else if (cfg.passwordFile != null) then
26 "password=@password_placeholder@"
27 else if (cfg.secretsFile != null) then
28 "@secrets_placeholder@"
29 else
30 ""
31 }
32 protocol=${cfg.protocol}
33 ${lib.optionalString (cfg.script != "") "script=${cfg.script}"}
34 ${lib.optionalString (cfg.server != "") "server=${cfg.server}"}
35 ${lib.optionalString (cfg.zone != "") "zone=${cfg.zone}"}
36 ssl=${boolToStr cfg.ssl}
37 wildcard=YES
38 quiet=${boolToStr cfg.quiet}
39 verbose=${boolToStr cfg.verbose}
40 ${cfg.extraConfig}
41 ${lib.concatStringsSep "," cfg.domains}
42 '';
43 configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
44
45 preStart = ''
46 install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf
47 ${lib.optionalString (cfg.configFile == null) (
48 if (cfg.protocol == "nsupdate") then
49 ''
50 install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
51 ''
52 else if (cfg.passwordFile != null) then
53 ''
54 "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
55 ''
56 else if (cfg.secretsFile != null) then
57 ''
58 "${pkgs.replace-secret}/bin/replace-secret" "@secrets_placeholder@" "${cfg.secretsFile}" "/run/${RuntimeDirectory}/ddclient.conf"
59 ''
60 else
61 ''
62 sed -i '/^password=@password_placeholder@$/d' /run/${RuntimeDirectory}/ddclient.conf
63 ''
64 )}
65 '';
66in
67{
68
69 imports = [
70 (lib.mkChangedOptionModule [ "services" "ddclient" "domain" ] [ "services" "ddclient" "domains" ] (
71 config:
72 let
73 value = lib.getAttrFromPath [ "services" "ddclient" "domain" ] config;
74 in
75 lib.optional (value != "") value
76 ))
77 (lib.mkRemovedOptionModule [ "services" "ddclient" "homeDir" ] "")
78 (lib.mkRemovedOptionModule [
79 "services"
80 "ddclient"
81 "password"
82 ] "Use services.ddclient.passwordFile instead.")
83 (lib.mkRemovedOptionModule [ "services" "ddclient" "ipv6" ] "")
84 ];
85
86 ###### interface
87
88 options = {
89
90 services.ddclient = with lib.types; {
91
92 enable = lib.mkOption {
93 default = false;
94 type = bool;
95 description = ''
96 Whether to synchronise your machine's IP address with a dynamic DNS provider (e.g. dyndns.org).
97 '';
98 };
99
100 package = lib.mkOption {
101 type = package;
102 default = pkgs.ddclient;
103 defaultText = lib.literalExpression "pkgs.ddclient";
104 description = ''
105 The ddclient executable package run by the service.
106 '';
107 };
108
109 domains = lib.mkOption {
110 default = [ "" ];
111 type = listOf str;
112 description = ''
113 Domain name(s) to synchronize.
114 '';
115 };
116
117 username = lib.mkOption {
118 # For `nsupdate` username contains the path to the nsupdate executable
119 default = lib.optionalString (
120 config.services.ddclient.protocol == "nsupdate"
121 ) "${pkgs.bind.dnsutils}/bin/nsupdate";
122 defaultText = "";
123 type = str;
124 description = ''
125 User name.
126 '';
127 };
128
129 passwordFile = lib.mkOption {
130 default = null;
131 type = nullOr str;
132 description = ''
133 A file containing the password or a TSIG key in named format when using the nsupdate protocol.
134 '';
135 };
136
137 secretsFile = lib.mkOption {
138 default = null;
139 type = nullOr str;
140 description = ''
141 A file containing the secrets for the dynamic DNS provider.
142 This file should contain lines of valid secrets in the format specified by the ddclient documentation.
143 If this option is set, it overrides the `passwordFile` option.
144 '';
145 };
146
147 interval = lib.mkOption {
148 default = "10min";
149 type = str;
150 description = ''
151 The interval at which to run the check and update.
152 See {command}`man 7 systemd.time` for the format.
153 '';
154 };
155
156 configFile = lib.mkOption {
157 default = null;
158 type = nullOr path;
159 description = ''
160 Path to configuration file.
161 When set this overrides the generated configuration from module options.
162 '';
163 example = "/root/nixos/secrets/ddclient.conf";
164 };
165
166 protocol = lib.mkOption {
167 default = "dyndns2";
168 type = str;
169 description = ''
170 Protocol to use with dynamic DNS provider (see <https://ddclient.net/protocols.html> ).
171 '';
172 };
173
174 server = lib.mkOption {
175 default = "";
176 type = str;
177 description = ''
178 Server address.
179 '';
180 };
181
182 ssl = lib.mkOption {
183 default = true;
184 type = bool;
185 description = ''
186 Whether to use SSL/TLS to connect to dynamic DNS provider.
187 '';
188 };
189
190 quiet = lib.mkOption {
191 default = false;
192 type = bool;
193 description = ''
194 Print no messages for unnecessary updates.
195 '';
196 };
197
198 script = lib.mkOption {
199 default = "";
200 type = str;
201 description = ''
202 script as required by some providers.
203 '';
204 };
205
206 use = lib.mkOption {
207 default = "";
208 type = str;
209 description = ''
210 Method to determine the IP address to send to the dynamic DNS provider.
211 '';
212 };
213 usev4 = lib.mkOption {
214 default = "webv4, webv4=ipify-ipv4";
215 type = str;
216 description = ''
217 Method to determine the IPv4 address to send to the dynamic DNS provider. Only used if `use` is not set.
218 '';
219 };
220 usev6 = lib.mkOption {
221 default = "webv6, webv6=ipify-ipv6";
222 type = str;
223 description = ''
224 Method to determine the IPv6 address to send to the dynamic DNS provider. Only used if `use` is not set.
225 '';
226 };
227
228 verbose = lib.mkOption {
229 default = false;
230 type = bool;
231 description = ''
232 Print verbose information.
233 '';
234 };
235
236 zone = lib.mkOption {
237 default = "";
238 type = str;
239 description = ''
240 zone as required by some providers.
241 '';
242 };
243
244 extraConfig = lib.mkOption {
245 default = "";
246 type = lines;
247 description = ''
248 Extra configuration. Contents will be added verbatim to the configuration file.
249
250 ::: {.note}
251 `daemon` should not be added here because it does not work great with the systemd-timer approach the service uses.
252 :::
253 '';
254 };
255 };
256 };
257
258 ###### implementation
259
260 config = lib.mkIf config.services.ddclient.enable {
261 warnings =
262 lib.optional (cfg.use != "")
263 "Setting `use` is deprecated, ddclient now supports `usev4` and `usev6` for separate IPv4/IPv6 configuration.";
264
265 assertions = [
266 {
267 assertion = !((cfg.passwordFile != null) && (cfg.secretsFile != null));
268 message = "You cannot use both services.ddclient.passwordFile and services.ddclient.secretsFile at the same time.";
269 }
270 {
271 assertion = (cfg.protocol != "nsupdate") || (cfg.secretsFile == null);
272 message = "You cannot use services.ddclient.secretsFile when services.ddclient.protocol is \"nsupdate\". Use services.ddclient.passwordFile instead.";
273 }
274 ];
275
276 systemd.services.ddclient = {
277 description = "Dynamic DNS Client";
278 wantedBy = [ "multi-user.target" ];
279 after = [ "network.target" ];
280 restartTriggers = lib.optional (cfg.configFile != null) cfg.configFile;
281 path = lib.optional (
282 lib.hasPrefix "if," cfg.use || lib.hasPrefix "ifv4," cfg.usev4 || lib.hasPrefix "ifv6," cfg.usev6
283 ) pkgs.iproute2;
284
285 serviceConfig = {
286 DynamicUser = true;
287 RuntimeDirectoryMode = "0700";
288 inherit RuntimeDirectory;
289 inherit StateDirectory;
290 Type = "oneshot";
291 ExecStartPre = [ "!${pkgs.writeShellScript "ddclient-prestart" preStart}" ];
292 ExecStart = "${lib.getExe cfg.package} -file /run/${RuntimeDirectory}/ddclient.conf";
293 };
294 };
295
296 systemd.timers.ddclient = {
297 description = "Run ddclient";
298 wantedBy = [ "timers.target" ];
299 timerConfig = {
300 OnBootSec = cfg.interval;
301 OnUnitInactiveSec = cfg.interval;
302 };
303 };
304 };
305}