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 };
327
328 systemd.tmpfiles.rules = [ "d /run/avahi-daemon - avahi avahi -" ];
329
330 systemd.services.avahi-daemon = {
331 description = "Avahi mDNS/DNS-SD Stack";
332 wantedBy = [ "multi-user.target" ];
333 requires = [ "avahi-daemon.socket" ];
334 documentation = [
335 "man:avahi-daemon(8)"
336 "man:avahi-daemon.conf(5)"
337 "man:avahi.hosts(5)"
338 "man:avahi.service(5)"
339 ];
340
341 # Make NSS modules visible so that `avahi_nss_support ()' can
342 # return a sensible value.
343 environment.LD_LIBRARY_PATH = config.system.nssModules.path;
344
345 path = [
346 pkgs.coreutils
347 cfg.package
348 ];
349
350 serviceConfig = {
351 NotifyAccess = "main";
352 BusName = "org.freedesktop.Avahi";
353 Type = "dbus";
354 ExecStart = "${cfg.package}/sbin/avahi-daemon --syslog -f ${avahiDaemonConf}";
355 ConfigurationDirectory = "avahi/services";
356
357 # Hardening
358 CapabilityBoundingSet = [
359 # https://github.com/avahi/avahi/blob/v0.9-rc1/avahi-daemon/caps.c#L38
360 "CAP_SYS_CHROOT"
361 "CAP_SETUID"
362 "CAP_SETGID"
363 ];
364 DevicePolicy = "closed";
365 LockPersonality = true;
366 MemoryDenyWriteExecute = true;
367 NoNewPrivileges = true;
368 PrivateDevices = true;
369 PrivateTmp = true;
370 PrivateUsers = false;
371 ProcSubset = "pid";
372 ProtectClock = true;
373 ProtectControlGroups = true;
374 ProtectHome = true;
375 ProtectHostname = true;
376 ProtectKernelLogs = true;
377 ProtectKernelModules = true;
378 ProtectKernelTunables = true;
379 ProtectProc = "invisible";
380 ProtectSystem = "strict";
381 RestrictAddressFamilies = [
382 "AF_INET"
383 "AF_INET6"
384 "AF_NETLINK"
385 "AF_UNIX"
386 ];
387 RestrictNamespaces = true;
388 RestrictRealtime = true;
389 RestrictSUIDSGID = true;
390 SystemCallArchitectures = "native";
391 SystemCallFilter = [
392 "@system-service"
393 "~@privileged"
394 "@chown setgroups setresuid"
395 ];
396 UMask = "0077";
397 };
398 };
399
400 services.dbus.enable = true;
401 services.dbus.packages = [ cfg.package ];
402
403 networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ 5353 ];
404 };
405}