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 quiet=${boolToStr cfg.quiet}
24 verbose=${boolToStr cfg.verbose}
25 ${cfg.extraConfig}
26 ${lib.concatStringsSep "," cfg.domains}
27 '';
28 configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
29
30 preStart = ''
31 install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf
32 ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then ''
33 install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
34 '' else if (cfg.passwordFile != null) then ''
35 "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
36 '' else ''
37 sed -i '/^password=@password_placeholder@$/d' /run/${RuntimeDirectory}/ddclient.conf
38 '')}
39 '';
40
41in
42
43with lib;
44
45{
46
47 imports = [
48 (mkChangedOptionModule [ "services" "ddclient" "domain" ] [ "services" "ddclient" "domains" ]
49 (config:
50 let value = getAttrFromPath [ "services" "ddclient" "domain" ] config;
51 in optional (value != "") value))
52 (mkRemovedOptionModule [ "services" "ddclient" "homeDir" ] "")
53 (mkRemovedOptionModule [ "services" "ddclient" "password" ] "Use services.ddclient.passwordFile instead.")
54 (mkRemovedOptionModule [ "services" "ddclient" "ipv6" ] "")
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 = ''
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 = ''
76 The ddclient executable package run by the service.
77 '';
78 };
79
80 domains = mkOption {
81 default = [ "" ];
82 type = listOf str;
83 description = ''
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 = ''
94 User name.
95 '';
96 };
97
98 passwordFile = mkOption {
99 default = null;
100 type = nullOr str;
101 description = ''
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 = ''
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 = ''
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 = ''
129 Protocol to use with dynamic DNS provider (see https://ddclient.net/protocols.html ).
130 '';
131 };
132
133 server = mkOption {
134 default = "";
135 type = str;
136 description = ''
137 Server address.
138 '';
139 };
140
141 ssl = mkOption {
142 default = true;
143 type = bool;
144 description = ''
145 Whether to use SSL/TLS to connect to dynamic DNS provider.
146 '';
147 };
148
149 quiet = mkOption {
150 default = false;
151 type = bool;
152 description = ''
153 Print no messages for unnecessary updates.
154 '';
155 };
156
157 script = mkOption {
158 default = "";
159 type = str;
160 description = ''
161 script as required by some providers.
162 '';
163 };
164
165 use = mkOption {
166 default = "web, web=checkip.dyndns.com/, web-skip='Current IP Address: '";
167 type = str;
168 description = ''
169 Method to determine the IP address to send to the dynamic DNS provider.
170 '';
171 };
172
173 verbose = mkOption {
174 default = false;
175 type = bool;
176 description = ''
177 Print verbose information.
178 '';
179 };
180
181 zone = mkOption {
182 default = "";
183 type = str;
184 description = ''
185 zone as required by some providers.
186 '';
187 };
188
189 extraConfig = mkOption {
190 default = "";
191 type = lines;
192 description = ''
193 Extra configuration. Contents will be added verbatim to the configuration file.
194
195 ::: {.note}
196 `daemon` should not be added here because it does not work great with the systemd-timer approach the service uses.
197 :::
198 '';
199 };
200 };
201 };
202
203
204 ###### implementation
205
206 config = mkIf config.services.ddclient.enable {
207 systemd.services.ddclient = {
208 description = "Dynamic DNS Client";
209 wantedBy = [ "multi-user.target" ];
210 after = [ "network.target" ];
211 restartTriggers = optional (cfg.configFile != null) cfg.configFile;
212 path = lib.optional (lib.hasPrefix "if," cfg.use) pkgs.iproute2;
213
214 serviceConfig = {
215 DynamicUser = true;
216 RuntimeDirectoryMode = "0700";
217 inherit RuntimeDirectory;
218 inherit StateDirectory;
219 Type = "oneshot";
220 ExecStartPre = [ "!${pkgs.writeShellScript "ddclient-prestart" preStart}" ];
221 ExecStart = "${lib.getExe cfg.package} -file /run/${RuntimeDirectory}/ddclient.conf";
222 };
223 };
224
225 systemd.timers.ddclient = {
226 description = "Run ddclient";
227 wantedBy = [ "timers.target" ];
228 timerConfig = {
229 OnBootSec = cfg.interval;
230 OnUnitInactiveSec = cfg.interval;
231 };
232 };
233 };
234}