Add WireGuard, DNS and more to the web UI and router-setup
Web UI: a WireGuard page (tunnels, peers, keys made on the router, client configs with a QR code, import of wg-quick configs, live status), a DNS page (records, wildcards, forward zones), interface dropdowns with VLAN devices, IPv6 options and a local domain. router-setup asks about VLANs, IPv6 and the domain. The router publishes its own name in DNS and hands the local domain to DHCP clients; wg and jq are on the router. Includes a script that takes over antigua's tunnels. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -113,7 +113,7 @@ let
|
||||
}
|
||||
|
||||
# Set by ask/choose (printf -v).
|
||||
disk="" wan="" lan="" wan_type="" want_vlans="" want_v6="" vlan_parent="" vlan_id="" host="" lan_address="" ssh_key="" answer=""
|
||||
disk="" domain="" wan="" lan="" wan_type="" want_vlans="" want_v6="" vlan_parent="" vlan_id="" host="" lan_address="" ssh_key="" answer=""
|
||||
webui_password="" webui_password2="" reuse="" reuse_label="" new=""
|
||||
|
||||
work=$(mktemp -d /run/router-setup.XXXXXX)
|
||||
@@ -326,6 +326,12 @@ let
|
||||
yes_no want_v6 "Enable IPv6 (the provider delegates a prefix to the LAN)?" y
|
||||
echo
|
||||
ask host "Host name" "router"
|
||||
while :; do
|
||||
ask domain "Local domain for DNS and DHCP, like home.example.org (optional)" ""
|
||||
[ -z "$domain" ] && break
|
||||
[[ $domain =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]] && break
|
||||
echo " lower case letters, digits, dashes and dots" >&2
|
||||
done
|
||||
while :; do
|
||||
ask lan_address "Router address on the LAN (a /24 network)" "192.168.1.1"
|
||||
ipv4_ok "$lan_address" && break
|
||||
@@ -348,7 +354,7 @@ let
|
||||
echo " disk: $disk (will be erased)"
|
||||
echo " WAN: $wan, $wan_type''${pppoe_user:+ as $pppoe_user}, IPv6 $([ "$want_v6" = y ] && echo on || echo off)"
|
||||
echo " LAN: $lan, $lan_address/24, DHCP $net.100 - $net.200"
|
||||
echo " host name: $host"
|
||||
echo " host name: $host''${domain:+ (domain $domain)}"
|
||||
echo " web UI: https://$lan_address"
|
||||
ask answer "Type the disk name ($disk) to install"
|
||||
[ "$answer" = "$disk" ] || { echo "aborted" >&2; exit 1; }
|
||||
@@ -362,7 +368,7 @@ let
|
||||
|
||||
keys=$( [ -n "$ssh_key" ] && jq -n --arg k "$ssh_key" '[$k]' || echo '[]')
|
||||
jq -n \
|
||||
--arg host "$host" --arg state "${cfg.stateVersion}" --argjson keys "$keys" \
|
||||
--arg host "$host" --arg domain "$domain" --arg state "${cfg.stateVersion}" --argjson keys "$keys" \
|
||||
--argjson schema ${toString cfg.schemaVersion} \
|
||||
--arg wan "$wan_dev" --arg type "$wan_type" --arg user "$pppoe_user" --arg vlan "$wan_vlan" \
|
||||
--arg lan "$lan_dev" --arg lanvlan "$lan_vlan" --arg v6 "$want_v6" \
|
||||
@@ -370,7 +376,7 @@ let
|
||||
{
|
||||
version: $schema,
|
||||
system: { hostName: $host, stateVersion: $state, sshKeys: $keys },
|
||||
router: {
|
||||
router: ({
|
||||
wan: ({ interface: $wan, ipv4: { type: $type } }
|
||||
| if $type == "pppoe" then .ipv4.pppoe = ({ username: $user, passwordFile: "/var/lib/router/pppoe-password" }
|
||||
+ (if $vlan == "" then {} else { vlan: ($vlan | tonumber) } end)) else . end
|
||||
@@ -385,6 +391,7 @@ let
|
||||
+ (if $v6 == "y" then { ipv6: { pd: true } } else {} end))
|
||||
}
|
||||
}
|
||||
+ (if $domain == "" then {} else { dns: { domain: $domain } } end))
|
||||
}' > "$config/settings.json"
|
||||
|
||||
if [ "$wan_type" = pppoe ]; then
|
||||
|
||||
@@ -71,6 +71,17 @@ in
|
||||
name = "domain-name-servers";
|
||||
data = l.ipv4.address;
|
||||
}
|
||||
]
|
||||
# The local domain: clients use it for short names (nas -> nas.home...).
|
||||
++ lib.optionals (cfg.dns.domain != null) [
|
||||
{
|
||||
name = "domain-name";
|
||||
data = cfg.dns.domain;
|
||||
}
|
||||
{
|
||||
name = "domain-search";
|
||||
data = cfg.dns.domain;
|
||||
}
|
||||
];
|
||||
}) dhcpLans;
|
||||
};
|
||||
|
||||
+20
-2
@@ -28,13 +28,29 @@ let
|
||||
reservations = lib.concatMap (
|
||||
l: lib.mapAttrsToList (n: r: lib.nameValuePair n [ r.address ]) l.dhcp.reservations
|
||||
) lans;
|
||||
# The router's own name resolves to its addresses on the LANs (and their
|
||||
# ULAs), so clients can reach it by name; with a domain also bare, for
|
||||
# clients that have no search domain.
|
||||
selfName = config.networking.hostName;
|
||||
selfAddrs =
|
||||
map (l: l.ipv4.address) lans
|
||||
++ map (l: addrOf l.ipv6.address) (lib.filter (l: l.ipv6.address != null) lans);
|
||||
# Unless dns.hosts names the router itself.
|
||||
selfDefined = lib.hasAttr selfName dns.hosts || lib.hasAttr (fqdn selfName) dns.hosts;
|
||||
selfRecord = lib.optional (!selfDefined) (lib.nameValuePair (fqdn selfName) selfAddrs);
|
||||
# (No reverse record for the bare name: the address has one already.)
|
||||
selfBare = lib.optional (!selfDefined && fqdn selfName != selfName) (
|
||||
lib.nameValuePair selfName selfAddrs
|
||||
);
|
||||
|
||||
records =
|
||||
lib.mapAttrsToList (n: a: lib.nameValuePair (fqdn n) a) dns.hosts
|
||||
selfRecord
|
||||
++ lib.mapAttrsToList (n: a: lib.nameValuePair (fqdn n) a) dns.hosts
|
||||
++ map (r: lib.nameValuePair (fqdn r.name) r.value) reservations
|
||||
++ lib.mapAttrsToList (n: a: lib.nameValuePair n a) dns.overrides;
|
||||
|
||||
# Unbound reads these as one quoted argument.
|
||||
localData = lib.concatMap (r: map (a: ''"${r.name}. ${rrOf a} ${a}"'') r.value) records;
|
||||
localData = lib.concatMap (r: map (a: ''"${r.name}. ${rrOf a} ${a}"'') r.value) (records ++ selfBare);
|
||||
# Reverse records for hosts and reservations (not for wildcard overrides).
|
||||
ptrs = lib.concatMap (r: map (a: ''"${a} ${r.name}."'') r.value) (
|
||||
lib.filter (r: !(lib.hasAttr r.name dns.overrides)) records
|
||||
@@ -72,6 +88,8 @@ in
|
||||
++ map (n: "${n} allow") dnsPeerNets;
|
||||
local-zone =
|
||||
lib.optional (domain != null) "${domain}. static"
|
||||
# The router's bare name is a zone of its own (not under the domain).
|
||||
++ lib.optional (!lib.hasInfix "." selfName) "${selfName}. transparent"
|
||||
++ map (n: "${n}. redirect") (lib.attrNames dns.overrides);
|
||||
local-data = localData;
|
||||
local-data-ptr = ptrs;
|
||||
|
||||
@@ -125,6 +125,8 @@ in
|
||||
environment.systemPackages = [
|
||||
apply
|
||||
webui
|
||||
pkgs.jq # for looking at and editing settings.json by hand
|
||||
pkgs.wireguard-tools # wg: the tunnels' state, from a shell
|
||||
];
|
||||
|
||||
# The web UI starts these rather than running anything itself, so a switch
|
||||
@@ -156,10 +158,13 @@ in
|
||||
description = "Router web UI";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
# iproute2: the dashboard reads the default route and the PPP peer.
|
||||
# iproute2: the dashboard reads the default route and the PPP peer;
|
||||
# wireguard-tools: the tunnels' status; qrencode: client configs.
|
||||
path = [
|
||||
config.systemd.package
|
||||
pkgs.iproute2
|
||||
pkgs.wireguard-tools
|
||||
pkgs.qrencode
|
||||
];
|
||||
serviceConfig = {
|
||||
ExecStart = "${lib.getExe webui} -listen :${toString cfg.webui.port} -config ${cfg.directory} -state ${cfg.stateDirectory}";
|
||||
|
||||
Reference in New Issue
Block a user