Both run on pinix, published only through the pinix Tailnet namespace and Traefik's web-internal entrypoint (unbound local-data + Headscale split DNS for intent./ereader.domain2). Live Intent reads its OpenAI key from the new pinix-only agenix secret. ereaderViewer is a path: flake input for now; the repo split plan moves it into its own repository on cgit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
210 lines
11 KiB
Nix
210 lines
11 KiB
Nix
{ config, pkgs, ... }:
|
|
let
|
|
ns = "pinix-dns";
|
|
hostIP = "fd42:7069:6e69:78::1";
|
|
peerIP = "fd42:7069:6e69:78::2";
|
|
dns4 = "100.96.0.1";
|
|
dns6 = "fd7a:115c:a1e0:96::1";
|
|
# Traefiks web-internal-Entrypoint (traefik.nix) auf der Host-Seite des
|
|
# veth-Paars. Eigener Port, weil websecure auf :443 aller Adressen hoert.
|
|
internalPort = 8443;
|
|
# UDP-Port von tailscaled-pinix (--port unten), per DNAT von aussen und aus
|
|
# dem LAN erreichbar, damit Peers direkt statt ueber DERP verbinden.
|
|
wgPort = 41642;
|
|
privateZone = "int.${config.domain2}";
|
|
resolv = pkgs.writeText "pinix-dns-resolv.conf" ''
|
|
nameserver 2a01:598:7ff:0:10:74:210:221
|
|
'';
|
|
hosts = pkgs.writeText "pinix-dns-hosts" ''
|
|
127.0.0.1 localhost
|
|
::1 localhost
|
|
${hostIP} head.${config.domain2} dex.${config.domain2}
|
|
'';
|
|
unboundConfig = pkgs.writeText "pinix-tailnet-unbound.conf" ''
|
|
server:
|
|
interface: ${dns4}
|
|
interface: ${dns6}
|
|
ip-freebind: yes
|
|
access-control: 100.96.0.0/24 allow
|
|
access-control: fd7a:115c:a1e0:96::/64 allow
|
|
access-control: 0.0.0.0/0 refuse
|
|
access-control: ::/0 refuse
|
|
username: ""
|
|
chroot: ""
|
|
directory: "/run/unbound-pinix"
|
|
pidfile: ""
|
|
use-syslog: no
|
|
do-daemonize: no
|
|
hide-identity: yes
|
|
hide-version: yes
|
|
# Upstream is already DNS64. Local DNSSEC validation of synthesized
|
|
# answers would fail, so validation stays with the upstream resolver.
|
|
module-config: "iterator"
|
|
local-zone: "${privateZone}." static
|
|
local-data: "dns.${privateZone}. A ${dns4}"
|
|
local-data: "dns.${privateZone}. AAAA ${dns6}"
|
|
# Tailnet-only-Dienste auf pinix, das Gegenstueck zu den
|
|
# AdGuard-Rewrites auf blunix. Nur AAAA: die Weiterleitung in den Host
|
|
# laeuft ueber das IPv6-only veth-Paar (gitserver/default.nix).
|
|
local-data: "git.${config.domain2}. AAAA ${dns6}"
|
|
# The LLDAP board is also exposed through Traefik's web-internal
|
|
# entrypoint; keep the Tailnet name on the technical node so the
|
|
# namespace forwarding path is used consistently.
|
|
local-data: "lldap.${config.domain2}. AAAA ${dns6}"
|
|
# The annotation viewer is private as well; do not publish its address
|
|
# in the public DNS zone.
|
|
local-data: "ereader.${config.domain2}. AAAA ${dns6}"
|
|
# Live Intent is private as well: it is published only through the
|
|
# pinix Tailnet namespace and Traefik's web-internal entrypoint.
|
|
local-data: "intent.${config.domain2}. AAAA ${dns6}"
|
|
forward-zone:
|
|
name: "."
|
|
forward-addr: 2a01:598:7ff:0:10:74:210:221
|
|
'';
|
|
in
|
|
{
|
|
# Eigener Adressbereich des pinix-Tailnets, getrennt von blunix'
|
|
# 100.64.0.x (docs/intent/gitserver-pinix.md). Bestehende Registrierungen
|
|
# behalten ihre alten Adressen, bis `headscale nodes backfillips` laeuft --
|
|
# vorher pinix-dns einschreiben, damit es .1 bekommt
|
|
# (scripts/enrol-pinix-dns.sh).
|
|
# The old tailscaled, addresses, routes and state remain untouched.
|
|
services.headscale.settings = {
|
|
prefixes = {
|
|
v4 = "100.96.0.0/24";
|
|
v6 = "fd7a:115c:a1e0:96::/64";
|
|
};
|
|
# Nur die Namen, die dieser Resolver wirklich selbst beantwortet. Ein
|
|
# Split fuer ganz domain2 wuerde bei einem Ausfall von unbound-pinix auch
|
|
# head./dex. fuer die Clients unaufloesbar machen.
|
|
dns.nameservers.split."git.${config.domain2}" = [ dns6 ];
|
|
dns.nameservers.split."lldap.${config.domain2}" = [ dns6 ];
|
|
dns.nameservers.split."ereader.${config.domain2}" = [ dns6 ];
|
|
dns.nameservers.split."intent.${config.domain2}" = [ dns6 ];
|
|
};
|
|
|
|
# Only the namespace underlay is forwarded, via the IPv6-only LAN uplink.
|
|
# RA reception on wan0 (accept_ra = 2) is owned by ../router-uplink.nix;
|
|
# never configure a static wan0
|
|
# address, which would disable dhcpcd router solicitation on this host.
|
|
boot.kernel.sysctl = {
|
|
"net.ipv6.conf.all.forwarding" = 1;
|
|
};
|
|
networking.firewall = {
|
|
# 443: Headscale/Dex-Bootstrap des Knotens. internalPort: Tailnet-only-
|
|
# Dienste. 22: git ueber SSH -- sshd ist auf pinix nur pro Interface
|
|
# freigegeben (end0, wlan0, tailscale0), nicht global.
|
|
interfaces.pdns-host.allowedTCPPorts = [ 22 443 internalPort ];
|
|
extraCommands = ''
|
|
ip6tables -N pinix-dns-forward 2>/dev/null || true
|
|
ip6tables -F pinix-dns-forward
|
|
ip6tables -A pinix-dns-forward -i pdns-host -o wan0 -s ${peerIP}/128 -j ACCEPT
|
|
ip6tables -A pinix-dns-forward -o pdns-host -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
ip6tables -A pinix-dns-forward -o pdns-host -d ${peerIP}/128 -p udp --dport ${toString wgPort} -m conntrack --ctstate DNAT -j ACCEPT
|
|
# Antworten auf genau diese weitergeleiteten Verbindungen, auch Richtung
|
|
# LAN (end0) -- die erste Regel oben erlaubt aus dem Namespace nur wan0.
|
|
ip6tables -A pinix-dns-forward -i pdns-host -m conntrack --ctstate DNAT --ctdir REPLY -j ACCEPT
|
|
ip6tables -A pinix-dns-forward -j REJECT
|
|
ip6tables -C FORWARD -i pdns-host -j pinix-dns-forward 2>/dev/null || ip6tables -I FORWARD 1 -i pdns-host -j pinix-dns-forward
|
|
ip6tables -C FORWARD -o pdns-host -j pinix-dns-forward 2>/dev/null || ip6tables -I FORWARD 1 -o pdns-host -j pinix-dns-forward
|
|
ip6tables -t nat -C POSTROUTING -s ${peerIP}/128 -o wan0 -j MASQUERADE 2>/dev/null || ip6tables -t nat -A POSTROUTING -s ${peerIP}/128 -o wan0 -j MASQUERADE
|
|
|
|
# WireGuard-Port des Namespace-Knotens wie eine Router-Portweiterleitung
|
|
# auf jede eigene Adresse von pinix. Ohne das bleibt jede Verbindung auf
|
|
# DERP: der Knoten erscheint nach aussen nur als pinix' oeffentliche
|
|
# IPv6 hinter Masquerade, und ein Paket aus dem LAN an genau diese
|
|
# Adresse fand nicht zurueck in den Namespace (gemessen 14.09.2026:
|
|
# tnix <-> pinix-dns "via DERP(fra)", ~110 ms). -i pdns-host
|
|
# ausgenommen, damit eigene Antworten des Knotens nicht umgebogen werden.
|
|
ip6tables -t nat -C PREROUTING ! -i pdns-host -m addrtype --dst-type LOCAL -p udp --dport ${toString wgPort} -j DNAT --to-destination [${peerIP}]:${toString wgPort} 2>/dev/null || ip6tables -t nat -A PREROUTING ! -i pdns-host -m addrtype --dst-type LOCAL -p udp --dport ${toString wgPort} -j DNAT --to-destination [${peerIP}]:${toString wgPort}
|
|
'';
|
|
extraStopCommands = ''
|
|
ip6tables -D FORWARD -i pdns-host -j pinix-dns-forward 2>/dev/null || true
|
|
ip6tables -D FORWARD -o pdns-host -j pinix-dns-forward 2>/dev/null || true
|
|
ip6tables -F pinix-dns-forward 2>/dev/null || true
|
|
ip6tables -X pinix-dns-forward 2>/dev/null || true
|
|
ip6tables -t nat -D POSTROUTING -s ${peerIP}/128 -o wan0 -j MASQUERADE 2>/dev/null || true
|
|
ip6tables -t nat -D PREROUTING ! -i pdns-host -m addrtype --dst-type LOCAL -p udp --dport ${toString wgPort} -j DNAT --to-destination [${peerIP}]:${toString wgPort} 2>/dev/null || true
|
|
'';
|
|
};
|
|
|
|
systemd.services.pinix-dns-netns = {
|
|
description = "Isolated underlay for the new pinix tailnet DNS node";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" "firewall.service" ];
|
|
wants = [ "network-online.target" ];
|
|
path = [ pkgs.iproute2 pkgs.procps pkgs.iptables ];
|
|
serviceConfig = { Type = "oneshot"; RemainAfterExit = true; };
|
|
script = ''
|
|
ip netns add ${ns}
|
|
# IPv6-Forwarding an, weil der Namespace genau zwei Ports aus dem
|
|
# Tailnet an den Host weiterreicht (unten); wie hosts/tnix/pinixnet.nix
|
|
# verwirft der Kernel das DNAT-Paket sonst still. Der FORWARD-Filter
|
|
# laesst nichts anderes durch -- kein Subnet-Router, kein Exit-Node.
|
|
ip netns exec ${ns} sysctl -qw net.ipv4.ip_forward=0 net.ipv6.conf.all.forwarding=1
|
|
ip link add pdns-host type veth peer name pdns-peer netns ${ns}
|
|
ip -6 addr add ${hostIP}/64 dev pdns-host nodad
|
|
ip link set pdns-host up
|
|
ip -n ${ns} link set lo up
|
|
ip -n ${ns} -6 addr add ${peerIP}/64 dev pdns-peer nodad
|
|
ip -n ${ns} link set pdns-peer up
|
|
ip -n ${ns} -6 route add default via ${hostIP}
|
|
|
|
# 443 -> Traefik web-internal, 22 -> sshd (git). SNAT auf peerIP, damit
|
|
# der Host nicht ueber seine eigenen blunix-Tailnet-Routen
|
|
# (fd7a:115c:a1e0::/48 auf tailscale0) antwortet.
|
|
ip netns exec ${ns} ip6tables -P FORWARD DROP
|
|
ip netns exec ${ns} ip6tables -A FORWARD -i tailscale-dns -o pdns-peer -d ${hostIP} -p tcp -m multiport --dports ${toString internalPort},22 -j ACCEPT
|
|
ip netns exec ${ns} ip6tables -A FORWARD -i pdns-peer -o tailscale-dns -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
ip netns exec ${ns} ip6tables -t nat -A PREROUTING -i tailscale-dns -d ${dns6} -p tcp --dport 443 -j DNAT --to-destination [${hostIP}]:${toString internalPort}
|
|
ip netns exec ${ns} ip6tables -t nat -A PREROUTING -i tailscale-dns -d ${dns6} -p tcp --dport 22 -j DNAT --to-destination [${hostIP}]:22
|
|
ip netns exec ${ns} ip6tables -t nat -A POSTROUTING -o pdns-peer -d ${hostIP} -p tcp -m multiport --dports ${toString internalPort},22 -j SNAT --to-source ${peerIP}
|
|
'';
|
|
preStop = ''
|
|
ip link delete pdns-host 2>/dev/null || true
|
|
ip netns delete ${ns}
|
|
'';
|
|
};
|
|
|
|
systemd.services.tailscaled-pinix = {
|
|
description = "Separate Tailscale identity for pinix's new DNS network";
|
|
wantedBy = [ "multi-user.target" ];
|
|
requires = [ "pinix-dns-netns.service" ];
|
|
after = [ "pinix-dns-netns.service" "headscale.service" ];
|
|
partOf = [ "pinix-dns-netns.service" ];
|
|
serviceConfig = {
|
|
NetworkNamespacePath = "/run/netns/${ns}";
|
|
BindReadOnlyPaths = [ "${resolv}:/etc/resolv.conf" "${hosts}:/etc/hosts" ];
|
|
StateDirectory = "tailscale-pinix";
|
|
StateDirectoryMode = "0700";
|
|
RuntimeDirectory = "tailscale-pinix";
|
|
RuntimeDirectoryMode = "0700";
|
|
ExecStart = "${pkgs.tailscale}/bin/tailscaled --state=/var/lib/tailscale-pinix/tailscaled.state --socket=/run/tailscale-pinix/tailscaled.sock --tun=tailscale-dns --port=${toString wgPort}";
|
|
Restart = "on-failure";
|
|
RestartSec = "5s";
|
|
};
|
|
};
|
|
|
|
systemd.services.unbound-pinix = {
|
|
description = "Private DNS for the new pinix tailnet";
|
|
wantedBy = [ "multi-user.target" ];
|
|
requires = [ "pinix-dns-netns.service" ];
|
|
after = [ "pinix-dns-netns.service" "tailscaled-pinix.service" ];
|
|
partOf = [ "pinix-dns-netns.service" ];
|
|
serviceConfig = {
|
|
NetworkNamespacePath = "/run/netns/${ns}";
|
|
DynamicUser = true;
|
|
RuntimeDirectory = "unbound-pinix";
|
|
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
|
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
|
NoNewPrivileges = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
PrivateTmp = true;
|
|
ExecStartPre = "${pkgs.unbound}/bin/unbound-checkconf ${unboundConfig}";
|
|
ExecStart = "${pkgs.unbound}/bin/unbound -d -c ${unboundConfig}";
|
|
Restart = "on-failure";
|
|
RestartSec = "5s";
|
|
};
|
|
};
|
|
}
|