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>
This commit is contained in:
parent
b6131654ea
commit
77eafded92
3 changed files with 190 additions and 0 deletions
|
|
@ -33,6 +33,7 @@
|
||||||
./services/homepage.nix
|
./services/homepage.nix
|
||||||
./services/arr-interconnect.nix
|
./services/arr-interconnect.nix
|
||||||
./services/adguard.nix
|
./services/adguard.nix
|
||||||
|
./services/router.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
### Make build time quicker
|
### Make build time quicker
|
||||||
|
|
|
||||||
36
ports.toml
Normal file
36
ports.toml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# ports.toml — WAN → LAN port forwards for the router (services/router.nix)
|
||||||
|
#
|
||||||
|
# Each [[forward]] block adds a DNAT rule from WAN to the LAN IP below.
|
||||||
|
# Fields:
|
||||||
|
# name — human label, appears in journal logs
|
||||||
|
# port — single port (number), e.g. 443
|
||||||
|
# ports — port range as a string, e.g. "26901-26902"
|
||||||
|
# protocol — "tcp", "udp", or "both"
|
||||||
|
# dest — LAN IP to forward to (optional; defaults to 10.0.0.1)
|
||||||
|
|
||||||
|
dest_default = "10.0.0.1"
|
||||||
|
|
||||||
|
[[forward]]
|
||||||
|
name = "HTTP"
|
||||||
|
port = 80
|
||||||
|
protocol = "tcp"
|
||||||
|
|
||||||
|
[[forward]]
|
||||||
|
name = "HTTPS"
|
||||||
|
port = 443
|
||||||
|
protocol = "tcp"
|
||||||
|
|
||||||
|
[[forward]]
|
||||||
|
name = "SSH"
|
||||||
|
port = 22
|
||||||
|
protocol = "tcp"
|
||||||
|
|
||||||
|
[[forward]]
|
||||||
|
name = "7DTD game"
|
||||||
|
port = 26900
|
||||||
|
protocol = "both"
|
||||||
|
|
||||||
|
[[forward]]
|
||||||
|
name = "7DTD voice/dynamic"
|
||||||
|
ports = "26901-26902"
|
||||||
|
protocol = "udp"
|
||||||
153
services/router.nix
Normal file
153
services/router.nix
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# Port forwards live in ../ports.toml so they're easy to edit.
|
||||||
|
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
portsData = builtins.fromTOML (builtins.readFile ../ports.toml);
|
||||||
|
destDefault = portsData.dest_default;
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
# --- Interface configuration ---
|
||||||
|
systemd.network = {
|
||||||
|
enable = true;
|
||||||
|
networks = {
|
||||||
|
"10-wan" = {
|
||||||
|
matchConfig.Name = "eno1";
|
||||||
|
networkConfig = {
|
||||||
|
DHCP = "ipv4";
|
||||||
|
IPv6AcceptRA = false;
|
||||||
|
};
|
||||||
|
dhcpV4Config = {
|
||||||
|
UseDNS = false; # don't overwrite resolv.conf from ISP DNS
|
||||||
|
UseHostname = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"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;
|
||||||
|
ct state established,related accept
|
||||||
|
ct state invalid drop
|
||||||
|
iifname "lo" accept
|
||||||
|
# LAN is trusted
|
||||||
|
iifname "eth0" accept
|
||||||
|
# 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;
|
||||||
|
ct state established,related accept
|
||||||
|
ct state invalid drop
|
||||||
|
# LAN → anywhere
|
||||||
|
iifname "eth0" accept
|
||||||
|
# WAN → LAN only if it was DNAT'd by a port-forward rule
|
||||||
|
iifname "eno1" oifname "eth0" ct status dnat accept
|
||||||
|
}
|
||||||
|
chain output {
|
||||||
|
type filter hook output priority 0; policy accept;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
tables.nat = {
|
||||||
|
family = "ip";
|
||||||
|
content = ''
|
||||||
|
chain prerouting {
|
||||||
|
type nat hook prerouting priority -100; policy accept;
|
||||||
|
iifname "eno1" jump port_forwards
|
||||||
|
}
|
||||||
|
chain port_forwards {
|
||||||
|
${dnatRules}
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
];
|
||||||
|
# Helpful: log leases to the journal
|
||||||
|
log-dhcp = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue