Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c589b8d0c | ||
|
|
c9b04711c9 |
@@ -21,9 +21,9 @@ mgmt-only trust model, SSH exposure and the Wi-Fi bridge ports. Run it with
|
|||||||
## What each gateway runs
|
## What each gateway runs
|
||||||
|
|
||||||
| Function | Implementation |
|
| Function | Implementation |
|
||||||
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| WAN | PPPoE (`pppd`), per-site ISP credentials via clan vars prompts; `wan.vlanId` when the ISP tags the session (AIS: 10); `wan.macAddress` to clone the old router's MAC if the ISP has it pinned |
|
| WAN | PPPoE (`pppd`), per-site ISP credentials via clan vars prompts; `wan.vlanId` when the ISP tags the session (AIS: 10); `wan.macAddress` to clone the old router's MAC if the ISP has it pinned |
|
||||||
| LAN | VLAN-filtering bridge `br0` over the trunk ports (networkd); `accessPorts` pin a port untagged to one VLAN — convention: the last copper port is an untagged `mgmt` recovery port |
|
| LAN | VLAN-filtering bridge `br0` over the trunk ports (networkd); `accessPorts` pin a port untagged to one VLAN — convention: the last copper port is an untagged `mgmt` recovery port; `stagingPort` turns a spare port into a DHCP uplink into the old LAN while the box runs alongside the router it replaces (drop at cutover) |
|
||||||
| Firewall/NAT | nftables: default-deny WAN, no inter-VLAN, MSS clamp, v4 NAT |
|
| Firewall/NAT | nftables: default-deny WAN, no inter-VLAN, MSS clamp, v4 NAT |
|
||||||
| DHCP | Kea, one subnet per VLAN |
|
| DHCP | Kea, one subnet per VLAN |
|
||||||
| DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control |
|
| DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control |
|
||||||
|
|||||||
+5
-4
@@ -40,10 +40,11 @@ in
|
|||||||
wan.interface = "enp1s0";
|
wan.interface = "enp1s0";
|
||||||
wan.vlanId = null; # this ISP runs PPPoE untagged on the port
|
wan.vlanId = null; # this ISP runs PPPoE untagged on the port
|
||||||
wan.macAddress = "a8:b8:e0:01:06:87";
|
wan.macAddress = "a8:b8:e0:01:06:87";
|
||||||
trunkPorts = [
|
trunkPorts = [ "enp2s0" ];
|
||||||
"enp2s0"
|
# STAGING (remove at cutover, move enp3s0 back into trunkPorts):
|
||||||
# "enp3s0" # STAGING: serves as the uplink until cutover (see the machine config)
|
# DHCP-client uplink into the existing OPNsense LAN so the box has
|
||||||
];
|
# internet + mesh while it runs alongside the old router.
|
||||||
|
stagingPort = "enp3s0";
|
||||||
# Dedicated on-site recovery port: untagged mgmt, always available even
|
# Dedicated on-site recovery port: untagged mgmt, always available even
|
||||||
# if the switch config is broken.
|
# if the switch config is broken.
|
||||||
accessPorts.enp4s0 = "mgmt";
|
accessPorts.enp4s0 = "mgmt";
|
||||||
|
|||||||
@@ -16,18 +16,6 @@
|
|||||||
builtins.hashString "sha256" config.networking.hostName
|
builtins.hashString "sha256" config.networking.hostName
|
||||||
);
|
);
|
||||||
|
|
||||||
# STAGING (remove at cutover, and restore enp3s0 to trunkPorts in clan.nix):
|
|
||||||
# DHCP-client uplink into the existing OPNsense LAN so the box has internet +
|
|
||||||
# mesh while it runs alongside the old router. Default-deny firewall on this
|
|
||||||
# interface (it's in no VLAN zone); PPPoE simply retries until the WAN port
|
|
||||||
# is cabled. Do NOT connect the trunk ports to the production switch while
|
|
||||||
# staging — Kea on tag 10 would fight the OPNsense LAN DHCP in one broadcast
|
|
||||||
# domain.
|
|
||||||
systemd.network.networks."05-staging" = {
|
|
||||||
matchConfig.Name = "enp3s0";
|
|
||||||
networkConfig.DHCP = "ipv4";
|
|
||||||
};
|
|
||||||
|
|
||||||
time.timeZone = "Etc/GMT-7"; # UTC+7 (Thailand, fixed offset, no DST)
|
time.timeZone = "Etc/GMT-7"; # UTC+7 (Thailand, fixed offset, no DST)
|
||||||
services.chrony.enable = true;
|
services.chrony.enable = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ inventory.instances.router = {
|
|||||||
wan.vlanId = 10; # or null for untagged PPPoE
|
wan.vlanId = 10; # or null for untagged PPPoE
|
||||||
trunkPorts = [ "enp2s0" ];
|
trunkPorts = [ "enp2s0" ];
|
||||||
accessPorts.enp4s0 = "mgmt"; # untagged on-site recovery port
|
accessPorts.enp4s0 = "mgmt"; # untagged on-site recovery port
|
||||||
|
# stagingPort = "enp3s0"; # DHCP uplink into the old LAN until cutover
|
||||||
vlans = {
|
vlans = {
|
||||||
mgmt.id = 10;
|
mgmt.id = 10;
|
||||||
lan.id = 20;
|
lan.id = 20;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# other VLANs -> DNS/DHCP on the router + WAN (if allowWan); no inter-VLAN
|
# other VLANs -> DNS/DHCP on the router + WAN (if allowWan); no inter-VLAN
|
||||||
# WAN (ppp0) -> nothing inbound beyond established/related
|
# WAN (ppp0) -> nothing inbound beyond established/related
|
||||||
# mesh -> admin SSH + metrics scrapes (same trust boundary as the fleet)
|
# mesh -> admin SSH + metrics scrapes (same trust boundary as the fleet)
|
||||||
|
# staging -> admin SSH only (pre-cutover uplink into the old LAN)
|
||||||
{ settings }:
|
{ settings }:
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
@@ -26,14 +27,20 @@ in
|
|||||||
filterForward = true;
|
filterForward = true;
|
||||||
trustedInterfaces = [ "vlan-mgmt" ];
|
trustedInterfaces = [ "vlan-mgmt" ];
|
||||||
|
|
||||||
# Non-mgmt VLANs may only talk to the router's DNS and DHCP.
|
# Non-mgmt VLANs may only talk to the router's DNS and DHCP; the staging
|
||||||
interfaces = lib.genAttrs nonMgmtIfs (_: {
|
# uplink (old LAN, pre-cutover) gets admin SSH so the box can be reached
|
||||||
|
# before the mgmt VLAN or the mesh are up.
|
||||||
|
interfaces =
|
||||||
|
lib.genAttrs nonMgmtIfs (_: {
|
||||||
allowedTCPPorts = [ 53 ];
|
allowedTCPPorts = [ 53 ];
|
||||||
allowedUDPPorts = [
|
allowedUDPPorts = [
|
||||||
53
|
53
|
||||||
67
|
67
|
||||||
];
|
];
|
||||||
});
|
})
|
||||||
|
// lib.optionalAttrs (cfg.stagingPort != null) {
|
||||||
|
${cfg.stagingPort}.allowedTCPPorts = [ 22 ];
|
||||||
|
};
|
||||||
|
|
||||||
extraInputRules = ''
|
extraInputRules = ''
|
||||||
ip6 saddr ${cfg.mesh.subnet} tcp dport 22 accept comment "admin ssh over the mesh"
|
ip6 saddr ${cfg.mesh.subnet} tcp dport 22 accept comment "admin ssh over the mesh"
|
||||||
|
|||||||
@@ -250,6 +250,22 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
stagingPort = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "enp3s0";
|
||||||
|
description = ''
|
||||||
|
Temporary DHCPv4-client uplink into the existing LAN while the box
|
||||||
|
runs alongside the router it replaces: gives it internet + mesh
|
||||||
|
before the WAN port is cabled (PPPoE simply retries until then). The
|
||||||
|
port is in no VLAN zone; the firewall admits only SSH on it. Do NOT
|
||||||
|
connect the trunk ports to the production switch while staging —
|
||||||
|
Kea on the mgmt tag would fight the old router's DHCP in one
|
||||||
|
broadcast domain. Set to null at cutover (and usually hand the port
|
||||||
|
back to `trunkPorts`).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
vlans = lib.mkOption {
|
vlans = lib.mkOption {
|
||||||
type = lib.types.attrsOf (lib.types.submodule vlanModule);
|
type = lib.types.attrsOf (lib.types.submodule vlanModule);
|
||||||
description = "VLANs served at this site; `mgmt` and `lan` are mandatory.";
|
description = "VLANs served at this site; `mgmt` and `lan` are mandatory.";
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ in
|
|||||||
assertion = lib.all (p: !(cfg.accessPorts ? ${p})) cfg.trunkPorts;
|
assertion = lib.all (p: !(cfg.accessPorts ? ${p})) cfg.trunkPorts;
|
||||||
message = "router: a port cannot be both a trunk and an access port.";
|
message = "router: a port cannot be both a trunk and an access port.";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
cfg.stagingPort == null
|
||||||
|
|| !(
|
||||||
|
cfg.stagingPort == cfg.wan.interface
|
||||||
|
|| lib.elem cfg.stagingPort cfg.trunkPorts
|
||||||
|
|| cfg.accessPorts ? ${cfg.stagingPort}
|
||||||
|
);
|
||||||
|
message = "router: stagingPort ${toString cfg.stagingPort} is also the WAN, a trunk or an access port.";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# Router diagnostics toolkit: packets (tcpdump), path (mtr), link
|
# Router diagnostics toolkit: packets (tcpdump), path (mtr), link
|
||||||
@@ -73,7 +83,15 @@ in
|
|||||||
let
|
let
|
||||||
taggedAll = lib.mapAttrsToList (_: vlan: { VLAN = vlan.id; }) cfg.vlans;
|
taggedAll = lib.mapAttrsToList (_: vlan: { VLAN = vlan.id; }) cfg.vlans;
|
||||||
in
|
in
|
||||||
{
|
lib.optionalAttrs (cfg.stagingPort != null) {
|
||||||
|
# Staging uplink (see interface.nix): plain DHCPv4 client on a spare
|
||||||
|
# port, no bridge/VLAN membership, so the firewall treats it as untrusted.
|
||||||
|
"05-staging" = {
|
||||||
|
matchConfig.Name = cfg.stagingPort;
|
||||||
|
networkConfig.DHCP = "ipv4";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// {
|
||||||
# WAN port carries only the PPPoE session; no IP config of its own.
|
# WAN port carries only the PPPoE session; no IP config of its own.
|
||||||
"10-wan" = {
|
"10-wan" = {
|
||||||
matchConfig.Name = cfg.wan.interface;
|
matchConfig.Name = cfg.wan.interface;
|
||||||
|
|||||||
Reference in New Issue
Block a user