pihole: init at various (#361571)

Adds pihole-ftl.service and pihole-ftl-log-deleter.service.

Authored-By: williamvds <william@williamvds.me>
This commit is contained in:
John Wiegley 2025-06-04 08:12:30 -07:00 committed by GitHub
commit 8922d4f099
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 2596 additions and 1 deletions

View file

@ -56,6 +56,12 @@
"module-services-opencloud-basic-usage": [
"index.html#module-services-opencloud-basic-usage"
],
"module-services-networking-pihole-ftl-configuration-inherit-dnsmasq": [
"index.html#module-services-networking-pihole-ftl-configuration-inherit-dnsmasq"
],
"module-services-networking-pihole-ftl-configuration-multiple-interfaces": [
"index.html#module-services-networking-pihole-ftl-configuration-multiple-interfaces"
],
"module-services-strfry": [
"index.html#module-services-strfry"
],
@ -1448,6 +1454,21 @@
"module-services-input-methods-kime": [
"index.html#module-services-input-methods-kime"
],
"module-services-networking-pihole-ftl": [
"index.html#module-services-networking-pihole-ftl"
],
"module-services-networking-pihole-ftl-administration": [
"index.html#module-services-networking-pihole-ftl-administration"
],
"module-services-networking-pihole-ftl-configuration": [
"index.html#module-services-networking-pihole-ftl-configuration"
],
"module-services-web-apps-pihole-web": [
"index.html#module-services-web-apps-pihole-web"
],
"module-services-web-apps-pihole-web-configuration": [
"index.html#module-services-web-apps-pihole-web-configuration"
],
"ch-profiles": [
"index.html#ch-profiles"
],

View file

@ -13,6 +13,8 @@
- [gtklock](https://github.com/jovanlanik/gtklock), a GTK-based lockscreen for Wayland. Available as [programs.gtklock](#opt-programs.gtklock.enable).
- [Chrysalis](https://github.com/keyboardio/Chrysalis), a graphical configurator for Kaleidoscope-powered keyboards. Available as [programs.chrysalis](#opt-programs.chrysalis.enable).
- [Pi-hole](https://pi-hole.net/), a DNS sinkhole for advertisements based on Dnsmasq. Available as [services.pihole-ftl](#opt-services.pihole-ftl.enable), and [services.pihole-web](#opt-services.pihole-web.enable) for the web GUI and API.
- [FileBrowser](https://filebrowser.org/), a web application for managing and sharing files. Available as [services.filebrowser](#opt-services.filebrowser.enable).
- [LACT](https://github.com/ilya-zlobintsev/LACT), a GPU monitoring and configuration tool, can now be enabled through [services.lact.enable](#opt-services.lact.enable).

View file

@ -1268,6 +1268,7 @@
./services/networking/pdnsd.nix
./services/networking/peroxide.nix
./services/networking/picosnitch.nix
./services/networking/pihole-ftl.nix
./services/networking/pixiecore.nix
./services/networking/pleroma.nix
./services/networking/powerdns.nix
@ -1629,6 +1630,7 @@
./services/web-apps/photoprism.nix
./services/web-apps/phylactery.nix
./services/web-apps/pict-rs.nix
./services/web-apps/pihole-web.nix
./services/web-apps/pingvin-share.nix
./services/web-apps/pixelfed.nix
./services/web-apps/plantuml-server.nix

View file

@ -115,6 +115,12 @@ in
'';
};
configFile = lib.mkOption {
type = lib.types.package;
default = dnsmasqConf;
internal = true;
};
};
};
@ -172,7 +178,7 @@ in
serviceConfig = {
Type = "dbus";
BusName = "uk.org.thekelleys.dnsmasq";
ExecStart = "${dnsmasq}/bin/dnsmasq -k --enable-dbus --user=dnsmasq -C ${dnsmasqConf}";
ExecStart = "${dnsmasq}/bin/dnsmasq -k --enable-dbus --user=dnsmasq -C ${cfg.configFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
PrivateTmp = true;
ProtectSystem = true;

View file

@ -0,0 +1,82 @@
{
cfg,
config,
lib,
pkgs,
}:
let
pihole = pkgs.pihole;
makePayload =
list:
builtins.toJSON {
inherit (list) type enabled;
address = list.url;
comment = list.description;
};
payloads = map makePayload cfg.lists;
in
''
# Can't use -u (unset) because api.sh uses API_URL before it is set
set -eo pipefail
pihole="${lib.getExe pihole}"
jq="${lib.getExe pkgs.jq}"
# If the database doesn't exist, it needs to be created with gravity.sh
if [ ! -f '${cfg.stateDirectory}'/gravity.db ]; then
$pihole -g
# Send SIGRTMIN to FTL, which makes it reload the database, opening the newly created one
${pkgs.procps}/bin/kill -s SIGRTMIN $(systemctl show --property MainPID --value ${config.systemd.services.pihole-ftl.name})
fi
source ${pihole}/usr/share/pihole/advanced/Scripts/api.sh
source ${pihole}/usr/share/pihole/advanced/Scripts/utils.sh
any_failed=0
addList() {
local payload="$1"
echo "Adding list: $payload"
local result=$(PostFTLData "lists" "$payload")
local error="$($jq '.error' <<< "$result")"
if [[ "$error" != "null" ]]; then
echo "Error: $error"
any_failed=1
return
fi
id="$($jq '.lists.[].id?' <<< "$result")"
if [[ "$id" == "null" ]]; then
any_failed=1
error="$($jq '.processed.errors.[].error' <<< "$result")"
echo "Error: $error"
return
fi
echo "Added list ID $id: $result"
}
for i in 1 2 3; do
(TestAPIAvailability) && break
echo "Retrying API shortly..."
${pkgs.coreutils}/bin/sleep .5s
done;
LoginAPI
${builtins.concatStringsSep "\n" (
map (
payload:
lib.pipe payload [
lib.strings.escapeShellArg
(payload: "addList ${payload}")
]
) payloads
)}
# Run gravity.sh to load any new lists
$pihole -g
exit $any_failed
''

View file

@ -0,0 +1,128 @@
# pihole-FTL {#module-services-networking-pihole-ftl}
*Upstream documentation*: <https://docs.pi-hole.net/ftldns/>
pihole-FTL is a fork of [Dnsmasq](index.html#module-services-networking-dnsmasq),
providing some additional features, including an API for analysis and
statistics.
Note that pihole-FTL and Dnsmasq cannot be enabled at
the same time.
## Configuration {#module-services-networking-pihole-ftl-configuration}
pihole-FTL can be configured with [{option}`services.pihole-ftl.settings`](options.html#opt-services.pihole-ftl.settings), which controls the content of `pihole.toml`.
The template pihole.toml is provided in `pihole-ftl.passthru.settingsTemplate`,
which describes all settings.
Example configuration:
```nix
{
services.pihole-ftl = {
enable = true;
openFirewallDHCP = true;
queryLogDeleter.enable = true;
lists = [
{
url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts";
# Alternatively, use the file from nixpkgs. Note its contents won't be
# automatically updated by Pi-hole, as it would with an online URL.
# url = "file://${pkgs.stevenblack-blocklist}/hosts";
description = "Steven Black's unified adlist";
}
];
settings = {
dns = {
domainNeeded = true;
expandHosts = true;
interface = "br-lan";
listeningMode = "BIND";
upstreams = [ "127.0.0.1#5053" ];
};
dhcp = {
active = true;
router = "192.168.10.1";
start = "192.168.10.2";
end = "192.168.10.254";
leaseTime = "1d";
ipv6 = true;
multiDNS = true;
hosts = [
# Static address for the current host
"aa:bb:cc:dd:ee:ff,192.168.10.1,${config.networking.hostName},infinite"
];
rapidCommit = true;
};
misc.dnsmasq_lines = [
# This DHCP server is the only one on the network
"dhcp-authoritative"
# Source: https://data.iana.org/root-anchors/root-anchors.xml
"trust-anchor=.,38696,8,2,683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16"
];
};
};
}
```
### Inheriting configuration from Dnsmasq {#module-services-networking-pihole-ftl-configuration-inherit-dnsmasq}
If [{option}`services.pihole-ftl.useDnsmasqConfig`](options.html#opt-services.pihole-ftl.useDnsmasqConfig) is enabled, the configuration [options of the Dnsmasq
module](index.html#module-services-networking-dnsmasq) will be automatically
used by pihole-FTL. Note that this may cause duplicate option errors
depending on pihole-FTL settings.
See the [Dnsmasq
example](index.html#module-services-networking-dnsmasq-configuration-home) for
an exemplar Dnsmasq configuration. Make sure to set
[{option}`services.dnsmasq.enable`](options.html#opt-services.dnsmasq.enable) to false and
[{option}`services.pihole-ftl.enable`](options.html#opt-services.pihole-ftl.enable) to true instead:
```nix
{
services.pihole-ftl = {
enable = true;
useDnsmasqConfig = true;
};
}
```
### Serving on multiple interfaces {#module-services-networking-pihole-ftl-configuration-multiple-interfaces}
Pi-hole's configuration only supports specifying a single interface. If you want
to configure additional interfaces with different configuration, use
`misc.dnsmasq_lines` to append extra Dnsmasq options.
```nix
{
services.pihole-ftl = {
settings.misc.dnsmasq_lines = [
# Specify the secondary interface
"interface=enp1s0"
# A different device is the router on this network, e.g. the one
# provided by your ISP
"dhcp-option=enp1s0,option:router,192.168.0.1"
# Specify the IPv4 ranges to allocate, with a 1-day lease time
"dhcp-range=enp1s0,192.168.0.10,192.168.0.253,1d"
# Enable IPv6
"dhcp-range=::f,::ff,constructor:enp1s0,ra-names,ra-stateless"
];
};
};
}
```
## Administration {#module-services-networking-pihole-ftl-administration}
*pihole command documentation*: <https://docs.pi-hole.net/main/pihole-command>
Enabling pihole-FTL provides the `pihole` command, which can be used to control
the daemon and some configuration.
Note that in NixOS the script has been patched to remove the reinstallation,
update, and Dnsmasq configuration commands. In NixOS, Pi-hole's configuration is
immutable and must be done with NixOS options.
For more convenient administration and monitoring, see [Pi-hole
Dashboard](#module-services-web-apps-pihole-web)

View file

@ -0,0 +1,483 @@
{
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";
};
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 = "${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.openFirewallDHCP {
allowedUDPPorts = [ 53 ];
allowedTCPPorts = [ 53 ];
})
(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; [ williamvds ];
};
}

View file

@ -0,0 +1,19 @@
# Pi-hole Web Dashboard {#module-services-web-apps-pihole-web}
The Pi-hole suite provides a web GUI for controlling and monitoring
[pihole-FTL](index.html#module-services-networking-pihole-ftl).
## Configuration {#module-services-web-apps-pihole-web-configuration}
Example configuration:
```nix
{
services.pihole-web = {
enable = true;
ports = [ 80 ];
};
}
```
The dashboard can be configured using [{option}`services.pihole-ftl.settings`](options.html#opt-services.pihole-ftl.settings), in particular the `webserver` subsection.

View file

@ -0,0 +1,104 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.pihole-web;
in
{
options.services.pihole-web = {
enable = lib.mkEnableOption "Pi-hole dashboard";
package = lib.mkPackageOption pkgs "pihole-web" { };
hostName = lib.mkOption {
type = lib.types.str;
description = "Domain name for the website.";
default = "pi.hole";
};
ports =
let
portType = lib.types.submodule {
options = {
port = lib.mkOption {
type = lib.types.port;
description = "Port to bind";
};
optional = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Skip the port if it cannot be bound";
};
redirectSSL = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Redirect from this port to the first configured SSL port";
};
ssl = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Serve SSL on the port";
};
};
};
in
lib.mkOption {
type = lib.types.listOf (
lib.types.oneOf [
lib.types.port
lib.types.str
portType
]
);
description = ''
Port(s) for the webserver to serve on.
If provided as a string, optionally append suffixes to control behaviour:
- `o`: to make the port is optional - failure to bind will not be an error.
- `s`: for the port to be used for SSL.
- `r`: for a non-SSL port to redirect to the first available SSL port.
'';
example = [
"80r"
"443s"
];
apply =
values:
let
convert =
value:
if (builtins.typeOf) value == "int" then
toString value
else if builtins.typeOf value == "set" then
lib.strings.concatStrings [
(toString value.port)
(lib.optionalString value.optional "o")
(lib.optionalString value.redirectSSL "r")
(lib.optionalString value.ssl "s")
]
else
value;
in
lib.strings.concatStringsSep "," (map convert values);
};
};
config = lib.mkIf cfg.enable {
services.pihole-ftl.settings.webserver = {
domain = cfg.hostName;
port = cfg.ports;
paths.webroot = "${cfg.package}/share/";
paths.webhome = "/";
};
};
meta = {
doc = ./pihole-web.md;
maintainers = with lib.maintainers; [ williamvds ];
};
}

View file

@ -0,0 +1,85 @@
{
lib,
stdenv,
fetchFromGitHub,
cmake,
gmp,
libidn2,
libunistring,
mbedtls,
ncurses,
nettle,
readline,
xxd,
iproute2,
...
}:
stdenv.mkDerivation (finalAttrs: {
pname = "pihole-ftl";
version = "6.1";
src = fetchFromGitHub {
owner = "pi-hole";
repo = "FTL";
tag = "v${finalAttrs.version}";
hash = "sha256-b3/kyDQa6qDK2avvDObWLvwUpAn6TFr1ZBdQC9AZWa4=";
};
nativeBuildInputs = [
cmake
xxd
];
buildInputs = [
gmp
libidn2
libunistring
mbedtls
ncurses
nettle
readline
];
cmakeFlags = [
(lib.cmakeBool "STATIC" stdenv.hostPlatform.isStatic)
];
postPatch = ''
substituteInPlace src/version.c.in \
--replace-quiet "@GIT_VERSION@" "v${finalAttrs.version}" \
--replace-quiet "@GIT_DATE@" "1970-01-01" \
--replace-quiet "@GIT_BRANCH@" "master" \
--replace-quiet "@GIT_TAG@" "v${finalAttrs.version}" \
--replace-quiet "@GIT_HASH@" "builtfromreleasetarball"
# Remove hard-coded absolute path to the pihole script, rely on it being provided by $PATH
# Use execvp instead of execv so PATH is followed
substituteInPlace src/api/action.c \
--replace-fail "/usr/local/bin/pihole" "pihole" \
--replace-fail "execv" "execvp"
substituteInPlace src/database/network-table.c \
--replace-fail "ip neigh show" "${iproute2}/bin/ip neigh show" \
--replace-fail "ip address show" "${iproute2}/bin/ip address show"
'';
installPhase = ''
runHook preInstall
install -Dm 555 -t $out/bin pihole-FTL
runHook postInstall
'';
passthru.settingsTemplate = ./pihole.toml;
meta = {
description = "Pi-hole FTL engine";
homepage = "https://github.com/pi-hole/FTL";
license = lib.licenses.eupl12;
maintainers = with lib.maintainers; [ williamvds ];
platforms = lib.platforms.linux;
mainProgram = "pihole-FTL";
};
})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
{
stdenv,
lib,
fetchFromGitHub,
pihole,
pihole-ftl,
procps,
...
}:
stdenv.mkDerivation (finalAttrs: {
pname = "pihole-web";
version = "6.1";
src = fetchFromGitHub {
owner = "pi-hole";
repo = "web";
tag = "v${finalAttrs.version}";
hash = "sha256-+h4cPDsTAKR8MM+Za0mp2nOX1cHW8LRlFmLqvrBHfbs=";
};
propagatedBuildInputs = [
pihole
pihole-ftl
procps
];
installPhase = ''
runHook preInstall
mkdir -p $out/share
cp -r -t $out/share *.lp img/ scripts/ style/ vendor/
mkdir -p $out/share/doc/$name/
cp README.md $out/share/doc/$name/
runHook postInstall
'';
meta = {
description = "Pi-hole web dashboard displaying stats and more";
longDescription = ''
Pi-hole's Web interface (based off of AdminLTE) provides a central
location to manage your Pi-hole and review the statistics generated by
FTLDNS.
'';
license = lib.licenses.eupl12;
maintainers = with lib.maintainers; [ williamvds ];
platforms = lib.platforms.linux;
};
})

View file

@ -0,0 +1,32 @@
From a2b3aa45d6e073272608506b1d27e4f43f2b0032 Mon Sep 17 00:00:00 2001
From: williamvds <william@williamvds.me>
Date: Sun, 6 Apr 2025 23:00:41 +0100
Subject: [PATCH 1/3] Remove sudo
Rely on polkit and sensible permissions
---
pihole | 8 --------
1 file changed, 8 deletions(-)
diff --git a/pihole b/pihole
index 1d5093c..6afc48a 100755
--- a/pihole
+++ b/pihole
@@ -570,14 +570,6 @@ if [[ -z ${USER} ]]; then
USER=$(whoami)
fi
-# Check if the current user is neither root nor pihole and if the command
-# requires root. If so, exit with an error message.
-if [[ $EUID -ne 0 && ${USER} != "pihole" && need_root -eq 1 ]];then
- echo -e " ${CROSS} The Pi-hole command requires root privileges, try:"
- echo -e " ${COL_GREEN}sudo pihole $*${COL_NC}"
- exit 1
-fi
-
# Handle redirecting to specific functions based on arguments
case "${1}" in
"allow" | "allowlist" ) listFunc "$@";;
--
2.48.1

View file

@ -0,0 +1,67 @@
From ab0650484cdd89afb5b60a0a046509ec5ae14375 Mon Sep 17 00:00:00 2001
From: williamvds <william@williamvds.me>
Date: Sun, 6 Apr 2025 23:01:30 +0100
Subject: [PATCH 2/3] Remove unsupported commands
Remove some unsupported maintenance commands, particularly the ones which
reinstall, update, and uninstall pihole. This is managed by NixOS, after all.
---
pihole | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/pihole b/pihole
index 6afc48a..cce7c97 100755
--- a/pihole
+++ b/pihole
@@ -92,6 +92,7 @@ debugFunc() {
}
flushFunc() {
+ unsupportedFunc
"${PI_HOLE_SCRIPT_DIR}"/piholeLogFlush.sh "$@"
exit 0
}
@@ -102,6 +103,7 @@ arpFunc() {
}
updatePiholeFunc() {
+ unsupportedFunc
if [ -n "${DOCKER_VERSION}" ]; then
unsupportedFunc
else
@@ -137,6 +139,7 @@ chronometerFunc() {
uninstallFunc() {
+ unsupportedFunc
if [ -n "${DOCKER_VERSION}" ]; then
unsupportedFunc
else
@@ -405,6 +408,7 @@ tailFunc() {
}
piholeCheckoutFunc() {
+ unsupportedFunc
if [ -n "${DOCKER_VERSION}" ]; then
echo -e "${CROSS} Function not supported in Docker images"
echo "Please build a custom image following the steps at"
@@ -460,13 +464,14 @@ tricorderFunc() {
}
updateCheckFunc() {
+ unsupportedFunc
"${PI_HOLE_SCRIPT_DIR}"/updatecheck.sh "$@"
exit 0
}
unsupportedFunc(){
- echo "Function not supported in Docker images"
- exit 0
+ echo "Function not supported in NixOS"
+ exit 1
}
helpFunc() {
--
2.48.1

View file

@ -0,0 +1,52 @@
From cca2f6437e3ba09019b8fcb1986b4558d7c6db4e Mon Sep 17 00:00:00 2001
From: williamvds <william@williamvds.me>
Date: Sat, 31 May 2025 13:43:42 +0100
Subject: [PATCH 3/3] Fix redefinition of readonly variable utilsfile
---
advanced/Scripts/api.sh | 2 +-
pihole | 10 +++-------
2 files changed, 4 insertions(+), 8 deletions(-)
diff --git a/advanced/Scripts/api.sh b/advanced/Scripts/api.sh
index 613a8d8..8720043 100755
--- a/advanced/Scripts/api.sh
+++ b/advanced/Scripts/api.sh
@@ -19,7 +19,7 @@
TestAPIAvailability() {
- local chaos_api_list authResponse authStatus authData apiAvailable DNSport
+ local chaos_api_list authResponse authStatus authData apiAvailable DNSport utilsfile
# as we are running locally, we can get the port value from FTL directly
readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
diff --git a/pihole b/pihole
index cce7c97..d63d064 100755
--- a/pihole
+++ b/pihole
@@ -16,18 +16,14 @@ readonly PI_HOLE_SCRIPT_DIR="/opt/pihole"
# error due to modifying a readonly variable.
PI_HOLE_BIN_DIR="/usr/local/bin"
-readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE"
# shellcheck source=./advanced/Scripts/COL_TABLE
-source "${colfile}"
+source "${PI_HOLE_SCRIPT_DIR}/COL_TABLE"
-readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
# shellcheck source=./advanced/Scripts/utils.sh
-source "${utilsfile}"
+source "${PI_HOLE_SCRIPT_DIR}/utils.sh"
-# Source api functions
-readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh"
# shellcheck source=./advanced/Scripts/api.sh
-source "${apifile}"
+source "${PI_HOLE_SCRIPT_DIR}/api.sh"
versionsfile="/etc/pihole/versions"
if [ -f "${versionsfile}" ]; then
--
2.48.1

View file

@ -0,0 +1,257 @@
{
lib,
fetchFromGitHub,
makeBinaryWrapper,
installShellFiles,
bash,
coreutils,
curl,
dig,
gawk,
getent,
glibc,
gnugrep,
gnused,
iproute2,
jq,
killall,
libidn2,
locale,
ncurses,
netcat,
nettools,
pihole-ftl,
procps,
resholve,
sqlite,
systemd,
util-linux,
stateDir ? "/etc/pihole",
...
}:
(resholve.mkDerivation rec {
pname = "pihole";
version = "6.1";
src = fetchFromGitHub {
owner = "pi-hole";
repo = "pi-hole";
tag = "v${version}";
hash = "sha256-aEnv8Lhb5vf0yDyuriVTaUY1wcdVmTdqoK+KDHvT/Lw=";
};
patches = [
# Remove use of sudo in the original script, prefer to use a wrapper
./0001-Remove-sudo.patch
# Disable unsupported subcommands, particularly those for imperatively installing/upgrading Pi-hole
./0002-Remove-unsupported-commands.patch
# Fix a readonly variable error caused by defining a shadowing local variable
./0003-Fix-redefinition-of-readonly-variable-utilsfile.patch
];
nativeBuildInputs = [
makeBinaryWrapper
installShellFiles
];
installPhase = ''
runHook preInstall
readonly scriptsDir=$out/usr/share/pihole
install -Dm 555 -t $out/bin pihole
install -Dm 555 -t $scriptsDir/advanced/Scripts gravity.sh
# The installation script is sourced by advanced/Scripts/piholeARPTable.sh etc
cp --parents -r -t $scriptsDir/ 'automated install/' advanced/{Scripts,Templates}/
installShellCompletion --bash --name pihole.bash \
advanced/bash-completion/pihole
runHook postInstall
'';
solutions.default =
let
out = builtins.placeholder "out";
scriptsDir = "${out}/usr/share/pihole/advanced/Scripts";
in
{
scripts =
let
relativeScripts = "usr/share/pihole/advanced/Scripts";
in
[
"bin/pihole"
"${relativeScripts}/api.sh"
"${relativeScripts}/database_migration/gravity-db.sh"
"${relativeScripts}/gravity.sh"
"${relativeScripts}/list.sh"
"${relativeScripts}/piholeARPTable.sh"
"${relativeScripts}/piholeCheckout.sh"
"${relativeScripts}/piholeDebug.sh"
"${relativeScripts}/piholeLogFlush.sh"
"${relativeScripts}/query.sh"
"${relativeScripts}/update.sh"
"${relativeScripts}/updatecheck.sh"
"${relativeScripts}/utils.sh"
"${relativeScripts}/version.sh"
];
interpreter = lib.getExe bash;
inputs = [
# TODO: see if these inputs can help resholving
"bin"
"usr/share/pihole/advanced/Scripts"
bash
coreutils
curl
dig
gawk
getent
gnugrep
gnused
iproute2
jq
killall
libidn2
locale
ncurses
netcat
nettools
pihole-ftl
procps
sqlite
systemd
util-linux
];
fake = {
source = [
"/etc/os-release"
"/etc/pihole/versions"
"/etc/pihole/setupVars.conf"
];
external = [
# Used by chronometer.sh to get GPU information on Raspberry Pis
"sudo"
"vcgencmd"
# used by the checkout and update scripts, which are patched out
"git"
"getenforce"
"firewall-cmd"
# Conditionally used in Docker builds
"service"
"lighttpd"
# Used in piholeLogFlush.sh
"/usr/sbin/logrotate"
# Used by teleporter in webpage.sh
"php"
];
};
fix = {
"$PI_HOLE_BIN_DIR" = [ "${out}/bin" ];
"$PI_HOLE_FILES_DIR" = [ "${out}/usr/share/pihole" ];
"$PI_HOLE_INSTALL_DIR" = [ scriptsDir ];
"$PI_HOLE_LOCAL_REPO" = [ "${out}/usr/share/pihole" ];
"$PI_HOLE_SCRIPT_DIR" = [ scriptsDir ];
"$colfile" = [ "${scriptsDir}/COL_TABLE" ];
"$coltable" = [ "${scriptsDir}/COL_TABLE" ];
"$PIHOLE_COLTABLE_FILE" = [ "${scriptsDir}/COL_TABLE" ];
"$utilsfile" = [ "${scriptsDir}/utils.sh" ];
"$apifile" = [ "${scriptsDir}/api.sh" ];
"$piholeGitDir" = [ "${out}/usr/share/pihole" ];
"$PIHOLE_COMMAND" = [ "pihole" ];
};
keep = {
source = [
"$pihole_FTL" # Global config file
"$setupVars" # Global config file
"$PIHOLE_SETUP_VARS_FILE"
"$versionsfile" # configuration file, doesn't exist on NixOS
"${out}/usr/share/pihole/automated install/basic-install.sh"
"${scriptsDir}/COL_TABLE"
"${scriptsDir}/database_migration/gravity-db.sh"
"${scriptsDir}/gravity.sh"
"${scriptsDir}/piholeCheckout.sh"
"${scriptsDir}/utils.sh"
"${scriptsDir}/api.sh"
"/etc/os-release"
"/etc/pihole/versions"
"/etc/pihole/setupVars.conf"
"$cachedVersions"
];
"$PIHOLE_SETUP_VARS_FILE" = true;
"$PKG_INSTALL" = true; # System package manager, patched out
"$PKG_MANAGER" = true; # System package manager, patched out
"$cmd" = true; # ping or ping6
"$program_name" = true; # alias for $1
"$svc" = true; # dynamic restart command
"${out}/bin/pihole" = true;
"${scriptsDir}/api.sh" = true;
"${scriptsDir}/gravity.sh" = true;
"${scriptsDir}/list.sh" = true;
"${scriptsDir}/piholeARPTable.sh" = true;
"${scriptsDir}/piholeDebug.sh" = true;
"${scriptsDir}/piholeLogFlush.sh" = true;
"${scriptsDir}/query.sh" = true;
"${scriptsDir}/uninstall.sh" = true;
"${scriptsDir}/update.sh" = true;
"${scriptsDir}/updatecheck.sh" = true;
"${scriptsDir}/version.sh" = true;
# boolean variables
"$addmode" = true;
"$noReloadRequested" = true;
"$oldAvail" = true;
"$verbose" = true;
"$web" = true;
"$wildcard" = true;
# Note that this path needs to be quoted due to the whitespace.
# TODO: raise upstream resholve issue. pihole scripts specify this path
# both quoted and escaped. Resholve apparently requires matching the
# literal path, so we need to provide a version with and without the
# backslash.
"'${out}/usr/share/pihole/automated\\ install/basic-install.sh'" = true;
"'${out}/usr/share/pihole/automated install/basic-install.sh'" = true;
"/etc/.pihole" = true; # Patched with an override
"/etc/os-release" = true;
"/etc/pihole/versions" = true;
"/etc/pihole/setupVars.conf" = true;
};
execer = [
"cannot:${pihole-ftl}/bin/pihole-FTL"
"cannot:${iproute2}/bin/ip"
"cannot:${systemd}/bin/systemctl"
"cannot:${glibc.bin}/bin/ldd"
"cannot:${out}/bin/pihole"
];
};
meta = {
description = "A black hole for Internet advertisements";
license = lib.licenses.eupl12;
maintainers = with lib.maintainers; [ williamvds ];
platforms = lib.platforms.linux;
mainProgram = "pihole";
};
passthru = {
stateDir = stateDir;
};
}).overrideAttrs
(old: {
# Resholve can't fix the hardcoded absolute paths, so substitute them before resholving
preFixup =
''
scriptsDir=$out/usr/share/pihole
substituteInPlace $out/bin/pihole $scriptsDir/advanced/Scripts/*.sh \
--replace-quiet /etc/.pihole $scriptsDir \
--replace-quiet /opt/pihole $scriptsDir/advanced/Scripts
''
+ old.preFixup;
})

10
sync.sh Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env bash
local_root=~/.config/nix/
nixpkgs_root="$(dirname "$0")"
cp "$local_root"/modules/pihole-ftl* "$nixpkgs_root"/nixos/modules/services/networking/
cp "$local_root"/modules/pihole-web* "$nixpkgs_root"/nixos/modules/services/web-apps/
cp -r "$local_root"/pkgs/pihole* "$nixpkgs_root"/pkgs/by-name/pi/
cp -r "$local_root"/modules/pihole-web* "$nixpkgs_root"/nixos/modules/services/web-apps/