1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.adguardhome;
9 settingsFormat = pkgs.formats.yaml { };
10
11 args = lib.concatStringsSep " " (
12 [
13 "--no-check-update"
14 "--pidfile /run/AdGuardHome/AdGuardHome.pid"
15 "--work-dir /var/lib/AdGuardHome/"
16 "--config /var/lib/AdGuardHome/AdGuardHome.yaml"
17 ]
18 ++ cfg.extraArgs
19 );
20
21 settings =
22 if (cfg.settings != null) then
23 cfg.settings
24 // (
25 if cfg.settings.schema_version < 23 then
26 {
27 bind_host = cfg.host;
28 bind_port = cfg.port;
29 }
30 else
31 {
32 http.address = "${cfg.host}:${toString cfg.port}";
33 }
34 )
35 else
36 null;
37
38 configFile = (settingsFormat.generate "AdGuardHome.yaml" settings).overrideAttrs (_: {
39 checkPhase = "${cfg.package}/bin/adguardhome -c $out --check-config";
40 });
41in
42{
43 options.services.adguardhome = with lib.types; {
44 enable = lib.mkEnableOption "AdGuard Home network-wide ad blocker";
45
46 package = lib.mkOption {
47 type = package;
48 default = pkgs.adguardhome;
49 defaultText = lib.literalExpression "pkgs.adguardhome";
50 description = ''
51 The package that runs adguardhome.
52 '';
53 };
54
55 openFirewall = lib.mkOption {
56 default = false;
57 type = bool;
58 description = ''
59 Open ports in the firewall for the AdGuard Home web interface. Does not
60 open the port needed to access the DNS resolver.
61 '';
62 };
63
64 allowDHCP = lib.mkOption {
65 default = settings.dhcp.enabled or false;
66 defaultText = lib.literalExpression "config.services.adguardhome.settings.dhcp.enabled or false";
67 type = bool;
68 description = ''
69 Allows AdGuard Home to open raw sockets (`CAP_NET_RAW`), which is
70 required for the integrated DHCP server.
71
72 The default enables this conditionally if the declarative configuration
73 enables the integrated DHCP server. Manually setting this option is only
74 required for non-declarative setups.
75 '';
76 };
77
78 mutableSettings = lib.mkOption {
79 default = true;
80 type = bool;
81 description = ''
82 Allow changes made on the AdGuard Home web interface to persist between
83 service restarts.
84 '';
85 };
86
87 host = lib.mkOption {
88 default = "0.0.0.0";
89 type = str;
90 description = ''
91 Host address to bind HTTP server to.
92 '';
93 };
94
95 port = lib.mkOption {
96 default = 3000;
97 type = port;
98 description = ''
99 Port to serve HTTP pages on.
100 '';
101 };
102
103 settings = lib.mkOption {
104 default = null;
105 type = nullOr (submodule {
106 freeformType = settingsFormat.type;
107 options = {
108 schema_version = lib.mkOption {
109 default = cfg.package.schema_version;
110 defaultText = lib.literalExpression "cfg.package.schema_version";
111 type = int;
112 description = ''
113 Schema version for the configuration.
114 Defaults to the `schema_version` supplied by `cfg.package`.
115 '';
116 };
117 };
118 });
119 description = ''
120 AdGuard Home configuration. Refer to
121 <https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#configuration-file>
122 for details on supported values.
123
124 ::: {.note}
125 On start and if {option}`mutableSettings` is `true`,
126 these options are merged into the configuration file on start, taking
127 precedence over configuration changes made on the web interface.
128
129 Set this to `null` (default) for a non-declarative configuration without any
130 Nix-supplied values.
131 Declarative configurations are supplied with a default `schema_version`, and `http.address`.
132 :::
133 '';
134 };
135
136 extraArgs = lib.mkOption {
137 default = [ ];
138 type = listOf str;
139 description = ''
140 Extra command line parameters to be passed to the adguardhome binary.
141 '';
142 };
143 };
144
145 config = lib.mkIf cfg.enable {
146 assertions = [
147 {
148 assertion = cfg.settings != null -> !(lib.hasAttrByPath [ "bind_host" ] cfg.settings);
149 message = "AdGuard option `settings.bind_host' has been superseded by `services.adguardhome.host'";
150 }
151 {
152 assertion = cfg.settings != null -> !(lib.hasAttrByPath [ "bind_port" ] cfg.settings);
153 message = "AdGuard option `settings.bind_port' has been superseded by `services.adguardhome.port'";
154 }
155 {
156 assertion =
157 settings != null -> cfg.mutableSettings || lib.hasAttrByPath [ "dns" "bootstrap_dns" ] settings;
158 message = "AdGuard setting dns.bootstrap_dns needs to be configured for a minimal working configuration";
159 }
160 {
161 assertion =
162 settings != null
163 ->
164 cfg.mutableSettings
165 || lib.hasAttrByPath [ "dns" "bootstrap_dns" ] settings && lib.isList settings.dns.bootstrap_dns;
166 message = "AdGuard setting dns.bootstrap_dns needs to be a list";
167 }
168 ];
169
170 systemd.services.adguardhome = {
171 description = "AdGuard Home: Network-level blocker";
172 after = [ "network.target" ];
173 wantedBy = [ "multi-user.target" ];
174 unitConfig = {
175 StartLimitIntervalSec = 5;
176 StartLimitBurst = 10;
177 };
178
179 preStart = lib.optionalString (settings != null) ''
180 if [ -e "$STATE_DIRECTORY/AdGuardHome.yaml" ] \
181 && [ "${toString cfg.mutableSettings}" = "1" ]; then
182 # First run a schema_version update on the existing configuration
183 # This ensures that both the new config and the existing one have the same schema_version
184 # Note: --check-config has the side effect of modifying the file at rest!
185 ${lib.getExe cfg.package} -c "$STATE_DIRECTORY/AdGuardHome.yaml" --check-config
186
187 # Writing directly to AdGuardHome.yaml results in empty file
188 ${lib.getExe pkgs.yaml-merge} "$STATE_DIRECTORY/AdGuardHome.yaml" "${configFile}" > "$STATE_DIRECTORY/AdGuardHome.yaml.tmp"
189 mv "$STATE_DIRECTORY/AdGuardHome.yaml.tmp" "$STATE_DIRECTORY/AdGuardHome.yaml"
190 else
191 cp --force "${configFile}" "$STATE_DIRECTORY/AdGuardHome.yaml"
192 chmod 600 "$STATE_DIRECTORY/AdGuardHome.yaml"
193 fi
194 '';
195
196 serviceConfig = {
197 DynamicUser = true;
198 ExecStart = "${lib.getExe cfg.package} ${args}";
199 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ] ++ lib.optionals cfg.allowDHCP [ "CAP_NET_RAW" ];
200 Restart = "always";
201 RestartSec = 10;
202 RuntimeDirectory = "AdGuardHome";
203 StateDirectory = "AdGuardHome";
204 };
205 };
206
207 networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
208 };
209}