feat: initial

This commit is contained in:
Katharina 2026-03-09 22:06:13 +01:00
commit bba9ceff39
18 changed files with 750 additions and 0 deletions

13
services/README.md Normal file
View file

@ -0,0 +1,13 @@
# Services
## List
- DHCP
- Kea
- DNS
- Blocky
- Reverse Proxy
- nginx
- Torrent
- qbittorrent
- Wiki
- kiwix

61
services/blocky.nix Normal file
View file

@ -0,0 +1,61 @@
{ config, pkgs, ... }:
let
net = import ../data/network.nix;
in
{
# Enable Blocky
services.blocky = {
enable = true;
settings = {
# Listen on port 53 (standard DNS port)
ports.dns = 53;
# Custom DNS entries for your local services
customDNS = {
# This maps your domains to your Pi's IP
mapping = net.dnsMappings;
# mapping = dnsMappings;
};
conditional = {
fallbackUpstream = false;
mapping = builtins.mapAttrs (name: value: net.ips.router) net.dnsMappings;
};
# Upstream DNS servers (with fallback)
upstreams = {
groups = {
default =
["https://cloudflare-dns.com/dns-query"] ++ net.fallback_dns_servers;
};
};
# Bootstrap DNS (for initially resolving DoH servers)
bootstrapDns = {
upstream = "https://1.1.1.1/dns-query";
ips = ["1.1.1.1" "1.0.0.1"];
};
# Enable caching for better performance
caching = {
minTime = "5m";
maxTime = "30m";
prefetching = true;
};
# blocking = {
# denylists = {
# ads = ["https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"];
# };
# clientGroupsBlock = {
# default = ["ads"];
# };
# };
};
};
# Allow DNS through the firewall
networking.firewall.allowedTCPPorts = [ 53 ];
networking.firewall.allowedUDPPorts = [ 53 ];
}

56
services/kea.nix Normal file
View file

@ -0,0 +1,56 @@
{ config, pkgs, ... }:
let
net = import ../data/network.nix;
in
{
services.kea.dhcp4 = {
enable = true;
settings = {
interfaces-config = {
interfaces = [ "eth0" ];
};
lease-database = {
name = "/var/lib/kea/dhcp4.leases";
type = "memfile";
};
subnet4 = [{
id = 1;
subnet = net.network.subnet;
pools = [{
pool = "${net.dhcp.pool_start} - ${net.dhcp.pool_end}";
}];
option-data = [
{
name = "routers";
data = net.ips.router;
}
{
name = "domain-name-servers";
data = builtins.concatStringsSep ", " ([net.ips.pi] ++ net.fallback_dns_servers);
}
{
name = "domain-name";
data = net.local_domain;
}
{
name = "domain-search";
data = net.local_domain;
}
];
reservations = net.dhcp.reservations;
}];
valid-lifetime = net.dhcp.default_lease;
renew-timer = net.dhcp.default_lease / 2;
rebind-timer = net.dhcp.default_lease * 3 / 4;
};
};
# Firewall rules for DHCP
networking.firewall = {
allowedUDPPorts = [ 67 68 ]; # DHCP ports
checkReversePath = false; # Sometimes needed for DHCP
};
}

View file

@ -0,0 +1,87 @@
{ config, pkgs, lib, ... }:
let
# Import service data (make sure this path is correct)
service_data = import ../data/services.nix;
kiwix = service_data.kiwix;
zimUrls = kiwix.urls;
updater = pkgs.writeShellScriptBin "kiwix-updater" ''
set -e
URLS=(
${builtins.concatStringsSep " " zimUrls}
)
download() {
local url=''$1
local filename=''$(basename "''$url")
local filepath="${kiwix.root_dir}"/''$filename
if [ -f "''$filepath" ]; then
echo "''$filepath exists!"
return 0
fi
cd ${kiwix.root_dir}
${pkgs.wget}/bin/wget --continue --quiet "''$url" -O "''$filename.tmp"
mv ''$filename.tmp ''$filename
}
build_lib() {
{
echo '<?xml version="1.0" encoding="UTF-8"?>'
echo '<library>'
for zim in "${kiwix.root_dir}"/*.zim; do
if [ -f "''$zim" ]; then
filename=''$(basename "''$zim")
size=''$(stat -c%s "''$zim")
case "''$filename" in
*_en_*)
title="English Wikipedia (Text Only)"
lang="eng"
;;
*_de_*)
title="German Wikipedia (Text Only)"
lang="deu"
;;
*)
title="''$filename"
lang="unknown"
;;
esac
cat << EOF
<book id="''$filename" path="/data/''$filename" title="''$title" language="''$lang">
<title>''$title</title>
<language>''$lang</language>
<path>/data/''$filename</path>
<size>''$size</size>
</book>
EOF
fi
done
echo '</library>'
} > "${kiwix.root_dir}/library.xml.tmp"
mv "${kiwix.root_dir}/library.xml.tmp" "${kiwix.root_dir}/library.xml"
}
for url in "''${URLS[@]}"; do
download "''$url"
done
build_lib
${pkgs.systemd}/bin/systemctl restart podman-kiwix-serve.service
'';
in {
environment.systemPackages = with pkgs; [
wget
curl
];
services.cron = {
enable = true;
systemCronJobs = [
"0 3 * * * root ${updater}/bin/kiwix-updater >/dev/null 2>&1"
];
};
}

36
services/kiwix.nix Normal file
View file

@ -0,0 +1,36 @@
{ config, pkgs, lib, ... }:
let
net = import ../data/network.nix;
service_data = import ../data/services.nix;
kiwix = service_data.kiwix;
in {
systemd.tmpfiles.rules = [
"d ${kiwix.root_dir} 0755 root root - -"
"d ${kiwix.root_dir}/data 0755 root root - -"
];
virtualisation.oci-containers.containers = {
kiwix-serve = {
image = "ghcr.io/kiwix/kiwix-serve:3.8.2";
ports = ["8086:8080"];
volumes = ["${kiwix.root_dir}/:/data:ro"];
cmd = [
"--monitorLibrary"
"--library" "/data/library.xml"
];
environment = {
TZ = "Europe/Berlin";
};
extraOptions = [
"--memory=512m" # Limit container to 512MB RAM
"--memory-swap=512m" # Disable swap usage
"--cpus=1" # Limit to 1 CPU core
];
autoStart = true;
};
};
networking.firewall = {
allowedTCPPorts = [8086];
};
}

47
services/nginx.nix Normal file
View file

@ -0,0 +1,47 @@
{ config, pkgs, lib, ... }:
let
network = import ../data/network.nix;
rproxyServices = builtins.mapAttrs (name: service: {
serverName = "${name}.${network.local_domain}";
listen = [ {addr = "0.0.0.0"; port = 80;} ];
locations = {
"/" = {
proxyPass = "http://127.0.0.1:${builtins.toString service.reverse_proxy.port}/";
proxyWebsockets = true;
};
};
extraConfig = ''
allow ${network.network.subnet};
deny all;
'';
}) network.reverse_proxy;
serviceNamesMessage = builtins.toString (builtins.attrNames network.reverse_proxy);
fallback = {
serverName = "_";
listen = [ {addr = "0.0.0.0"; port = 80;} ];
locations."/" = {
return = "404";
extraConfig = ''
add_header Content-Type text/plain;
'';
};
extraConfig = ''
return 404 "This domain is not configured. Available services: ${serviceNamesMessage}";
'';
};
in {
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
virtualHosts = rproxyServices // {fallback = fallback;};
};
# TODO add 443 for https
networking.firewall.allowedTCPPorts = [80];
}

17
services/openssh.nix Normal file
View file

@ -0,0 +1,17 @@
#{ config, pkgs, lib, ... }:
let
ssh_data = import ../data/ssh.nix;
in {
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = true;
PermitRootLogin = "no";
AllowUsers = ssh_data.ssh_users;
};
};
users.users = builtins.mapAttrs (username: value: {
openssh.authorizedKeys.keys = ssh_data.keys.${username};
}) ssh_data.keys;
}

92
services/qbittorrent.nix Normal file
View file

@ -0,0 +1,92 @@
{ config, pkgs, lib, ... }:
let
net = import ../data/network.nix;
serviceData = import ../data/services.nix;
qbt = serviceData.qbittorrent;
in {
systemd.tmpfiles.rules = [
"d ${qbt.root_dir} 0755 root root - -"
"d ${qbt.root_dir}/gluetun 0755 root root - -"
"d ${qbt.root_dir}/downloads 0755 root root - -"
"d ${qbt.root_dir}/config 0755 root root - -"
];
environment.etc."qbittorrent-compose/docker-compose.yml" = {
mode = "0444";
text = ''
services:
gluetun:
image: docker.io/qmcgaw/gluetun:latest
pull_policy: always
cap_add:
- NET_ADMIN
network_mode: bridge
ports:
- 127.0.0.1:8085:8085 # qBittorrent
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ${qbt.root_dir}/gluetun/:/gluetun
environment:
- VPN_SERVICE_PROVIDER=protonvpn
- SERVER_HOSTNAME=node-nl-28.protonvpn.net,node-ch-06.protonvpn.net,node-nl-13.protonvpn.net,node-ch-06.protonvpn.net,node-es-04.protonvpn.net
- UPDATER_PERIOD=24h
- OPENVPN_USER=${qbt.vpn.username}
- OPENVPN_PASSWORD=${qbt.vpn.password}
- DOT_PROVIDERS=cloudflare,google
- BLOCK_ADS=off
- BLOCK_MALICIOUS=off
- BLOCK_SURVEILLANCE=off
- TZ=Europe/Berlin
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
pull_policy: always
network_mode: 'service:gluetun'
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
- WEBUI_PORT=8085
volumes:
- ${qbt.root_dir}/config/:/config
- ${qbt.root_dir}/downloads/:/downloads
'';
};
systemd.services.qbittorrent-stack = {
description = "qbittorrent stack";
after = ["docker.service" "network.target"];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
WorkingDirectory = qbt.root_dir;
ExecStart = "${pkgs.writeShellScript "torrent-start" ''
set -e
# Copy compose file to working directory
cp /etc/qbittorrent-compose/docker-compose.yml ${qbt.root_dir}/
cd ${qbt.root_dir}
${pkgs.docker-compose}/bin/docker-compose up -d
''}";
ExecStop = "${pkgs.writeShellScript "torrent-stop" ''
cd ${qbt.root_dir}
${pkgs.docker-compose}/bin/docker-compose down
''}";
ExecReload = "${pkgs.writeShellScript "torrent-reload" ''
cd ${qbt.root_dir}
${pkgs.docker-compose}/bin/docker-compose restart
''}";
Restart = "on-failure";
RestartSec = 10;
};
};
networking.firewall = {
allowedTCPPorts = [8085];
};
}

34
services/unbound.nix Normal file
View file

@ -0,0 +1,34 @@
{ config, pkgs, ... }:
let
net = import ../data/network.nix;
in
{
services.unbound = {
enable = true;
settings = {
server = {
interface = ["0.0.0.0" "::0"];
access-control = ["127.0.0.1 allow" "${net.network.subnet} allow"];
local-zone = "\"${net.local_domain}.\" static";
local-data =
(map (name:
let ip = net.dnsMappings.${name}; in
"\"${name}. IN A ${ip}\""
) (builtins.attrNames net.dnsMappings));
};
forward-zone = {
name = ".";
forward-addr = net.fallback_dns_servers;
};
};
};
# Allow DNS through the firewall
networking.firewall.allowedTCPPorts = [ 53 ];
networking.firewall.allowedUDPPorts = [ 53 ];
}