Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
# services/router.nix — mediaserver acts as the home router
|
|
|
|
|
#
|
|
|
|
|
# Layout:
|
|
|
|
|
# eno1 = WAN (DHCP from ISP; in phase 1, from the eero still in router mode)
|
|
|
|
|
# eth0 = LAN (static 10.0.0.1/24, serves DHCP to downstream clients)
|
|
|
|
|
#
|
|
|
|
|
# Services on this box:
|
|
|
|
|
# - systemd-networkd: interface management (replaces NetworkManager here)
|
|
|
|
|
# - nftables: NAT (masquerade out WAN) + firewall (drop WAN inbound except ports.toml)
|
|
|
|
|
# - dnsmasq: DHCP only (port 0 for DNS — AdGuard Home owns :53)
|
|
|
|
|
# - AdGuard Home (already running): DNS for LAN clients
|
|
|
|
|
#
|
2026-08-15 12:15:54 +01:00
|
|
|
# Port forwards live in ../ports.toml and LAN devices (static reservations +
|
|
|
|
|
# block list) in ../devices.toml, so both are easy to edit — by hand, or via
|
|
|
|
|
# the router UI (services/router-ui.nix), which only ever writes those two
|
|
|
|
|
# TOML files and never generates Nix.
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
|
|
|
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
|
let
|
2026-09-14 11:50:22 +01:00
|
|
|
portsData = (import ./validate-forwards.nix { inherit lib; })
|
|
|
|
|
(builtins.fromTOML (builtins.readFile ../ports.toml));
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
destDefault = portsData.dest_default;
|
|
|
|
|
|
2026-08-15 12:15:54 +01:00
|
|
|
devices = (builtins.fromTOML (builtins.readFile ../devices.toml)).device or [ ];
|
|
|
|
|
reservedDevices = builtins.filter (d: d ? ip) devices;
|
|
|
|
|
blockedDevices = builtins.filter (d: d.blocked or false) devices;
|
|
|
|
|
|
|
|
|
|
# Drop everything from a blocked MAC arriving on the LAN. These are emitted
|
|
|
|
|
# at the TOP of the input and forward chains, ahead of the `ct state
|
|
|
|
|
# established,related accept` line — otherwise a device that was already
|
|
|
|
|
# talking keeps its existing flows alive indefinitely.
|
|
|
|
|
#
|
|
|
|
|
# ponytail: conntrack entries created before the block still linger until
|
|
|
|
|
# they time out (a few minutes). Add `conntrack -D -s <ip>` to the apply
|
|
|
|
|
# path if that wait ever matters.
|
|
|
|
|
# ponytail: MAC-based, so a device that randomises its MAC walks around it.
|
|
|
|
|
blockRules = lib.concatMapStringsSep "\n "
|
|
|
|
|
(d: ''iifname "eth0" ether saddr ${d.mac} drop comment "${d.name} blocked"'')
|
|
|
|
|
blockedDevices;
|
|
|
|
|
|
2026-09-12 12:52:27 +01:00
|
|
|
isolatedDevices = builtins.filter (d: d.isolated or false) devices;
|
|
|
|
|
|
|
|
|
|
# Forward-chain-only drop: the device may still talk to the router itself,
|
|
|
|
|
# but the router will not carry its traffic anywhere. For the IP camera that
|
|
|
|
|
# means go2rtc keeps pulling RTSP (router is the endpoint, so that path never
|
|
|
|
|
# reaches the forward hook) while the camera loses the internet and every
|
|
|
|
|
# other LAN host — the isolation a separate VLAN would have given us.
|
|
|
|
|
#
|
|
|
|
|
# Emitted at the TOP of the forward chain for the same reason blockRules are:
|
|
|
|
|
# placed after `ct state established,related accept`, an already-open session
|
|
|
|
|
# to the vendor cloud would survive indefinitely.
|
|
|
|
|
#
|
|
|
|
|
# ponytail: MAC-based like blockRules, so it inherits the same weakness — a
|
|
|
|
|
# device that randomises its MAC walks around it. Fine for an IP camera,
|
|
|
|
|
# which won't; match on the DHCP-reserved IP too if that ever changes.
|
2026-09-12 12:59:22 +01:00
|
|
|
# `counter` so the rule is self-verifying: `nft list chain inet filter
|
|
|
|
|
# forward` shows how much the device has tried to send off-LAN. A climbing
|
|
|
|
|
# number means isolation is doing work; a flat zero means either a quiet
|
|
|
|
|
# device or a rule that stopped matching, and those are worth telling apart.
|
2026-09-12 12:52:27 +01:00
|
|
|
isolateRules = lib.concatMapStringsSep "\n "
|
2026-09-12 12:59:22 +01:00
|
|
|
(d: ''iifname "eth0" ether saddr ${d.mac} counter drop comment "${d.name} isolated (no routing)"'')
|
2026-09-12 12:52:27 +01:00
|
|
|
isolatedDevices;
|
|
|
|
|
|
2026-04-24 10:52:11 +01:00
|
|
|
# Phase-1 transition list; empty now that eero is in bridge mode and
|
|
|
|
|
# eno1 is strictly the ISP-facing WAN.
|
|
|
|
|
trustedLegacyCidrs = [ ];
|
2026-04-24 10:17:06 +01:00
|
|
|
|
|
|
|
|
legacyTrustRules = lib.concatMapStringsSep "\n "
|
|
|
|
|
(cidr: ''iifname "eno1" ip saddr ${cidr} accept'')
|
|
|
|
|
trustedLegacyCidrs;
|
|
|
|
|
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
# Expand "both" into [tcp, udp]; normalise port vs ports; default dest.
|
|
|
|
|
expandForward = entry:
|
|
|
|
|
let
|
|
|
|
|
protos = if entry.protocol == "both" then [ "tcp" "udp" ] else [ entry.protocol ];
|
|
|
|
|
portExpr =
|
|
|
|
|
if entry ? port then toString entry.port
|
|
|
|
|
else if entry ? ports then builtins.replaceStrings [ "-" ] [ "-" ] entry.ports
|
|
|
|
|
else throw "ports.toml entry '${entry.name}' has neither 'port' nor 'ports'";
|
|
|
|
|
dest = entry.dest or destDefault;
|
|
|
|
|
in
|
|
|
|
|
map (p: { inherit (entry) name; proto = p; port = portExpr; dest = dest; }) protos;
|
|
|
|
|
|
|
|
|
|
forwards = lib.concatMap expandForward portsData.forward;
|
|
|
|
|
|
2026-09-15 14:59:40 +01:00
|
|
|
# LAN clients normally resolve nordhammer.it to 10.0.0.1 through AdGuard.
|
|
|
|
|
# Some game/browser stacks retain or independently resolve the public WAN
|
|
|
|
|
# address, though. Reflect only Pelican's allocation range back to its LAN
|
|
|
|
|
# destination so those clients take the same path as an external player.
|
|
|
|
|
# Selecting the declared forward keeps this in sync with ports.toml.
|
|
|
|
|
pelicanForwards = builtins.filter
|
|
|
|
|
(f: f.name == "Pelican game servers")
|
|
|
|
|
forwards;
|
|
|
|
|
lanGameReflectionRules = lib.concatMapStringsSep "\n "
|
|
|
|
|
(f: ''${f.proto} dport ${f.port} dnat to ${f.dest} comment "${f.name} LAN reflection"'')
|
|
|
|
|
pelicanForwards;
|
|
|
|
|
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
# nftables accepts port-range literals like "26901-26902" as-is.
|
|
|
|
|
dnatRules = lib.concatMapStringsSep "\n "
|
|
|
|
|
(f: ''${f.proto} dport ${f.port} dnat to ${f.dest} comment "${f.name}"'')
|
|
|
|
|
forwards;
|
|
|
|
|
|
2026-04-24 10:22:56 +01:00
|
|
|
# Input-chain accept rules so WAN traffic to forwarded ports reaches the
|
|
|
|
|
# mediaserver. Works in both phases:
|
|
|
|
|
# phase 1: eero DNATs to 192.168.4.25, arrives on eno1 — matched here.
|
|
|
|
|
# phase 2: our DNAT rewrites dst to 10.0.0.1 (local), arrives on eno1 — matched here.
|
|
|
|
|
wanPortInputRules = lib.concatMapStringsSep "\n "
|
|
|
|
|
(f: ''iifname "eno1" ${f.proto} dport ${f.port} accept comment "${f.name}"'')
|
|
|
|
|
forwards;
|
|
|
|
|
|
2026-09-14 11:50:22 +01:00
|
|
|
# Docker also creates DNAT rules. A DNAT flag alone is not permission to
|
|
|
|
|
# enter from WAN: check the published/original port, before translation.
|
|
|
|
|
# The declared Pelican allocation range remains automatic for new servers.
|
|
|
|
|
wanPortForwardRules = lib.concatMapStringsSep "\n "
|
|
|
|
|
(f: ''iifname "eno1" ct status dnat meta l4proto ${f.proto} ct original proto-dst ${f.port} accept comment "${f.name}"'')
|
|
|
|
|
forwards;
|
|
|
|
|
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
in
|
|
|
|
|
{
|
|
|
|
|
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
|
|
|
|
|
|
|
|
|
|
# --- Networking stack: systemd-networkd owns the router NICs ---
|
|
|
|
|
networking.networkmanager.enable = lib.mkForce false;
|
|
|
|
|
networking.useNetworkd = true;
|
|
|
|
|
services.resolved.enable = false; # AdGuard Home binds :53
|
|
|
|
|
|
|
|
|
|
# Disable the scripted firewall — nftables takes over below.
|
|
|
|
|
networking.firewall.enable = false;
|
|
|
|
|
|
|
|
|
|
# IP forwarding is required for routing.
|
|
|
|
|
boot.kernel.sysctl = {
|
|
|
|
|
"net.ipv4.ip_forward" = 1;
|
|
|
|
|
"net.ipv4.conf.all.rp_filter" = 1;
|
|
|
|
|
"net.ipv6.conf.all.forwarding" = 0; # no IPv6 upstream yet
|
2026-05-20 13:44:17 +01:00
|
|
|
"net.ipv6.conf.all.disable_ipv6" = 1;
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
};
|
|
|
|
|
|
2026-05-22 09:27:10 +01:00
|
|
|
# Pin interface names to MAC addresses so they never swap across boots.
|
|
|
|
|
# Without this, "eth0" is an unpredictable kernel name that depends on
|
|
|
|
|
# device probe order — if the NICs swap, the entire LAN/WAN config breaks.
|
|
|
|
|
services.udev.extraRules = ''
|
|
|
|
|
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="6c:0b:84:0c:4c:58", NAME="eth0"
|
|
|
|
|
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="6c:0b:84:0c:4c:59", NAME="eno1"
|
|
|
|
|
'';
|
|
|
|
|
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
# --- Interface configuration ---
|
|
|
|
|
systemd.network = {
|
|
|
|
|
enable = true;
|
2026-05-22 09:02:03 +01:00
|
|
|
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
networks = {
|
|
|
|
|
"10-wan" = {
|
|
|
|
|
matchConfig.Name = "eno1";
|
|
|
|
|
networkConfig = {
|
|
|
|
|
DHCP = "ipv4";
|
|
|
|
|
IPv6AcceptRA = false;
|
|
|
|
|
};
|
|
|
|
|
dhcpV4Config = {
|
|
|
|
|
UseDNS = false; # don't overwrite resolv.conf from ISP DNS
|
|
|
|
|
UseHostname = false;
|
2026-08-24 19:49:59 +01:00
|
|
|
# networkd defaults SendRelease=true: stopping the unit (every
|
|
|
|
|
# nixos-upgrade, and the watchdog's 4min escalation) sends a
|
|
|
|
|
# DHCPRELEASE that hands our IP back to the ISP pool, so the next
|
|
|
|
|
# start DISCOVERs a fresh one. Every WAN IP change in the journal
|
|
|
|
|
# landed at 05:1x — the upgrade window — not on ISP timing.
|
|
|
|
|
# false = keep the lease and re-REQUEST it (INIT-REBOOT).
|
|
|
|
|
SendRelease = false;
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
};
|
2026-09-01 10:05:37 +01:00
|
|
|
|
|
|
|
|
# CAKE shaper on WAN egress. eno1 carries fq_codel at 1Gbit line
|
|
|
|
|
# rate, so it never actually engages: the uplink is ~70Mbit, which
|
|
|
|
|
# means the real queue forms upstream in TalkTalk's kit where we
|
|
|
|
|
# can't touch it. Shaping just under measured line rate pulls that
|
|
|
|
|
# queue down here, where CAKE's per-flow fairness keeps sparse
|
|
|
|
|
# latency-sensitive traffic (games, VoIP) from sitting behind a bulk
|
|
|
|
|
# upload. Measured by router-speedtest: 8.73-9.52 MB/s up, i.e.
|
|
|
|
|
# ~70Mbit floor; 63M is 90% of that.
|
|
|
|
|
#
|
|
|
|
|
# NAT=true so flow isolation resolves real LAN hosts rather than
|
|
|
|
|
# lumping the whole house behind one post-NAT address. Kernel default
|
|
|
|
|
# FlowIsolationMode (triple-isolate) is host-aware, so this matters.
|
|
|
|
|
#
|
|
|
|
|
# Egress only: 500Mbit down is far harder to saturate and shaping it
|
|
|
|
|
# would need an IFB device for not much gain.
|
|
|
|
|
cakeConfig = {
|
|
|
|
|
Parent = "root";
|
|
|
|
|
Bandwidth = "63M";
|
|
|
|
|
NAT = true;
|
|
|
|
|
};
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
};
|
|
|
|
|
"20-lan" = {
|
|
|
|
|
matchConfig.Name = "eth0";
|
|
|
|
|
networkConfig = {
|
|
|
|
|
Address = "10.0.0.1/24";
|
|
|
|
|
ConfigureWithoutCarrier = true;
|
|
|
|
|
IPv6AcceptRA = false;
|
|
|
|
|
};
|
|
|
|
|
linkConfig.RequiredForOnline = "no";
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
# --- nftables: NAT + firewall ---
|
|
|
|
|
networking.nftables = {
|
|
|
|
|
enable = true;
|
|
|
|
|
tables.filter = {
|
|
|
|
|
family = "inet";
|
|
|
|
|
content = ''
|
|
|
|
|
chain input {
|
|
|
|
|
type filter hook input priority 0; policy drop;
|
2026-08-15 12:15:54 +01:00
|
|
|
# Blocked devices first — before the conntrack accept.
|
|
|
|
|
${blockRules}
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
ct state established,related accept
|
|
|
|
|
ct state invalid drop
|
|
|
|
|
iifname "lo" accept
|
|
|
|
|
# LAN is trusted
|
|
|
|
|
iifname "eth0" accept
|
2026-05-21 15:02:09 +01:00
|
|
|
# Container bridges reaching host services (e.g. Profilarr → Radarr
|
|
|
|
|
# on 10.0.0.1:7878, Forgejo runner → AdGuard DNS). br-* covers
|
|
|
|
|
# Docker user-defined networks, podman* covers Podman (rootful).
|
2026-08-27 20:19:34 +01:00
|
|
|
# pelican0 is wings' own bridge (config.yml names it), so br-* misses it.
|
|
|
|
|
iifname { "docker0", "br-*", "podman*", "pelican0" } accept
|
2026-04-24 10:17:06 +01:00
|
|
|
# Phase 1: also trust the existing eero subnet on eno1 so SSH
|
|
|
|
|
# and AdGuard DNS keep working during the transition.
|
|
|
|
|
${legacyTrustRules}
|
2026-04-24 10:22:56 +01:00
|
|
|
# Accept WAN traffic for ports we publicly expose (ports.toml).
|
|
|
|
|
${wanPortInputRules}
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
# ICMP from anywhere (ping, path-MTU)
|
|
|
|
|
icmp type echo-request accept
|
|
|
|
|
icmpv6 type echo-request accept
|
|
|
|
|
}
|
|
|
|
|
chain forward {
|
|
|
|
|
type filter hook forward priority 0; policy drop;
|
2026-09-12 12:52:27 +01:00
|
|
|
# Blocked and isolated devices first — before the conntrack accept.
|
2026-08-15 12:15:54 +01:00
|
|
|
${blockRules}
|
2026-09-12 12:52:27 +01:00
|
|
|
${isolateRules}
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
ct state established,related accept
|
|
|
|
|
ct state invalid drop
|
|
|
|
|
# LAN → anywhere
|
|
|
|
|
iifname "eth0" accept
|
2026-05-21 15:02:09 +01:00
|
|
|
# Containers → anywhere (image pulls, Forgejo runner workflows,
|
|
|
|
|
# etc.). br-* = Docker custom networks, podman* = Podman rootful.
|
2026-08-27 20:19:34 +01:00
|
|
|
# pelican0 = wings' bridge; it does not match br-*.
|
|
|
|
|
iifname { "docker0", "br-*", "podman*", "pelican0" } accept
|
2026-09-14 11:50:22 +01:00
|
|
|
# WAN → explicitly published ports only, including Docker DNAT.
|
|
|
|
|
${wanPortForwardRules}
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
}
|
|
|
|
|
chain output {
|
|
|
|
|
type filter hook output priority 0; policy accept;
|
|
|
|
|
}
|
|
|
|
|
'';
|
|
|
|
|
};
|
2026-08-15 12:42:50 +01:00
|
|
|
# Per-device byte counters for the router UI's traffic page.
|
|
|
|
|
#
|
|
|
|
|
# Dynamic sets keyed on the LAN address, so hosts appear on their own —
|
|
|
|
|
# nothing here needs to know which devices exist. prerouting/postrouting
|
|
|
|
|
# rather than forward, because forward misses LAN↔router traffic, and on
|
|
|
|
|
# this box that includes every Jellyfin stream. Priority -300 puts both
|
|
|
|
|
# chains ahead of NAT, so addresses are still the device's own.
|
|
|
|
|
#
|
|
|
|
|
# Counters reset whenever this table reloads (i.e. every switch that
|
|
|
|
|
# touches nftables). router-accounting.service snapshots them each
|
|
|
|
|
# minute and accumulates deltas into a file that survives, so the UI's
|
|
|
|
|
# totals are continuous even though these counters aren't.
|
|
|
|
|
tables.accounting = {
|
|
|
|
|
family = "ip";
|
|
|
|
|
content = ''
|
|
|
|
|
set up {
|
|
|
|
|
type ipv4_addr
|
|
|
|
|
flags dynamic
|
|
|
|
|
counter
|
|
|
|
|
timeout 7d
|
|
|
|
|
}
|
|
|
|
|
set down {
|
|
|
|
|
type ipv4_addr
|
|
|
|
|
flags dynamic
|
|
|
|
|
counter
|
|
|
|
|
timeout 7d
|
|
|
|
|
}
|
|
|
|
|
chain pre {
|
|
|
|
|
type filter hook prerouting priority -300; policy accept;
|
|
|
|
|
iifname "eth0" update @up { ip saddr }
|
|
|
|
|
}
|
|
|
|
|
chain post {
|
|
|
|
|
type filter hook postrouting priority -300; policy accept;
|
|
|
|
|
oifname "eth0" update @down { ip daddr }
|
|
|
|
|
}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-26 19:43:33 +01:00
|
|
|
# Use a distinct table name so we don't share `ip nat` with Docker —
|
|
|
|
|
# Docker manages its own DOCKER/PREROUTING chains in `ip nat`, and
|
|
|
|
|
# NixOS's nftables module rebuilds whichever tables it owns on every
|
|
|
|
|
# activation, which would wipe Docker's rules. Hooks at the same
|
|
|
|
|
# priority across separate tables coexist fine.
|
|
|
|
|
tables.router-nat = {
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
family = "ip";
|
|
|
|
|
content = ''
|
|
|
|
|
chain prerouting {
|
|
|
|
|
type nat hook prerouting priority -100; policy accept;
|
|
|
|
|
iifname "eno1" jump port_forwards
|
2026-09-15 14:59:40 +01:00
|
|
|
# Hairpin only public/local-address requests from the LAN. Direct
|
|
|
|
|
# 10.0.0.x traffic and every non-Pelican port remain untouched.
|
|
|
|
|
iifname "eth0" ip daddr != 10.0.0.0/24 fib daddr type local jump lan_game_reflection
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
}
|
|
|
|
|
chain port_forwards {
|
|
|
|
|
${dnatRules}
|
|
|
|
|
}
|
2026-09-15 14:59:40 +01:00
|
|
|
chain lan_game_reflection {
|
|
|
|
|
${lanGameReflectionRules}
|
|
|
|
|
}
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
chain postrouting {
|
|
|
|
|
type nat hook postrouting priority 100; policy accept;
|
|
|
|
|
oifname "eno1" masquerade
|
|
|
|
|
}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
# --- DHCP server on the LAN ---
|
|
|
|
|
services.dnsmasq = {
|
|
|
|
|
enable = true;
|
|
|
|
|
settings = {
|
|
|
|
|
interface = "eth0";
|
|
|
|
|
bind-interfaces = true;
|
|
|
|
|
# AdGuard Home owns DNS; dnsmasq only does DHCP.
|
|
|
|
|
port = 0;
|
|
|
|
|
dhcp-range = [ "10.0.0.100,10.0.0.250,12h" ];
|
|
|
|
|
dhcp-option = [
|
|
|
|
|
"option:router,10.0.0.1"
|
|
|
|
|
"option:dns-server,10.0.0.1"
|
|
|
|
|
];
|
2026-08-15 12:15:54 +01:00
|
|
|
# Static reservations — format: "MAC,label,IP". From ../devices.toml.
|
|
|
|
|
dhcp-host = map (d: "${d.mac},${d.name},${d.ip}") reservedDevices;
|
Turn mediaserver into a home router
Adds services/router.nix with systemd-networkd (eno1=WAN via DHCP,
eth0=LAN 10.0.0.1/24), nftables (NAT + firewall, default drop on WAN
in), dnsmasq (DHCP only — AdGuard Home keeps :53 for DNS), and sysctl
IP forwarding. NetworkManager is forced off on this host.
Port forwards live in ports.toml at the repo root and are imported via
builtins.fromTOML. Supports single ports, ranges ("26901-26902"), and
"both" protocol. Initial forwards: 22, 80, 443, 26900, 26901-26902.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 09:48:38 +01:00
|
|
|
# Helpful: log leases to the journal
|
|
|
|
|
log-dhcp = true;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
}
|