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