1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.avahi;
9
10 yesNo = yes: if yes then "yes" else "no";
11
12 avahiDaemonConf =
13 with cfg;
14 pkgs.writeText "avahi-daemon.conf" ''
15 [server]
16 ${
17 # Users can set `networking.hostName' to the empty string, when getting
18 # a host name from DHCP. In that case, let Avahi take whatever the
19 # current host name is; setting `host-name' to the empty string in
20 # `avahi-daemon.conf' would be invalid.
21 lib.optionalString (hostName != "") "host-name=${hostName}"
22 }
23 browse-domains=${lib.concatStringsSep ", " browseDomains}
24 use-ipv4=${yesNo ipv4}
25 use-ipv6=${yesNo ipv6}
26 ${lib.optionalString (
27 allowInterfaces != null
28 ) "allow-interfaces=${lib.concatStringsSep "," allowInterfaces}"}
29 ${lib.optionalString (
30 denyInterfaces != null
31 ) "deny-interfaces=${lib.concatStringsSep "," denyInterfaces}"}
32 ${lib.optionalString (domainName != null) "domain-name=${domainName}"}
33 allow-point-to-point=${yesNo allowPointToPoint}
34 ${lib.optionalString (cacheEntriesMax != null) "cache-entries-max=${toString cacheEntriesMax}"}
35
36 [wide-area]
37 enable-wide-area=${yesNo wideArea}
38
39 [publish]
40 disable-publishing=${yesNo (!publish.enable)}
41 disable-user-service-publishing=${yesNo (!publish.userServices)}
42 publish-addresses=${yesNo (publish.userServices || publish.addresses)}
43 publish-hinfo=${yesNo publish.hinfo}
44 publish-workstation=${yesNo publish.workstation}
45 publish-domain=${yesNo publish.domain}
46
47 [reflector]
48 enable-reflector=${yesNo reflector}
49 ${extraConfig}
50 '';
51in
52{
53 imports = [
54 (lib.mkRenamedOptionModule
55 [ "services" "avahi" "interfaces" ]
56 [ "services" "avahi" "allowInterfaces" ]
57 )
58 (lib.mkRenamedOptionModule [ "services" "avahi" "nssmdns" ] [ "services" "avahi" "nssmdns4" ])
59 ];
60
61 options.services.avahi = {
62 enable = lib.mkOption {
63 type = lib.types.bool;
64 default = false;
65 description = ''
66 Whether to run the Avahi daemon, which allows Avahi clients
67 to use Avahi's service discovery facilities and also allows
68 the local machine to advertise its presence and services
69 (through the mDNS responder implemented by `avahi-daemon`).
70 '';
71 };
72
73 package = lib.mkPackageOption pkgs "avahi" { };
74
75 hostName = lib.mkOption {
76 type = lib.types.str;
77 default = config.networking.hostName;
78 defaultText = lib.literalExpression "config.networking.hostName";
79 description = ''
80 Host name advertised on the LAN. If not set, avahi will use the value
81 of {option}`config.networking.hostName`.
82 '';
83 };
84
85 domainName = lib.mkOption {
86 type = lib.types.str;
87 default = "local";
88 description = ''
89 Domain name for all advertisements.
90 '';
91 };
92
93 browseDomains = lib.mkOption {
94 type = lib.types.listOf lib.types.str;
95 default = [ ];
96 example = [
97 "0pointer.de"
98 "zeroconf.org"
99 ];
100 description = ''
101 List of non-local DNS domains to be browsed.
102 '';
103 };
104
105 ipv4 = lib.mkOption {
106 type = lib.types.bool;
107 default = true;
108 description = "Whether to use IPv4.";
109 };
110
111 ipv6 = lib.mkOption {
112 type = lib.types.bool;
113 default = config.networking.enableIPv6;
114 defaultText = lib.literalExpression "config.networking.enableIPv6";
115 description = "Whether to use IPv6.";
116 };
117
118 allowInterfaces = lib.mkOption {
119 type = lib.types.nullOr (lib.types.listOf lib.types.str);
120 default = null;
121 description = ''
122 List of network interfaces that should be used by the {command}`avahi-daemon`.
123 Other interfaces will be ignored. If `null`, all local interfaces
124 except loopback and point-to-point will be used.
125 '';
126 };
127
128 denyInterfaces = lib.mkOption {
129 type = lib.types.nullOr (lib.types.listOf lib.types.str);
130 default = null;
131 description = ''
132 List of network interfaces that should be ignored by the
133 {command}`avahi-daemon`. Other unspecified interfaces will be used,
134 unless {option}`allowInterfaces` is set. This option takes precedence
135 over {option}`allowInterfaces`.
136 '';
137 };
138
139 openFirewall = lib.mkOption {
140 type = lib.types.bool;
141 default = true;
142 description = ''
143 Whether to open the firewall for UDP port 5353.
144 Disabling this setting also disables discovering of network devices.
145 '';
146 };
147
148 allowPointToPoint = lib.mkOption {
149 type = lib.types.bool;
150 default = false;
151 description = ''
152 Whether to use POINTTOPOINT interfaces. Might make mDNS unreliable due to usually large
153 latencies with such links and opens a potential security hole by allowing mDNS access from Internet
154 connections.
155 '';
156 };
157
158 wideArea = lib.mkOption {
159 type = lib.types.bool;
160 default = true;
161 description = "Whether to enable wide-area service discovery.";
162 };
163
164 reflector = lib.mkOption {
165 type = lib.types.bool;
166 default = false;
167 description = "Reflect incoming mDNS requests to all allowed network interfaces.";
168 };
169
170 extraServiceFiles = lib.mkOption {
171 type = with lib.types; attrsOf (either str path);
172 default = { };
173 example = lib.literalExpression ''
174 {
175 ssh = "''${pkgs.avahi}/etc/avahi/services/ssh.service";
176 smb = '''
177 <?xml version="1.0" standalone='no'?><!--*-nxml-*-->
178 <!DOCTYPE service-group SYSTEM "avahi-service.dtd">
179 <service-group>
180 <name replace-wildcards="yes">%h</name>
181 <service>
182 <type>_smb._tcp</type>
183 <port>445</port>
184 </service>
185 </service-group>
186 ''';
187 }
188 '';
189 description = ''
190 Specify custom service definitions which are placed in the avahi service directory.
191 See the {manpage}`avahi.service(5)` manpage for detailed information.
192 '';
193 };
194
195 publish = {
196 enable = lib.mkOption {
197 type = lib.types.bool;
198 default = false;
199 description = "Whether to allow publishing in general.";
200 };
201
202 userServices = lib.mkOption {
203 type = lib.types.bool;
204 default = false;
205 description = "Whether to publish user services. Will set `addresses=true`.";
206 };
207
208 addresses = lib.mkOption {
209 type = lib.types.bool;
210 default = false;
211 description = "Whether to register mDNS address records for all local IP addresses.";
212 };
213
214 hinfo = lib.mkOption {
215 type = lib.types.bool;
216 default = false;
217 description = ''
218 Whether to register a mDNS HINFO record which contains information about the
219 local operating system and CPU.
220 '';
221 };
222
223 workstation = lib.mkOption {
224 type = lib.types.bool;
225 default = false;
226 description = ''
227 Whether to register a service of type "_workstation._tcp" on the local LAN.
228 '';
229 };
230
231 domain = lib.mkOption {
232 type = lib.types.bool;
233 default = false;
234 description = "Whether to announce the locally used domain name for browsing by other hosts.";
235 };
236 };
237
238 nssmdns4 = lib.mkOption {
239 type = lib.types.bool;
240 default = false;
241 description = ''
242 Whether to enable the mDNS NSS (Name Service Switch) plug-in for IPv4.
243 Enabling it allows applications to resolve names in the `.local`
244 domain by transparently querying the Avahi daemon.
245 '';
246 };
247
248 nssmdns6 = lib.mkOption {
249 type = lib.types.bool;
250 default = false;
251 description = ''
252 Whether to enable the mDNS NSS (Name Service Switch) plug-in for IPv6.
253 Enabling it allows applications to resolve names in the `.local`
254 domain by transparently querying the Avahi daemon.
255
256 ::: {.note}
257 Due to the fact that most mDNS responders only register local IPv4 addresses,
258 most user want to leave this option disabled to avoid long timeouts when applications first resolve the none existing IPv6 address.
259 :::
260 '';
261 };
262
263 cacheEntriesMax = lib.mkOption {
264 type = lib.types.nullOr lib.types.int;
265 default = null;
266 description = ''
267 Number of resource records to be cached per interface. Use 0 to
268 disable caching. Avahi daemon defaults to 4096 if not set.
269 '';
270 };
271
272 extraConfig = lib.mkOption {
273 type = lib.types.lines;
274 default = "";
275 description = ''
276 Extra config to append to avahi-daemon.conf.
277 '';
278 };
279 };
280
281 config = lib.mkIf cfg.enable {
282 users.users.avahi = {
283 description = "avahi-daemon privilege separation user";
284 home = "/var/empty";
285 group = "avahi";
286 isSystemUser = true;
287 };
288
289 users.groups.avahi = { };
290
291 system.nssModules = lib.optional (cfg.nssmdns4 || cfg.nssmdns6) pkgs.nssmdns;
292 system.nssDatabases.hosts =
293 let
294 mdns =
295 if (cfg.nssmdns4 && cfg.nssmdns6) then
296 "mdns"
297 else if (!cfg.nssmdns4 && cfg.nssmdns6) then
298 "mdns6"
299 else if (cfg.nssmdns4 && !cfg.nssmdns6) then
300 "mdns4"
301 else
302 "";
303 in
304 lib.optionals (cfg.nssmdns4 || cfg.nssmdns6) (
305 lib.mkMerge [
306 (lib.mkBefore [ "${mdns}_minimal [NOTFOUND=return]" ]) # before resolve
307 (lib.mkAfter [ "${mdns}" ]) # after dns
308 ]
309 );
310
311 environment.systemPackages = [ cfg.package ];
312
313 environment.etc = (
314 lib.mapAttrs' (
315 n: v:
316 lib.nameValuePair "avahi/services/${n}.service" {
317 ${if lib.types.path.check v then "source" else "text"} = v;
318 }
319 ) cfg.extraServiceFiles
320 );
321
322 systemd.sockets.avahi-daemon = {
323 description = "Avahi mDNS/DNS-SD Stack Activation Socket";
324 listenStreams = [ "/run/avahi-daemon/socket" ];
325 wantedBy = [ "sockets.target" ];
326 after = [
327 # Ensure that `/run/avahi-daemon` owned by `avahi` is created by `systemd.tmpfiles.rules` before the `avahi-daemon.socket`,
328 # otherwise `avahi-daemon.socket` will automatically create it owned by `root`, which will cause `avahi-daemon.service` to fail.
329 "systemd-tmpfiles-setup.service"
330 ];
331 };
332
333 systemd.tmpfiles.rules = [ "d /run/avahi-daemon - avahi avahi -" ];
334
335 systemd.services.avahi-daemon = {
336 description = "Avahi mDNS/DNS-SD Stack";
337 wantedBy = [ "multi-user.target" ];
338 requires = [ "avahi-daemon.socket" ];
339 documentation = [
340 "man:avahi-daemon(8)"
341 "man:avahi-daemon.conf(5)"
342 "man:avahi.hosts(5)"
343 "man:avahi.service(5)"
344 ];
345
346 # Make NSS modules visible so that `avahi_nss_support ()' can
347 # return a sensible value.
348 environment.LD_LIBRARY_PATH = config.system.nssModules.path;
349
350 path = [
351 pkgs.coreutils
352 cfg.package
353 ];
354
355 serviceConfig = {
356 NotifyAccess = "main";
357 BusName = "org.freedesktop.Avahi";
358 Type = "dbus";
359 ExecStart = "${cfg.package}/sbin/avahi-daemon --syslog -f ${avahiDaemonConf}";
360 ConfigurationDirectory = "avahi/services";
361
362 # Hardening
363 CapabilityBoundingSet = [
364 # https://github.com/avahi/avahi/blob/v0.9-rc1/avahi-daemon/caps.c#L38
365 "CAP_SYS_CHROOT"
366 "CAP_SETUID"
367 "CAP_SETGID"
368 ];
369 DevicePolicy = "closed";
370 LockPersonality = true;
371 MemoryDenyWriteExecute = true;
372 NoNewPrivileges = true;
373 PrivateDevices = true;
374 PrivateTmp = true;
375 PrivateUsers = false;
376 ProcSubset = "pid";
377 ProtectClock = true;
378 ProtectControlGroups = true;
379 ProtectHome = true;
380 ProtectHostname = true;
381 ProtectKernelLogs = true;
382 ProtectKernelModules = true;
383 ProtectKernelTunables = true;
384 ProtectProc = "invisible";
385 ProtectSystem = "strict";
386 RestrictAddressFamilies = [
387 "AF_INET"
388 "AF_INET6"
389 "AF_NETLINK"
390 "AF_UNIX"
391 ];
392 RestrictNamespaces = true;
393 RestrictRealtime = true;
394 RestrictSUIDSGID = true;
395 SystemCallArchitectures = "native";
396 SystemCallFilter = [
397 "@system-service"
398 "~@privileged"
399 "@chown setgroups setresuid"
400 ];
401 UMask = "0077";
402 };
403 };
404
405 services.dbus.enable = true;
406 services.dbus.packages = [ cfg.package ];
407
408 networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ 5353 ];
409 };
410}