494 lines
14 KiB
Nix
494 lines
14 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
with {
|
|
inherit (lib)
|
|
elemAt
|
|
getExe
|
|
hasAttrByPath
|
|
mkEnableOption
|
|
mkIf
|
|
mkOption
|
|
strings
|
|
types
|
|
;
|
|
};
|
|
|
|
let
|
|
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
|
|
|
|
cfg = config.services.pihole-ftl;
|
|
|
|
piholeScript = pkgs.writeScriptBin "pihole" ''
|
|
sudo=exec
|
|
if [[ "$USER" != '${cfg.user}' ]]; then
|
|
sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
|
|
fi
|
|
$sudo ${getExe cfg.piholePackage} "$@"
|
|
'';
|
|
|
|
settingsFormat = pkgs.formats.toml { };
|
|
settingsFile = settingsFormat.generate "pihole.toml" cfg.settings;
|
|
in
|
|
{
|
|
options.services.pihole-ftl = {
|
|
enable = mkEnableOption "Pi-hole FTL";
|
|
|
|
package = lib.mkPackageOption pkgs "pihole-ftl" { };
|
|
piholePackage = lib.mkPackageOption pkgs "pihole" { };
|
|
|
|
privacyLevel = mkOption {
|
|
type = types.numbers.between 0 3;
|
|
description = ''
|
|
Level of detail in generated statistics. 0 enables full statistics, 3
|
|
shows only anonymous statistics.
|
|
|
|
See [the documentation](https://docs.pi-hole.net/ftldns/privacylevels).
|
|
|
|
Also see services.dnsmasq.settings.log-queries to completely disable
|
|
query logging.
|
|
'';
|
|
default = 0;
|
|
example = "3";
|
|
};
|
|
|
|
openFirewallDNS = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Open ports in the firewall for pihole-FTL's DNS server.";
|
|
};
|
|
|
|
openFirewallDHCP = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Open ports in the firewall for pihole-FTL's DHCP server.";
|
|
};
|
|
|
|
openFirewallWebserver = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Open ports in the firewall for pihole-FTL's webserver, as configured in `settings.webserver.port`.
|
|
'';
|
|
};
|
|
|
|
configDirectory = mkOption {
|
|
type = types.path;
|
|
default = "/etc/pihole";
|
|
internal = true;
|
|
readOnly = true;
|
|
description = ''
|
|
Path for pihole configuration.
|
|
pihole does not currently support any path other than /etc/pihole.
|
|
'';
|
|
};
|
|
|
|
stateDirectory = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/pihole";
|
|
description = ''
|
|
Path for pihole state files.
|
|
'';
|
|
};
|
|
|
|
logDirectory = mkOption {
|
|
type = types.path;
|
|
default = "/var/log/pihole";
|
|
description = "Path for Pi-hole log files";
|
|
};
|
|
|
|
settings = mkOption {
|
|
type = settingsFormat.type;
|
|
description = ''
|
|
Configuration options for pihole.toml.
|
|
See the upstream [documentation](https://docs.pi-hole.net/ftldns/configfile).
|
|
'';
|
|
};
|
|
|
|
useDnsmasqConfig = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Import options defined in [](#opt-services.dnsmasq.settings) via
|
|
misc.dnsmasq_lines in Pi-hole's config.
|
|
'';
|
|
};
|
|
|
|
pihole = mkOption {
|
|
type = types.package;
|
|
default = piholeScript;
|
|
internal = true;
|
|
description = "Pi-hole admin script";
|
|
};
|
|
|
|
lists =
|
|
let
|
|
adlistType = types.submodule {
|
|
options = {
|
|
url = mkOption {
|
|
type = types.str;
|
|
description = "URL of the domain list";
|
|
};
|
|
type = mkOption {
|
|
type = types.enum [
|
|
"allow"
|
|
"block"
|
|
];
|
|
default = "block";
|
|
description = "Whether domains on this list should be explicitly allowed, or blocked";
|
|
};
|
|
enabled = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Whether this list is enabled";
|
|
};
|
|
description = mkOption {
|
|
type = types.str;
|
|
description = "Description of the list";
|
|
default = "";
|
|
};
|
|
};
|
|
};
|
|
in
|
|
mkOption {
|
|
type = with types; listOf adlistType;
|
|
description = "Deny (or allow) domain lists to use";
|
|
default = [ ];
|
|
example = [
|
|
{
|
|
url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts";
|
|
}
|
|
];
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "pihole";
|
|
description = "User to run the service as.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "pihole";
|
|
description = "Group to run the service as.";
|
|
};
|
|
|
|
queryLogDeleter = {
|
|
enable = mkEnableOption ("Pi-hole FTL DNS query log deleter");
|
|
|
|
age = mkOption {
|
|
type = types.int;
|
|
default = 90;
|
|
description = ''
|
|
Delete DNS query logs older than this many days, if
|
|
[](#opt-services.pihole-ftl.queryLogDeleter.enable) is on.
|
|
'';
|
|
};
|
|
|
|
interval = mkOption {
|
|
type = types.str;
|
|
default = "weekly";
|
|
description = ''
|
|
How often the query log deleter is run. See systemd.time(7) for more
|
|
information about the format.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = !config.services.dnsmasq.enable;
|
|
message = "pihole-ftl conflicts with dnsmasq. Please disable one of them.";
|
|
}
|
|
|
|
{
|
|
assertion =
|
|
builtins.length cfg.lists == 0
|
|
|| (
|
|
(hasAttrByPath [ "webserver" "port" ] cfg.settings)
|
|
&& !builtins.elem cfg.settings.webserver.port [
|
|
""
|
|
null
|
|
]
|
|
);
|
|
message = ''
|
|
The Pi-hole webserver must be enabled for lists set in services.pihole-ftl.lists to be automatically loaded on startup via the web API.
|
|
services.pihole-ftl.settings.port must be defined, e.g. by enabling services.pihole-web.enable and defining services.pihole-web.port.
|
|
'';
|
|
}
|
|
|
|
{
|
|
assertion =
|
|
builtins.length cfg.lists == 0
|
|
|| !(hasAttrByPath [ "webserver" "api" "cli_pw" ] cfg.settings)
|
|
|| cfg.settings.webserver.api.cli_pw == true;
|
|
message = ''
|
|
services.pihole-ftl.settings.webserver.api.cli_pw must be true for lists set in services.pihole-ftl.lists to be automatically loaded on startup.
|
|
This enables an ephemeral password used by the pihole command.
|
|
'';
|
|
}
|
|
];
|
|
|
|
services.pihole-ftl.settings = lib.mkMerge [
|
|
# Defaults
|
|
(mkDefaults {
|
|
misc.readOnly = true; # Prevent config changes via API or CLI by default
|
|
webserver.port = ""; # Disable the webserver by default
|
|
misc.privacyLevel = cfg.privacyLevel;
|
|
})
|
|
|
|
# Move state files to cfg.stateDirectory
|
|
{
|
|
# TODO: Pi-hole currently hardcodes dhcp-leasefile this in its
|
|
# generated dnsmasq.conf, and we can't override it
|
|
misc.dnsmasq_lines = [
|
|
# "dhcp-leasefile=${cfg.stateDirectory}/dhcp.leases"
|
|
# "hostsdir=${cfg.stateDirectory}/hosts"
|
|
];
|
|
|
|
files = {
|
|
database = "${cfg.stateDirectory}/pihole-FTL.db";
|
|
gravity = "${cfg.stateDirectory}/gravity.db";
|
|
macvendor = "${cfg.stateDirectory}/gravity.db";
|
|
log.ftl = "${cfg.logDirectory}/FTL.log";
|
|
log.dnsmasq = "${cfg.logDirectory}/pihole.log";
|
|
log.webserver = "${cfg.logDirectory}/webserver.log";
|
|
};
|
|
|
|
webserver.tls.cert = "${cfg.stateDirectory}/tls.pem";
|
|
}
|
|
|
|
(lib.optionalAttrs cfg.useDnsmasqConfig {
|
|
misc.dnsmasq_lines = lib.pipe config.services.dnsmasq.configFile [
|
|
builtins.readFile
|
|
(lib.strings.splitString "\n")
|
|
(builtins.filter (s: s != ""))
|
|
];
|
|
})
|
|
];
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.configDirectory} 0700 ${cfg.user} ${cfg.group} - -"
|
|
"d ${cfg.stateDirectory} 0700 ${cfg.user} ${cfg.group} - -"
|
|
"d ${cfg.logDirectory} 0700 ${cfg.user} ${cfg.group} - -"
|
|
];
|
|
|
|
systemd.services = {
|
|
pihole-ftl =
|
|
let
|
|
setupService = config.systemd.services.pihole-ftl-setup.name;
|
|
in
|
|
{
|
|
description = "Pi-hole FTL";
|
|
|
|
after = [ "network.target" ];
|
|
before = [ setupService ];
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ setupService ];
|
|
|
|
environment = {
|
|
# Currently unused, but allows the service to be reloaded
|
|
# automatically when the config is changed.
|
|
PIHOLE_CONFIG = settingsFile;
|
|
|
|
# pihole is executed by the /actions/gravity API endpoint
|
|
PATH = lib.mkForce (
|
|
lib.makeBinPath [
|
|
cfg.piholePackage
|
|
]
|
|
);
|
|
};
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
AmbientCapabilities = [
|
|
"CAP_NET_BIND_SERVICE"
|
|
"CAP_NET_RAW"
|
|
"CAP_NET_ADMIN"
|
|
"CAP_SYS_NICE"
|
|
"CAP_IPC_LOCK"
|
|
"CAP_CHOWN"
|
|
"CAP_SYS_TIME"
|
|
];
|
|
ExecStart = "${getExe cfg.package} no-daemon";
|
|
Restart = "on-failure";
|
|
RestartSec = 1;
|
|
# Hardening
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
DevicePolicy = "closed";
|
|
ProtectSystem = "strict";
|
|
ProtectHome = "read-only";
|
|
ProtectControlGroups = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ReadWritePaths = [
|
|
cfg.configDirectory
|
|
cfg.stateDirectory
|
|
cfg.logDirectory
|
|
];
|
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
MemoryDenyWriteExecute = true;
|
|
LockPersonality = true;
|
|
};
|
|
};
|
|
|
|
pihole-ftl-setup = {
|
|
description = "Pi-hole FTL setup";
|
|
# Wait for network so lists can be downloaded
|
|
after = [ "network-online.target" ];
|
|
requires = [ "network-online.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
|
|
# Hardening
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
DevicePolicy = "closed";
|
|
ProtectSystem = "strict";
|
|
ProtectHome = "read-only";
|
|
ProtectControlGroups = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ReadWritePaths = [
|
|
cfg.configDirectory
|
|
cfg.stateDirectory
|
|
cfg.logDirectory
|
|
];
|
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
MemoryDenyWriteExecute = true;
|
|
LockPersonality = true;
|
|
};
|
|
script = import ./pihole-ftl-setup-script.nix {
|
|
inherit
|
|
cfg
|
|
config
|
|
lib
|
|
pkgs
|
|
;
|
|
};
|
|
};
|
|
|
|
pihole-ftl-log-deleter = mkIf cfg.queryLogDeleter.enable {
|
|
description = "Pi-hole FTL DNS query log deleter";
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
# Hardening
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
DevicePolicy = "closed";
|
|
ProtectSystem = "strict";
|
|
ProtectHome = "read-only";
|
|
ProtectControlGroups = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ReadWritePaths = [ cfg.stateDirectory ];
|
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
MemoryDenyWriteExecute = true;
|
|
LockPersonality = true;
|
|
};
|
|
script =
|
|
let
|
|
days = toString cfg.queryLogDeleter.age;
|
|
database = "${cfg.stateDirectory}/pihole-FTL.db";
|
|
in
|
|
''
|
|
set -euo pipefail
|
|
|
|
echo "Deleting query logs older than ${days} days"
|
|
${getExe cfg.package} sqlite3 "${database}" "DELETE FROM query_storage WHERE timestamp <= CAST(strftime('%s', date('now', '-${days} day')) AS INT); select changes() from query_storage limit 1"
|
|
'';
|
|
};
|
|
};
|
|
|
|
systemd.timers.pihole-ftl-log-deleter = mkIf cfg.queryLogDeleter.enable {
|
|
description = "Pi-hole FTL DNS query log deleter";
|
|
before = [
|
|
config.systemd.services.pihole-ftl.name
|
|
config.systemd.services.pihole-ftl-setup.name
|
|
];
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnCalendar = cfg.queryLogDeleter.interval;
|
|
Unit = "pihole-ftl-log-deleter.service";
|
|
};
|
|
};
|
|
|
|
networking.firewall = lib.mkMerge [
|
|
(mkIf cfg.openFirewallDNS {
|
|
allowedUDPPorts = [ 53 ];
|
|
allowedTCPPorts = [ 53 ];
|
|
})
|
|
|
|
(mkIf cfg.openFirewallDHCP {
|
|
allowedUDPPorts = [ 67 ];
|
|
})
|
|
|
|
(mkIf cfg.openFirewallWebserver {
|
|
allowedTCPPorts = lib.pipe cfg.settings.webserver.port [
|
|
(lib.splitString ",")
|
|
(map (
|
|
port:
|
|
lib.pipe port [
|
|
(builtins.split "[[:alpha:]]+")
|
|
builtins.head
|
|
lib.toInt
|
|
]
|
|
))
|
|
];
|
|
})
|
|
];
|
|
|
|
users.users.${cfg.user} = {
|
|
group = cfg.group;
|
|
isSystemUser = true;
|
|
};
|
|
|
|
users.groups.${cfg.group} = { };
|
|
|
|
environment.etc."pihole/pihole.toml" = {
|
|
source = settingsFile;
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
mode = "400";
|
|
};
|
|
|
|
environment.systemPackages = [ cfg.pihole ];
|
|
|
|
services.logrotate.settings.pihole-ftl = {
|
|
enable = true;
|
|
files = [ "${cfg.logDirectory}/FTL.log" ];
|
|
};
|
|
};
|
|
|
|
meta = {
|
|
doc = ./pihole-ftl.md;
|
|
maintainers = with lib.maintainers; [ averyvigolo ];
|
|
};
|
|
}
|