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