1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.adguardhome;
7
8 args = concatStringsSep " " ([
9 "--no-check-update"
10 "--pidfile /run/AdGuardHome/AdGuardHome.pid"
11 "--work-dir /var/lib/AdGuardHome/"
12 "--config /var/lib/AdGuardHome/AdGuardHome.yaml"
13 ] ++ cfg.extraArgs);
14
15 configFile = pkgs.writeTextFile {
16 name = "AdGuardHome.yaml";
17 text = builtins.toJSON cfg.settings;
18 checkPhase = "${pkgs.adguardhome}/bin/adguardhome -c $out --check-config";
19 };
20
21in
22{
23
24 imports =
25 let cfgPath = [ "services" "adguardhome" ];
26 in
27 [
28 (mkRenamedOptionModuleWith { sinceRelease = 2211; from = cfgPath ++ [ "host" ]; to = cfgPath ++ [ "settings" "bind_host" ]; })
29 (mkRenamedOptionModuleWith { sinceRelease = 2211; from = cfgPath ++ [ "port" ]; to = cfgPath ++ [ "settings" "bind_port" ]; })
30 ];
31
32 options.services.adguardhome = with types; {
33 enable = mkEnableOption (lib.mdDoc "AdGuard Home network-wide ad blocker");
34
35 openFirewall = mkOption {
36 default = false;
37 type = bool;
38 description = lib.mdDoc ''
39 Open ports in the firewall for the AdGuard Home web interface. Does not
40 open the port needed to access the DNS resolver.
41 '';
42 };
43
44 mutableSettings = mkOption {
45 default = true;
46 type = bool;
47 description = lib.mdDoc ''
48 Allow changes made on the AdGuard Home web interface to persist between
49 service restarts.
50 '';
51 };
52
53 settings = mkOption {
54 default = null;
55 type = nullOr (submodule {
56 freeformType = (pkgs.formats.yaml { }).type;
57 options = {
58 schema_version = mkOption {
59 default = pkgs.adguardhome.schema_version;
60 defaultText = literalExpression "pkgs.adguardhome.schema_version";
61 type = int;
62 description = lib.mdDoc ''
63 Schema version for the configuration.
64 Defaults to the `schema_version` supplied by `pkgs.adguardhome`.
65 '';
66 };
67 bind_host = mkOption {
68 default = "0.0.0.0";
69 type = str;
70 description = lib.mdDoc ''
71 Host address to bind HTTP server to.
72 '';
73 };
74 bind_port = mkOption {
75 default = 3000;
76 type = port;
77 description = lib.mdDoc ''
78 Port to serve HTTP pages on.
79 '';
80 };
81 };
82 });
83 description = lib.mdDoc ''
84 AdGuard Home configuration. Refer to
85 <https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#configuration-file>
86 for details on supported values.
87
88 ::: {.note}
89 On start and if {option}`mutableSettings` is `true`,
90 these options are merged into the configuration file on start, taking
91 precedence over configuration changes made on the web interface.
92
93 Set this to `null` (default) for a non-declarative configuration without any
94 Nix-supplied values.
95 Declarative configurations are supplied with a default `schema_version`, `bind_host`, and `bind_port`.
96 :::
97 '';
98 };
99
100 extraArgs = mkOption {
101 default = [ ];
102 type = listOf str;
103 description = lib.mdDoc ''
104 Extra command line parameters to be passed to the adguardhome binary.
105 '';
106 };
107 };
108
109 config = mkIf cfg.enable {
110 assertions = [
111 {
112 assertion = cfg.settings != null -> cfg.mutableSettings
113 || (hasAttrByPath [ "dns" "bind_host" ] cfg.settings)
114 || (hasAttrByPath [ "dns" "bind_hosts" ] cfg.settings);
115 message =
116 "AdGuard setting dns.bind_host or dns.bind_hosts needs to be configured for a minimal working configuration";
117 }
118 {
119 assertion = cfg.settings != null -> cfg.mutableSettings
120 || hasAttrByPath [ "dns" "bootstrap_dns" ] cfg.settings;
121 message =
122 "AdGuard setting dns.bootstrap_dns needs to be configured for a minimal working configuration";
123 }
124 ];
125
126 systemd.services.adguardhome = {
127 description = "AdGuard Home: Network-level blocker";
128 after = [ "network.target" ];
129 wantedBy = [ "multi-user.target" ];
130 unitConfig = {
131 StartLimitIntervalSec = 5;
132 StartLimitBurst = 10;
133 };
134
135 preStart = optionalString (cfg.settings != null) ''
136 if [ -e "$STATE_DIRECTORY/AdGuardHome.yaml" ] \
137 && [ "${toString cfg.mutableSettings}" = "1" ]; then
138 # Writing directly to AdGuardHome.yaml results in empty file
139 ${pkgs.yaml-merge}/bin/yaml-merge "$STATE_DIRECTORY/AdGuardHome.yaml" "${configFile}" > "$STATE_DIRECTORY/AdGuardHome.yaml.tmp"
140 mv "$STATE_DIRECTORY/AdGuardHome.yaml.tmp" "$STATE_DIRECTORY/AdGuardHome.yaml"
141 else
142 cp --force "${configFile}" "$STATE_DIRECTORY/AdGuardHome.yaml"
143 chmod 600 "$STATE_DIRECTORY/AdGuardHome.yaml"
144 fi
145 '';
146
147 serviceConfig = {
148 DynamicUser = true;
149 ExecStart = "${pkgs.adguardhome}/bin/adguardhome ${args}";
150 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
151 Restart = "always";
152 RestartSec = 10;
153 RuntimeDirectory = "AdGuardHome";
154 StateDirectory = "AdGuardHome";
155 };
156 };
157
158 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.bind_port ];
159 };
160}