Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e316ec56ce | ||
|
|
559115e52b | ||
|
|
9cfcf91b51 | ||
|
|
570fe64497 | ||
|
|
2ac4215237 | ||
|
|
d51a334ec6 | ||
|
|
b81c0aaa89 | ||
|
|
fcd8e55024 |
@@ -13,7 +13,7 @@ Naming: `gw-<city>-<n>`, e.g. `gw-cnx-1`.
|
|||||||
| 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) |
|
| 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 |
|
||||||
| 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 |
|
||||||
|
|||||||
Generated
+4
@@ -247,6 +247,10 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clan-core": "clan-core",
|
"clan-core": "clan-core",
|
||||||
|
"flake-parts": [
|
||||||
|
"clan-core",
|
||||||
|
"flake-parts"
|
||||||
|
],
|
||||||
"nixos-mailserver": "nixos-mailserver",
|
"nixos-mailserver": "nixos-mailserver",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"clan-core",
|
"clan-core",
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-26.05";
|
inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-26.05";
|
||||||
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
|
inputs.flake-parts.follows = "clan-core/flake-parts";
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
inputs@{
|
||||||
self,
|
self,
|
||||||
clan-core,
|
clan-core,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
|
flake-parts,
|
||||||
...
|
...
|
||||||
}@inputs:
|
}:
|
||||||
let
|
let
|
||||||
# Usage see: https://docs.clan.lol
|
# Usage see: https://docs.clan.lol
|
||||||
clan = clan-core.lib.clan {
|
clan = clan-core.lib.clan {
|
||||||
@@ -36,38 +39,31 @@
|
|||||||
"age-plugin-fido2-hmac"
|
"age-plugin-fido2-hmac"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
in
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
systems = [
|
systems = [
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
"aarch64-linux"
|
"aarch64-linux"
|
||||||
"aarch64-darwin"
|
"aarch64-darwin"
|
||||||
"x86_64-darwin"
|
"x86_64-darwin"
|
||||||
];
|
];
|
||||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
|
||||||
pkgsFor = system: clan-core.inputs.nixpkgs.legacyPackages.${system};
|
|
||||||
treefmtFor = system: inputs.treefmt-nix.lib.evalModule (pkgsFor system) ./fmt.nix;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
|
||||||
clan = clan.config;
|
|
||||||
|
|
||||||
# `nix fmt` and the `nix flake check` formatting gate.
|
flake = {
|
||||||
formatter = forAllSystems (system: (treefmtFor system).config.build.wrapper);
|
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
||||||
checks = forAllSystems (system: {
|
clan = clan.config;
|
||||||
formatting = (treefmtFor system).config.build.check self;
|
};
|
||||||
});
|
|
||||||
|
|
||||||
# Add the Clan cli tool to the dev shell.
|
perSystem =
|
||||||
# Use "nix develop" to enter the dev shell.
|
{ system, ... }:
|
||||||
devShells = forAllSystems (
|
|
||||||
system:
|
|
||||||
let
|
let
|
||||||
|
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
|
||||||
|
treefmtEval = inputs.treefmt-nix.lib.evalModule pkgs ./fmt.nix;
|
||||||
clanCli = clan-core.packages.${system}.clan-cli;
|
clanCli = clan-core.packages.${system}.clan-cli;
|
||||||
# `clan machines update a b c` normally runs machines in parallel,
|
# `clan machines update a b c` normally runs machines in parallel,
|
||||||
# which interleaves their output and buries the YubiKey PIN prompts.
|
# which interleaves their output and buries the YubiKey PIN prompts.
|
||||||
# This wrapper (first in PATH) runs them one at a time instead; any
|
# This wrapper (first in PATH) runs them one at a time instead; any
|
||||||
# flags fall through to the real CLI untouched.
|
# flags fall through to the real CLI untouched.
|
||||||
clanSequential = (pkgsFor system).writeShellScriptBin "clan" ''
|
clanSequential = pkgs.writeShellScriptBin "clan" ''
|
||||||
if [ "$#" -gt 3 ] && [ "$1" = machines ] && [ "$2" = update ]; then
|
if [ "$#" -gt 3 ] && [ "$1" = machines ] && [ "$2" = update ]; then
|
||||||
shift 2
|
shift 2
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
@@ -84,14 +80,19 @@
|
|||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = (pkgsFor system).mkShell {
|
# `nix fmt` and the `nix flake check` formatting gate.
|
||||||
|
formatter = treefmtEval.config.build.wrapper;
|
||||||
|
checks.formatting = treefmtEval.config.build.check self;
|
||||||
|
|
||||||
|
# Add the Clan cli tool to the dev shell.
|
||||||
|
# Use "nix develop" to enter the dev shell.
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
clanSequential
|
clanSequential
|
||||||
clanCli
|
clanCli
|
||||||
(treefmtFor system).config.build.wrapper
|
treefmtEval.config.build.wrapper
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
},
|
},
|
||||||
"web01": {
|
"web01": {
|
||||||
"installedAt": 1781983723
|
"installedAt": 1781983723
|
||||||
|
},
|
||||||
|
"gw-cnx-1": {
|
||||||
|
"installedAt": 1785494267
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,11 @@
|
|||||||
wan.vlanId = null; # this ISP runs PPPoE untagged on the port
|
wan.vlanId = null; # this ISP runs PPPoE untagged on the port
|
||||||
trunkPorts = [
|
trunkPorts = [
|
||||||
"enp2s0"
|
"enp2s0"
|
||||||
"enp3s0"
|
# "enp3s0" # STAGING: serves as the uplink below until cutover
|
||||||
"enp4s0"
|
|
||||||
];
|
];
|
||||||
|
# Dedicated on-site recovery port: untagged mgmt, always available even
|
||||||
|
# if the switch config is broken.
|
||||||
|
accessPorts.enp4s0 = "mgmt";
|
||||||
# Replaces the newedge.house OPNsense box; renumbered to the fleet
|
# Replaces the newedge.house OPNsense box; renumbered to the fleet
|
||||||
# convention (10.1.<vlanId>.0/24, router .1, pool .100-.199). The old
|
# convention (10.1.<vlanId>.0/24, router .1, pool .100-.199). The old
|
||||||
# untagged LAN becomes tagged mgmt — infra switch ports get PVID 10.
|
# untagged LAN becomes tagged mgmt — infra switch ports get PVID 10.
|
||||||
@@ -73,6 +75,17 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# STAGING (remove at cutover, and restore enp3s0 to trunkPorts): 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;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -160,6 +160,19 @@ in
|
|||||||
description = "LAN ports carrying all VLANs tagged (incl. any 10G SFP+ ports).";
|
description = "LAN ports carrying all VLANs tagged (incl. any 10G SFP+ ports).";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
accessPorts = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.str;
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
enp4s0 = "mgmt";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Ports acting as untagged access ports on a single VLAN (port name ->
|
||||||
|
VLAN name). Frames are untagged on the wire; the bridge tags them with
|
||||||
|
the VLAN's PVID. Use for an always-available on-site mgmt port.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
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.";
|
||||||
@@ -172,6 +185,14 @@ in
|
|||||||
assertion = cfg.vlans ? mgmt && cfg.vlans ? lan;
|
assertion = cfg.vlans ? mgmt && cfg.vlans ? lan;
|
||||||
message = "cnx.router: every site must define the `mgmt` and `lan` VLANs.";
|
message = "cnx.router: every site must define the `mgmt` and `lan` VLANs.";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion = lib.all (v: cfg.vlans ? ${v}) (lib.attrValues cfg.accessPorts);
|
||||||
|
message = "cnx.router: every accessPorts value must name a defined VLAN.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = lib.all (p: !(cfg.accessPorts ? ${p})) cfg.trunkPorts;
|
||||||
|
message = "cnx.router: a port cannot be both a trunk and an access port.";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# Router diagnostics toolkit: packets (tcpdump), path (mtr), link
|
# Router diagnostics toolkit: packets (tcpdump), path (mtr), link
|
||||||
@@ -270,6 +291,21 @@ in
|
|||||||
};
|
};
|
||||||
}) cfg.trunkPorts
|
}) cfg.trunkPorts
|
||||||
)
|
)
|
||||||
|
// lib.mapAttrs' (
|
||||||
|
port: vlanName:
|
||||||
|
lib.nameValuePair "25-access-${port}" {
|
||||||
|
matchConfig.Name = port;
|
||||||
|
networkConfig.Bridge = "br0";
|
||||||
|
bridgeVLANs = [
|
||||||
|
{
|
||||||
|
VLAN = cfg.vlans.${vlanName}.id;
|
||||||
|
PVID = cfg.vlans.${vlanName}.id;
|
||||||
|
EgressUntagged = cfg.vlans.${vlanName}.id;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
}
|
||||||
|
) cfg.accessPorts
|
||||||
// lib.mapAttrs' (
|
// lib.mapAttrs' (
|
||||||
name: vlan:
|
name: vlan:
|
||||||
lib.nameValuePair "40-${vlanIf name}" {
|
lib.nameValuePair "40-${vlanIf name}" {
|
||||||
|
|||||||
Reference in New Issue
Block a user