1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.inadyn;
9
10 # check if a value of an attrset is not null or an empty collection
11 nonEmptyValue = _: v: v != null && v != [ ] && v != { };
12
13 renderOption =
14 k: v:
15 if
16 builtins.elem k [
17 "provider"
18 "custom"
19 ]
20 then
21 lib.concatStringsSep "\n" (
22 lib.mapAttrsToList (name: config: ''
23 ${k} ${name} {
24 ${lib.concatStringsSep "\n " (
25 lib.mapAttrsToList renderOption (lib.filterAttrs nonEmptyValue config)
26 )}
27 }'') v
28 )
29 else if k == "include" then
30 "${k}(\"${v}\")"
31 else if k == "hostname" && builtins.isList v then
32 "${k} = { ${builtins.concatStringsSep ", " (map (s: "\"${s}\"") v)} }"
33 else if builtins.isBool v then
34 "${k} = ${lib.boolToString v}"
35 else if builtins.isString v then
36 "${k} = \"${v}\""
37 else
38 "${k} = ${toString v}";
39
40 configFile' = pkgs.writeText "inadyn.conf" ''
41 # This file was generated by nix
42 # do not edit
43
44 ${
45 (lib.concatStringsSep "\n" (
46 lib.mapAttrsToList renderOption (lib.filterAttrs nonEmptyValue cfg.settings)
47 ))
48 }
49 '';
50
51 configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
52in
53{
54 options.services.inadyn =
55 with lib.types;
56 let
57 providerOptions = {
58 include = lib.mkOption {
59 default = null;
60 description = "File to include additional settings for this provider from.";
61 type = nullOr path;
62 };
63 ssl = lib.mkOption {
64 default = true;
65 description = "Whether to use HTTPS for this DDNS provider.";
66 type = bool;
67 };
68 username = lib.mkOption {
69 default = null;
70 description = "Username for this DDNS provider.";
71 type = nullOr str;
72 };
73 password = lib.mkOption {
74 default = null;
75 description = ''
76 Password for this DDNS provider.
77
78 WARNING: This will be world-readable in the nix store.
79 To store credentials securely, use the `include` or `configFile` options.
80 '';
81 type = nullOr str;
82 };
83 hostname = lib.mkOption {
84 default = "*";
85 example = "your.cool-domain.com";
86 description = "Hostname alias(es).";
87 type = either str (listOf str);
88 };
89 };
90 in
91 {
92 enable = lib.mkEnableOption (''
93 synchronise your machine's IP address with a dynamic DNS provider using inadyn
94 '');
95 user = lib.mkOption {
96 default = "inadyn";
97 type = lib.types.str;
98 description = ''
99 User account under which inadyn runs.
100
101 ::: {.note}
102 If left as the default value this user will automatically be created
103 on system activation, otherwise you are responsible for
104 ensuring the user exists before the inadyn service starts.
105 :::
106 '';
107 };
108 group = lib.mkOption {
109 default = "inadyn";
110 type = lib.types.str;
111 description = ''
112 Group account under which inadyn runs.
113
114 ::: {.note}
115 If left as the default value this user will automatically be created
116 on system activation, otherwise you are responsible for
117 ensuring the user exists before the inadyn service starts.
118 :::
119 '';
120 };
121 interval = lib.mkOption {
122 default = "*-*-* *:*:00";
123 description = ''
124 How often to check the current IP.
125 Uses the format described in {manpage}`systemd.time(7)`";
126 '';
127 type = str;
128 };
129 logLevel = lib.mkOption {
130 type = lib.types.enum [
131 "none"
132 "err"
133 "warning"
134 "info"
135 "notice"
136 "debug"
137 ];
138 default = "notice";
139 description = "Set inadyn's log level.";
140 };
141 settings = lib.mkOption {
142 default = { };
143 description = "See `inadyn.conf (5)`";
144 type = submodule {
145 freeformType = attrs;
146 options = {
147 allow-ipv6 = lib.mkOption {
148 default = config.networking.enableIPv6;
149 defaultText = lib.literalExpression "config.networking.enableIPv6";
150 description = "Whether to get IPv6 addresses from interfaces.";
151 type = bool;
152 };
153 forced-update = lib.mkOption {
154 default = 2592000;
155 description = "Duration (in seconds) after which an update is forced.";
156 type = ints.positive;
157 };
158 provider = lib.mkOption {
159 default = { };
160 description = ''
161 Settings for DDNS providers built-in to inadyn.
162
163 For a list of built-in providers, see `inadyn.conf (5)`.
164 '';
165 type = attrsOf (submodule {
166 freeformType = attrs;
167 options = providerOptions;
168 });
169 };
170 custom = lib.mkOption {
171 default = { };
172 description = ''
173 Settings for custom DNS providers.
174 '';
175 type = attrsOf (submodule {
176 freeformType = attrs;
177 options = providerOptions // {
178 ddns-server = lib.mkOption {
179 description = "DDNS server name.";
180 type = str;
181 };
182 ddns-path = lib.mkOption {
183 description = ''
184 DDNS server path.
185
186 See `inadnyn.conf (5)` for a list for format specifiers that can be used.
187 '';
188 example = "/update?user=%u&password=%p&domain=%h&myip=%i";
189 type = str;
190 };
191 };
192 });
193 };
194 };
195 };
196 };
197 configFile = lib.mkOption {
198 default = null;
199 description = ''
200 Configuration file for inadyn.
201
202 Setting this will override all other configuration options.
203
204 Passed to the inadyn service using LoadCredential.
205 '';
206 type = nullOr path;
207 };
208 };
209
210 config = lib.mkIf cfg.enable {
211 systemd = {
212 services.inadyn = {
213 description = "Update nameservers using inadyn";
214 documentation = [
215 "man:inadyn"
216 "man:inadyn.conf"
217 "file:${pkgs.inadyn}/share/doc/inadyn/README.md"
218 ];
219 requires = [ "network-online.target" ];
220 wantedBy = [ "multi-user.target" ];
221 startAt = cfg.interval;
222 serviceConfig = {
223 Type = "oneshot";
224 ExecStart = ''${lib.getExe pkgs.inadyn} -f ${configFile} --cache-dir ''${CACHE_DIRECTORY} -1 --foreground -l ${cfg.logLevel}'';
225 LoadCredential = "config:${configFile}";
226 CacheDirectory = "inadyn";
227
228 User = cfg.user;
229 Group = cfg.group;
230 UMask = "0177";
231 LockPersonality = true;
232 MemoryDenyWriteExecute = true;
233 RestrictAddressFamilies = "AF_INET AF_INET6 AF_NETLINK";
234 NoNewPrivileges = true;
235 PrivateDevices = true;
236 PrivateTmp = true;
237 PrivateUsers = true;
238 ProtectSystem = "strict";
239 ProtectProc = "invisible";
240 ProtectHome = true;
241 ProtectClock = true;
242 ProtectControlGroups = true;
243 ProtectHostname = true;
244 ProtectKernelLogs = true;
245 ProtectKernelModules = true;
246 ProtectKernelTunables = true;
247 RestrictNamespaces = true;
248 RestrictRealtime = true;
249 RestrictSUIDSGID = true;
250 SystemCallArchitectures = "native";
251 SystemCallErrorNumber = "EPERM";
252 SystemCallFilter = "@system-service";
253 CapabilityBoundingSet = "";
254 };
255 };
256
257 timers.inadyn.timerConfig.Persistent = true;
258 };
259
260 users.users.inadyn = lib.mkIf (cfg.user == "inadyn") {
261 group = cfg.group;
262 isSystemUser = true;
263 };
264
265 users.groups = lib.mkIf (cfg.group == "inadyn") {
266 inadyn = { };
267 };
268 };
269}