Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a97f3f82e5 | ||
|
|
a46b83cbc7 | ||
|
|
5642a0f6fc | ||
|
|
df389266f6 | ||
|
|
81626a75db | ||
|
|
8171207e9c | ||
|
|
53c03af2a0 | ||
|
|
1a56a2d24b | ||
|
|
e316ec56ce | ||
|
|
559115e52b | ||
|
|
9cfcf91b51 | ||
|
|
570fe64497 | ||
|
|
2ac4215237 | ||
|
|
d51a334ec6 | ||
|
|
b81c0aaa89 | ||
|
|
fcd8e55024 |
@@ -1,3 +1,4 @@
|
|||||||
|
{ inputs, self, ... }:
|
||||||
let
|
let
|
||||||
hosts = import ./modules/hosts.nix;
|
hosts = import ./modules/hosts.nix;
|
||||||
|
|
||||||
@@ -17,83 +18,192 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Ensure this is unique among all clans you want to use.
|
clan = {
|
||||||
meta.name = "cnx-network-clan";
|
# Ensure this is unique among all clans you want to use.
|
||||||
meta.domain = "cnx-network.internal";
|
meta.name = "cnx-network-clan";
|
||||||
|
meta.domain = "cnx-network.internal";
|
||||||
|
|
||||||
inventory.machines = fleet;
|
specialArgs = { inherit inputs self; };
|
||||||
|
|
||||||
inventory.instances = {
|
# Customize nixpkgs
|
||||||
|
# pkgsForSystem =
|
||||||
|
# system:
|
||||||
|
# import nixpkgs {
|
||||||
|
# inherit system;
|
||||||
|
# config = {
|
||||||
|
# allowUnfree = true;
|
||||||
|
# };
|
||||||
|
# overlays = [];
|
||||||
|
# };
|
||||||
|
secrets.age.plugins = [
|
||||||
|
"age-plugin-yubikey"
|
||||||
|
"age-plugin-fido2-hmac"
|
||||||
|
];
|
||||||
|
|
||||||
# Admin SSH keys + root password, split per the clan-core migration off
|
inventory.machines = fleet;
|
||||||
# the deprecated `admin` service (sshd handles keys, users the password).
|
|
||||||
sshd = {
|
inventory.instances = {
|
||||||
roles.server.tags.all = { };
|
|
||||||
roles.server.settings.authorizedKeys = {
|
# Admin SSH keys + root password, split per the clan-core migration off
|
||||||
"berwn" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIENAjhGQGraQoAjJzsomKP8GAmQPeGL1rNRNHgRcLqtT";
|
# the deprecated `admin` service (sshd handles keys, users the password).
|
||||||
"kurogeek" =
|
sshd = {
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcZ/p1Ofa9liwIzPWzNtONhJ7+FUWd2lCz33r81t8+w kurogeek@kurogeek";
|
roles.server.tags.all = { };
|
||||||
|
roles.server.settings.authorizedKeys = {
|
||||||
|
"berwn" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIENAjhGQGraQoAjJzsomKP8GAmQPeGL1rNRNHgRcLqtT";
|
||||||
|
"kurogeek" =
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcZ/p1Ofa9liwIzPWzNtONhJ7+FUWd2lCz33r81t8+w kurogeek@kurogeek";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
gw-router = {
|
||||||
|
module = {
|
||||||
|
name = "gw-router";
|
||||||
|
input = "self";
|
||||||
|
};
|
||||||
|
roles.default.machines."gw-cnx-1" = {
|
||||||
|
settings = {
|
||||||
|
wan = {
|
||||||
|
interface = "enp1s0";
|
||||||
|
vlanId = null;
|
||||||
|
pppInterface = "enp1s0";
|
||||||
|
};
|
||||||
|
vlans = {
|
||||||
|
mgmt = {
|
||||||
|
id = 10;
|
||||||
|
address = "10.1.10.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.10.0/24";
|
||||||
|
dhcp.fixIPs.storinator01 = {
|
||||||
|
hwAddress = "7c:c2:55:e0:d6:40";
|
||||||
|
ipAddress = "10.1.10.53";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
lan = {
|
||||||
|
id = 20;
|
||||||
|
address = "10.1.20.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.20.0/24";
|
||||||
|
};
|
||||||
|
iot = {
|
||||||
|
id = 40;
|
||||||
|
address = "10.1.40.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.40.0/24";
|
||||||
|
};
|
||||||
|
voip = {
|
||||||
|
id = 50;
|
||||||
|
address = "10.1.50.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.50.0/24";
|
||||||
|
};
|
||||||
|
dmz = {
|
||||||
|
id = 60;
|
||||||
|
address = "10.1.60.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.60.0/24";
|
||||||
|
};
|
||||||
|
unit1 = {
|
||||||
|
id = 110;
|
||||||
|
address = "10.1.110.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.110.0/24";
|
||||||
|
};
|
||||||
|
unit2 = {
|
||||||
|
id = 120;
|
||||||
|
address = "10.1.120.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.120.0/24";
|
||||||
|
};
|
||||||
|
unit3 = {
|
||||||
|
id = 130;
|
||||||
|
address = "10.1.130.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.130.0/24";
|
||||||
|
};
|
||||||
|
unit4 = {
|
||||||
|
id = 140;
|
||||||
|
address = "10.1.140.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.140.0/24";
|
||||||
|
};
|
||||||
|
unit5 = {
|
||||||
|
id = 150;
|
||||||
|
address = "10.1.150.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
subnet = "10.1.150.0/24";
|
||||||
|
dhcp.fixIPs.newt = {
|
||||||
|
hwAddress = "7c:d3:0a:21:58:0b";
|
||||||
|
ipAddress = "10.1.150.22";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
trunkPorts = [ "enp2s0" ];
|
||||||
|
accessPorts = {
|
||||||
|
enp4s0.vlanId = 10;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
root-user = {
|
||||||
|
module = {
|
||||||
|
name = "users";
|
||||||
|
input = "clan-core";
|
||||||
|
};
|
||||||
|
roles.default.tags.all = { };
|
||||||
|
roles.default.settings = {
|
||||||
|
user = "root";
|
||||||
|
prompt = false; # auto-generate, like the old admin service
|
||||||
|
share = false; # per-machine password, not fleet-wide
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
zerotier = {
|
||||||
|
roles.controller.machines."control" = { };
|
||||||
|
roles.peer.tags.all = { };
|
||||||
|
# External members admitted by ZeroTier node id (stable per device).
|
||||||
|
# Inventory machines are auto-accepted; this is only for peers outside the
|
||||||
|
# clan. Node id comes from `zerotier-cli info` on the joining device.
|
||||||
|
roles.controller.settings.allowedIds = [
|
||||||
|
"8802c8d7e0" # alex-nixos
|
||||||
|
"2bd36db8cc" # kurogeek-thinkpad
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
tor = {
|
||||||
|
roles.server.tags.nixos = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
# Direct SSH to public IPs — clan's priority-1 connection path, with the
|
||||||
|
# ZeroTier mesh and Tor kept as automatic fallbacks. Raw IPs (from
|
||||||
|
# modules/hosts.nix, not the ns1/ns2 DNS names) so reaching these hosts never
|
||||||
|
# depends on their own DNS being up.
|
||||||
|
internet.roles.default.machines = builtins.mapAttrs (_: h: {
|
||||||
|
settings.host = h.ipv4;
|
||||||
|
}) hosts;
|
||||||
|
|
||||||
|
# Recovery root password for console access when a machine fails to boot.
|
||||||
|
emergency-access = {
|
||||||
|
roles.default.tags.nixos = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
# Encrypted, deduplicating backups. control hosts the repos; ns1 is the
|
||||||
|
# only client, backing up its declared clan.core.state (the Knot DNSSEC
|
||||||
|
# keystore) over the mesh. Repo lives at /var/lib/borgbackup/ns1 on control.
|
||||||
|
# Cross-host so an ns1 loss is recoverable; repokey encryption means control
|
||||||
|
# never holds plaintext. Run `clan vars generate ns1` (YubiKey) before deploy.
|
||||||
|
borgbackup = {
|
||||||
|
roles.server.machines.control = { };
|
||||||
|
roles.client.machines.ns1 = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
root-user = {
|
# Fleet-wide baseline applied to every machine. Secrets minted by
|
||||||
module = {
|
# `clan vars generate` are encrypted for the admins group from the very
|
||||||
name = "users";
|
# first run — generating before this took effect is what forced the
|
||||||
input = "clan-core";
|
# re-encryption dance (`clan vars fix`) on gw-cnx-1.
|
||||||
};
|
machines = builtins.mapAttrs (_: _: {
|
||||||
roles.default.tags.all = { };
|
clan.core.sops.defaultGroups = [ "admins" ];
|
||||||
roles.default.settings = {
|
}) fleet;
|
||||||
user = "root";
|
|
||||||
prompt = false; # auto-generate, like the old admin service
|
|
||||||
share = false; # per-machine password, not fleet-wide
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
zerotier = {
|
|
||||||
roles.controller.machines."control" = { };
|
|
||||||
roles.peer.tags.all = { };
|
|
||||||
# External members admitted by ZeroTier node id (stable per device).
|
|
||||||
# Inventory machines are auto-accepted; this is only for peers outside the
|
|
||||||
# clan. Node id comes from `zerotier-cli info` on the joining device.
|
|
||||||
roles.controller.settings.allowedIds = [
|
|
||||||
"8802c8d7e0" # alex-nixos
|
|
||||||
"2bd36db8cc" # kurogeek-thinkpad
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
tor = {
|
|
||||||
roles.server.tags.nixos = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
# Direct SSH to public IPs — clan's priority-1 connection path, with the
|
|
||||||
# ZeroTier mesh and Tor kept as automatic fallbacks. Raw IPs (from
|
|
||||||
# modules/hosts.nix, not the ns1/ns2 DNS names) so reaching these hosts never
|
|
||||||
# depends on their own DNS being up.
|
|
||||||
internet.roles.default.machines = builtins.mapAttrs (_: h: {
|
|
||||||
settings.host = h.ipv4;
|
|
||||||
}) hosts;
|
|
||||||
|
|
||||||
# Recovery root password for console access when a machine fails to boot.
|
|
||||||
emergency-access = {
|
|
||||||
roles.default.tags.nixos = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
# Encrypted, deduplicating backups. control hosts the repos; ns1 is the
|
|
||||||
# only client, backing up its declared clan.core.state (the Knot DNSSEC
|
|
||||||
# keystore) over the mesh. Repo lives at /var/lib/borgbackup/ns1 on control.
|
|
||||||
# Cross-host so an ns1 loss is recoverable; repokey encryption means control
|
|
||||||
# never holds plaintext. Run `clan vars generate ns1` (YubiKey) before deploy.
|
|
||||||
borgbackup = {
|
|
||||||
roles.server.machines.control = { };
|
|
||||||
roles.client.machines.ns1 = { };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Fleet-wide baseline applied to every machine. Secrets minted by
|
|
||||||
# `clan vars generate` are encrypted for the admins group from the very
|
|
||||||
# first run — generating before this took effect is what forced the
|
|
||||||
# re-encryption dance (`clan vars fix`) on gw-cnx-1.
|
|
||||||
machines = builtins.mapAttrs (_: _: {
|
|
||||||
clan.core.sops.defaultGroups = [ "admins" ];
|
|
||||||
}) fleet;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,67 +7,43 @@
|
|||||||
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,
|
flake-parts,
|
||||||
...
|
...
|
||||||
}@inputs:
|
}:
|
||||||
let
|
let
|
||||||
# Usage see: https://docs.clan.lol
|
in
|
||||||
clan = clan-core.lib.clan {
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
inherit self;
|
|
||||||
imports = [ ./clan.nix ];
|
|
||||||
specialArgs = { inherit inputs; };
|
|
||||||
|
|
||||||
# Customize nixpkgs
|
|
||||||
# pkgsForSystem =
|
|
||||||
# system:
|
|
||||||
# import nixpkgs {
|
|
||||||
# inherit system;
|
|
||||||
# config = {
|
|
||||||
# allowUnfree = true;
|
|
||||||
# };
|
|
||||||
# overlays = [];
|
|
||||||
# };
|
|
||||||
secrets.age.plugins = [
|
|
||||||
"age-plugin-yubikey"
|
|
||||||
"age-plugin-fido2-hmac"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
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.
|
imports = [
|
||||||
formatter = forAllSystems (system: (treefmtFor system).config.build.wrapper);
|
inputs.clan-core.flakeModules.default
|
||||||
checks = forAllSystems (system: {
|
|
||||||
formatting = (treefmtFor system).config.build.check self;
|
|
||||||
});
|
|
||||||
|
|
||||||
# Add the Clan cli tool to the dev shell.
|
./clan.nix
|
||||||
# Use "nix develop" to enter the dev shell.
|
./modules/clan/flake-module.nix
|
||||||
devShells = forAllSystems (
|
];
|
||||||
system:
|
|
||||||
|
perSystem =
|
||||||
|
{ 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 +60,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../../modules/router
|
# ../../modules/router
|
||||||
../../modules/monitoring/exporters.nix
|
../../modules/monitoring/exporters.nix
|
||||||
(import ../../modules/dns/acme-gw-secret.nix "gw-cnx-1")
|
(import ../../modules/dns/acme-gw-secret.nix "gw-cnx-1")
|
||||||
];
|
];
|
||||||
@@ -20,57 +20,70 @@
|
|||||||
builtins.hashString "sha256" config.networking.hostName
|
builtins.hashString "sha256" config.networking.hostName
|
||||||
);
|
);
|
||||||
|
|
||||||
cnx.router = {
|
# cnx.router = {
|
||||||
enable = true;
|
# enable = true;
|
||||||
site = "cnx";
|
# site = "cnx";
|
||||||
siteId = 1;
|
# siteId = 1;
|
||||||
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
|
||||||
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
|
||||||
# Replaces the newedge.house OPNsense box; renumbered to the fleet
|
# # if the switch config is broken.
|
||||||
# convention (10.1.<vlanId>.0/24, router .1, pool .100-.199). The old
|
# accessPorts.enp4s0 = "mgmt";
|
||||||
# untagged LAN becomes tagged mgmt — infra switch ports get PVID 10.
|
# # Replaces the newedge.house OPNsense box; renumbered to the fleet
|
||||||
vlans = {
|
# # convention (10.1.<vlanId>.0/24, router .1, pool .100-.199). The old
|
||||||
mgmt = {
|
# # untagged LAN becomes tagged mgmt — infra switch ports get PVID 10.
|
||||||
id = 10; # 10.1.10.0/24 — servers, APs, switches, Omada, admin
|
# vlans = {
|
||||||
dhcp.reservations.storinator01 = {
|
# mgmt = {
|
||||||
hwAddress = "7c:c2:55:e0:d6:40";
|
# id = 10; # 10.1.10.0/24 — servers, APs, switches, Omada, admin
|
||||||
ipAddress = "10.1.10.53";
|
# dhcp.reservations.storinator01 = {
|
||||||
};
|
# hwAddress = "7c:c2:55:e0:d6:40";
|
||||||
};
|
# ipAddress = "10.1.10.53";
|
||||||
lan.id = 20; # 10.1.20.0/24 — trusted clients
|
# };
|
||||||
iot.id = 40; # 10.1.40.0/24
|
# };
|
||||||
voip.id = 50; # 10.1.50.0/24
|
# lan.id = 20; # 10.1.20.0/24 — trusted clients
|
||||||
dmz.id = 60; # 10.1.60.0/24
|
# iot.id = 40; # 10.1.40.0/24
|
||||||
unit1.id = 110; # 10.1.110.0/24
|
# voip.id = 50; # 10.1.50.0/24
|
||||||
unit2.id = 120; # 10.1.120.0/24
|
# dmz.id = 60; # 10.1.60.0/24
|
||||||
unit3.id = 130; # 10.1.130.0/24
|
# unit1.id = 110; # 10.1.110.0/24
|
||||||
unit4.id = 140; # 10.1.140.0/24
|
# unit2.id = 120; # 10.1.120.0/24
|
||||||
unit5 = {
|
# unit3.id = 130; # 10.1.130.0/24
|
||||||
id = 150; # 10.1.150.0/24
|
# unit4.id = 140; # 10.1.140.0/24
|
||||||
dhcp.reservations.newt = {
|
# unit5 = {
|
||||||
hwAddress = "7c:d3:0a:21:58:0b";
|
# id = 150; # 10.1.150.0/24
|
||||||
ipAddress = "10.1.150.22";
|
# dhcp.reservations.newt = {
|
||||||
};
|
# hwAddress = "7c:d3:0a:21:58:0b";
|
||||||
};
|
# ipAddress = "10.1.150.22";
|
||||||
};
|
# };
|
||||||
# This site runs the Omada controller for its APs/switches.
|
# };
|
||||||
omada.enable = true;
|
# };
|
||||||
|
# # This site runs the Omada controller for its APs/switches.
|
||||||
|
# omada.enable = true;
|
||||||
|
#
|
||||||
|
# # Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky
|
||||||
|
# # resolves the names to the router's LAN address for mgmt+lan clients.
|
||||||
|
# proxy = {
|
||||||
|
# enable = true;
|
||||||
|
# services.omada = {
|
||||||
|
# # Omada's UI is HTTPS with a self-signed cert on the host network.
|
||||||
|
# backend = "https://127.0.0.1:8043";
|
||||||
|
# insecureSkipVerify = true;
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
|
||||||
# Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky
|
# STAGING (remove at cutover, and restore enp3s0 to trunkPorts): DHCP-client
|
||||||
# resolves the names to the router's LAN address for mgmt+lan clients.
|
# uplink into the existing OPNsense LAN so the box has internet + mesh while
|
||||||
proxy = {
|
# it runs alongside the old router. Default-deny firewall on this interface
|
||||||
enable = true;
|
# (it's in no VLAN zone); PPPoE simply retries until the WAN port is cabled.
|
||||||
services.omada = {
|
# Do NOT connect the trunk ports to the production switch while staging —
|
||||||
# Omada's UI is HTTPS with a self-signed cert on the host network.
|
# Kea on tag 10 would fight the OPNsense LAN DHCP in one broadcast domain.
|
||||||
backend = "https://127.0.0.1:8043";
|
systemd.network.networks."05-staging" = {
|
||||||
insecureSkipVerify = true;
|
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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
inputs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports =
|
||||||
|
let
|
||||||
|
# Get all subdirectories in the current directory
|
||||||
|
dirContents = builtins.readDir ./.;
|
||||||
|
|
||||||
|
# Filter to include only directories that have a flake-module.nix file
|
||||||
|
# and exclude special directories like 'result'
|
||||||
|
validModuleDirs = builtins.filter (
|
||||||
|
name:
|
||||||
|
name != "result"
|
||||||
|
&& dirContents.${name} == "directory"
|
||||||
|
&& builtins.pathExists (./. + "/${name}/flake-module.nix")
|
||||||
|
) (builtins.attrNames dirContents);
|
||||||
|
|
||||||
|
# Create import paths for each valid directory
|
||||||
|
imports = (map (name: ./. + "/${name}/flake-module.nix") validModuleDirs) ++ [
|
||||||
|
inputs.clan-core.flakeModules.testModule
|
||||||
|
];
|
||||||
|
in
|
||||||
|
imports;
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
_class = "clan.service";
|
||||||
|
manifest.name = "gw-router";
|
||||||
|
manifest.description = "A gateway router service to configure most of a router features";
|
||||||
|
manifest.readme = "A gateway router service to configure most of a router features";
|
||||||
|
manifest.categories = [ "System" ];
|
||||||
|
|
||||||
|
roles.default = {
|
||||||
|
description = "Site gateway router role";
|
||||||
|
|
||||||
|
interface =
|
||||||
|
{ lib, config, ... }:
|
||||||
|
let
|
||||||
|
vlanModule =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
id = lib.mkOption {
|
||||||
|
type = lib.types.ints.between 1 4094;
|
||||||
|
description = "802.1Q VLAN id.";
|
||||||
|
};
|
||||||
|
address = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "10.0.10.1";
|
||||||
|
description = "Router address on this VLAN.";
|
||||||
|
};
|
||||||
|
prefixLength = lib.mkOption {
|
||||||
|
type = lib.types.ints.between 8 30;
|
||||||
|
default = 24;
|
||||||
|
};
|
||||||
|
subnet = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "10.0.10.0/24";
|
||||||
|
description = "The VLAN's network in CIDR form (must contain `address`).";
|
||||||
|
};
|
||||||
|
dhcp = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
pool = {
|
||||||
|
from = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "10.0.10.100";
|
||||||
|
};
|
||||||
|
to = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "10.0.10.199";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
leaseTime = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 86400;
|
||||||
|
description = ''
|
||||||
|
Lease validity in seconds. Lower it for high-churn networks,
|
||||||
|
e.g. public-WiFi guest VLANs (3600-7200), so the pool recycles.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
fixedIPs = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
hwAddress = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "aa:bb:cc:dd:ee:ff";
|
||||||
|
description = "Client MAC address.";
|
||||||
|
};
|
||||||
|
ipAddress = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Fixed address handed to this client (inside the VLAN's subnet, outside the pool).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
description = "Static DHCP leases; the attribute name becomes the client's hostname.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
allowWAN = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether clients on this VLAN may reach the internet.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
wan = {
|
||||||
|
interface = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Physical WAN port the PPPoE session runs on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
vlanId = lib.mkOption {
|
||||||
|
type = lib.types.nullOr (lib.types.ints.between 1 4094);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
802.1Q tag the ISP requires for the PPPoE session (AIS Thailand: 10);
|
||||||
|
null for untagged PPPoE directly on the port. Unrelated to the LAN
|
||||||
|
VLANs — this tag exists only on the WAN port.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
macAddress = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "aa:bb:cc:dd:ee:ff";
|
||||||
|
description = ''
|
||||||
|
Spoofed MAC for the WAN port, e.g. to keep the MAC the ISP has
|
||||||
|
pinned (cloned from the old router). null keeps the hardware MAC.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
pppInterface = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
internal = true;
|
||||||
|
readOnly = true;
|
||||||
|
default = if config.wan.vlanId == null then config.wan.interface else "wan-vlan";
|
||||||
|
description = "Interface pppd dials on (the WAN port or its ISP VLAN).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
vlans = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.submodule vlanModule);
|
||||||
|
description = "VLANs setup for this router";
|
||||||
|
};
|
||||||
|
|
||||||
|
trunkPorts = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
description = "LAN ports carrying all VLANs tagged (incl. any 10G SFP+ ports).";
|
||||||
|
};
|
||||||
|
|
||||||
|
accessPorts = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
vlanId = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
description = "Untagged traffic in from the device gets tagged VLANs inside the bridge, and VLANs traffic going back out to the device gets untagged, so the device itself never has to know VLANs exist.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
description = ''
|
||||||
|
Ports acting as untagged access ports on a single VLAN (port name ->
|
||||||
|
VLAN id). Frames are untagged on the wire; the bridge tags them with
|
||||||
|
the VLAN's PVID. Use for an always-available on-site mgmt port.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
perInstance =
|
||||||
|
{ settings, ... }:
|
||||||
|
{
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
vlanIf = name: "vlan=${name}";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
networking.useNetworkd = true;
|
||||||
|
networking.useDHCP = false;
|
||||||
|
systemd.network.enable = true;
|
||||||
|
|
||||||
|
systemd.network.netdevs = {
|
||||||
|
"20-br0" = {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = "br0";
|
||||||
|
Kind = "bridge";
|
||||||
|
};
|
||||||
|
bridgeConfig = {
|
||||||
|
VLANFiltering = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (settings.wan.vlanId != null) {
|
||||||
|
"15-wan-lan" = {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = "wan-vlan";
|
||||||
|
Kind = "vlan";
|
||||||
|
};
|
||||||
|
vlanConfig.Id = settings.wan.vlanId;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.mapAttrs' (
|
||||||
|
name: vlan:
|
||||||
|
lib.nameValuePair "30-${vlanIf name}" {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = vlanIf name;
|
||||||
|
Kind = "vlan";
|
||||||
|
};
|
||||||
|
vlanConfig.Id = vlan.id;
|
||||||
|
}
|
||||||
|
) settings.vlans;
|
||||||
|
|
||||||
|
systemd.network.networks =
|
||||||
|
let
|
||||||
|
allVLANs = lib.mapAttrsToList (_: vlan: { VLAN = vlan.id; }) settings.vlans;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
"10-wan" = {
|
||||||
|
matchConfig.Name = settings.wan.interface;
|
||||||
|
networkConfig.LinkLocalAddressing = "no";
|
||||||
|
vlan = lib.optional (settings.wan.vlanId != null) "wan-wlan";
|
||||||
|
linkConfig = {
|
||||||
|
RequiredForOnline = "carrier";
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (settings.wan.macAddress != null) {
|
||||||
|
MACAddress = settings.wan.macAddress;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (settings.wan.vlanId != null) {
|
||||||
|
"15-wan-lan" = {
|
||||||
|
matchConfig.Name = "wan-vlan";
|
||||||
|
networkConfig.LinkLocalAddressing = "no";
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
"20-br0" = {
|
||||||
|
matchConfig.Name = "br0";
|
||||||
|
networkConfig.LinkLocalAddressing = "no";
|
||||||
|
vlan = lib.mapAttrsToList (name: _: vlanIf name) settings.vlans;
|
||||||
|
bridgeVLANs = allVLANs;
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.listToAttrs (
|
||||||
|
map (port: {
|
||||||
|
name = "25-trunk-${port}";
|
||||||
|
value = {
|
||||||
|
matchConfig.Name = port;
|
||||||
|
networkConfig.Bridge = "br0";
|
||||||
|
bridgeVLANs = allVLANs;
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
}) settings.trunkPorts
|
||||||
|
)
|
||||||
|
// lib.mapAttrs' (
|
||||||
|
iface: port:
|
||||||
|
lib.nameValuePair "20-access-${iface}" {
|
||||||
|
matchConfig.Name = port;
|
||||||
|
networkConfig.Bridge = "br0";
|
||||||
|
bridgeVLANs = [
|
||||||
|
{
|
||||||
|
VLAN = port.vlanId;
|
||||||
|
PVID = port.vlanId;
|
||||||
|
EgressUntagged = port.vlanId;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
}
|
||||||
|
) settings.accessPorts
|
||||||
|
// lib.mapAttrs' (
|
||||||
|
name: vlan:
|
||||||
|
lib.nameValuePair "40-${vlanIf name}" {
|
||||||
|
matchConfig.Name = vlanIf name;
|
||||||
|
address = [ "${vlan.address}/${toString vlan.prefixLength}" ];
|
||||||
|
networkConfig = {
|
||||||
|
IPv6AcceptRA = false;
|
||||||
|
IPv6SendRA = true;
|
||||||
|
DHCPPrefixDelegation = true;
|
||||||
|
};
|
||||||
|
dhcpPrefixDelegationConfig.SubnetId = "auto";
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
}
|
||||||
|
) settings.vlans;
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
tcpdump
|
||||||
|
mtr
|
||||||
|
ethtool
|
||||||
|
conntrack-tools
|
||||||
|
knot-dns
|
||||||
|
iftop
|
||||||
|
librespeed-cli
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = [ ./pppoe.nix ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{ self, inputs, ... }:
|
||||||
|
let
|
||||||
|
module = ./default.nix;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
flake.clan.modules = {
|
||||||
|
gw-router = module;
|
||||||
|
};
|
||||||
|
# perSystem =
|
||||||
|
# { ... }:
|
||||||
|
# {
|
||||||
|
# clan.nixosTests.service-headplane = {
|
||||||
|
# imports = [ ./tests/vm/default.nix ];
|
||||||
|
# _module.args = { inherit self inputs; };
|
||||||
|
#
|
||||||
|
# clan.modules."@clan/headplane" = module;
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{ config, ... }:
|
||||||
|
let
|
||||||
|
creds = config.clan.core.vars.generators.gw-router;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
clan.core.vars.generators.gw-router = {
|
||||||
|
prompts.pppoe-username = {
|
||||||
|
description = "PPPoE username (from the ISP)";
|
||||||
|
type = "hidden";
|
||||||
|
};
|
||||||
|
prompts.pppoe-password = {
|
||||||
|
description = "PPPoE password (from the ISP)";
|
||||||
|
type = "hidden";
|
||||||
|
};
|
||||||
|
files."pppoe-username".secret = true;
|
||||||
|
files."pppoe-password".secret = true;
|
||||||
|
script = ''
|
||||||
|
user="$(cat "$prompts"/pppoe-username)"
|
||||||
|
pass="$(cat "$prompts"/pppoe-password)"
|
||||||
|
printf 'user "%s"\n' "$user" > "$out"/pppoe-username
|
||||||
|
printf '"%s" * "%s"\n' "$user" "$pass" > "$out"/pppoe-password
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.pppd = {
|
||||||
|
enable = true;
|
||||||
|
peers.wan = {
|
||||||
|
autostart = true;
|
||||||
|
config = ''
|
||||||
|
plugin pppoe.so ${settings.wan.pppInterface}
|
||||||
|
ifname ppp0
|
||||||
|
file ${creds.files."pppoe-username".path}
|
||||||
|
noipdefault
|
||||||
|
defaultroute
|
||||||
|
noauth
|
||||||
|
hide-password
|
||||||
|
persist
|
||||||
|
maxfail 0
|
||||||
|
holdoff 5
|
||||||
|
lcp-echo-interval 15
|
||||||
|
lcp-echo-failure 3
|
||||||
|
+ipv6
|
||||||
|
mtu 1492
|
||||||
|
mru 1492
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.etc."ppp/chap-secrets".source = creds.files."pppoe-password".path;
|
||||||
|
environment.etc."ppp/pap-secrets".source = creds.files."pppoe-password".path;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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