Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0c4388238 |
@@ -1,3 +1,10 @@
|
|||||||
|
{
|
||||||
|
inputs,
|
||||||
|
self,
|
||||||
|
libNet,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
hosts = import ./modules/hosts.nix;
|
hosts = import ./modules/hosts.nix;
|
||||||
|
|
||||||
@@ -17,10 +24,28 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
clan = {
|
||||||
# Ensure this is unique among all clans you want to use.
|
# Ensure this is unique among all clans you want to use.
|
||||||
meta.name = "cnx-network-clan";
|
meta.name = "cnx-network-clan";
|
||||||
meta.domain = "cnx-network.internal";
|
meta.domain = "cnx-network.internal";
|
||||||
|
|
||||||
|
specialArgs = { inherit inputs self; };
|
||||||
|
|
||||||
|
# Customize nixpkgs
|
||||||
|
# pkgsForSystem =
|
||||||
|
# system:
|
||||||
|
# import nixpkgs {
|
||||||
|
# inherit system;
|
||||||
|
# config = {
|
||||||
|
# allowUnfree = true;
|
||||||
|
# };
|
||||||
|
# overlays = [];
|
||||||
|
# };
|
||||||
|
secrets.age.plugins = [
|
||||||
|
"age-plugin-yubikey"
|
||||||
|
"age-plugin-fido2-hmac"
|
||||||
|
];
|
||||||
|
|
||||||
inventory.machines = fleet;
|
inventory.machines = fleet;
|
||||||
|
|
||||||
inventory.instances = {
|
inventory.instances = {
|
||||||
@@ -36,6 +61,70 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
gw-router = {
|
||||||
|
module = {
|
||||||
|
name = "gw-router";
|
||||||
|
input = "self";
|
||||||
|
};
|
||||||
|
roles.default.machines."gw-cnx-1" =
|
||||||
|
let
|
||||||
|
genVlan = id: length: rec {
|
||||||
|
inherit id;
|
||||||
|
prefixLength = length;
|
||||||
|
subnet = libNet.net.cidr.make length "10.0.${lib.toString id}.0";
|
||||||
|
address = libNet.net.cidr.host 1 subnet;
|
||||||
|
dhcp.pool.from = libNet.net.cidr.host 100 subnet;
|
||||||
|
dhcp.pool.to = libNet.net.cidr.host 200 subnet;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
settings = {
|
||||||
|
wan = {
|
||||||
|
interface = "enp1s0";
|
||||||
|
vlanId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
vlans = {
|
||||||
|
mgmt = lib.recursiveUpdate (genVlan 10 24) {
|
||||||
|
dhcp.fixedIPs.storinator01 = {
|
||||||
|
hwAddress = "7c:c2:55:e0:d6:40";
|
||||||
|
ipAddress = "10.1.10.53";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
lan = genVlan 20 24;
|
||||||
|
iot = genVlan 40 24;
|
||||||
|
voip = genVlan 50 24;
|
||||||
|
dmz = genVlan 60 24;
|
||||||
|
unit1 = genVlan 110 24;
|
||||||
|
unit2 = genVlan 120 24;
|
||||||
|
unit3 = genVlan 130 24;
|
||||||
|
unit4 = genVlan 140 24;
|
||||||
|
unit5 = lib.recursiveUpdate (genVlan 150 24) {
|
||||||
|
dhcp.fixedIPs.newt = {
|
||||||
|
hwAddress = "7c:d3:0a:21:58:0b";
|
||||||
|
ipAddress = "10.1.150.22";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
trunkPorts = [ "enp2s0" ];
|
||||||
|
|
||||||
|
accessPorts = {
|
||||||
|
enp4s0.vlanId = 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
upLinkPorts = [ "enp3s0" ];
|
||||||
|
|
||||||
|
enableOmada = true;
|
||||||
|
|
||||||
|
speedtest = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
root-user = {
|
root-user = {
|
||||||
module = {
|
module = {
|
||||||
name = "users";
|
name = "users";
|
||||||
@@ -96,4 +185,5 @@ in
|
|||||||
machines = builtins.mapAttrs (_: _: {
|
machines = builtins.mapAttrs (_: _: {
|
||||||
clan.core.sops.defaultGroups = [ "admins" ];
|
clan.core.sops.defaultGroups = [ "admins" ];
|
||||||
}) fleet;
|
}) fleet;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+32
@@ -173,6 +173,19 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lib-net": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1596309860,
|
||||||
|
"narHash": "sha256-izAzepR/6cDvnRfaa2ceSolMLMwqzQB5x9q62aR5J2g=",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba/archive/3885f7cd9ed0a746a9d675da6f265d41e9fd6704.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba/archive/3885f7cd9ed0a746a9d675da6f265d41e9fd6704.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nix-darwin": {
|
"nix-darwin": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -194,6 +207,24 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nix-lib-net": {
|
||||||
|
"inputs": {
|
||||||
|
"lib-net": "lib-net"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1732312010,
|
||||||
|
"narHash": "sha256-kQuiPSq3Iz/t5RC+QqwHfvj4hKQesm/WGffcQlhhCVE=",
|
||||||
|
"owner": "reo101",
|
||||||
|
"repo": "nix-lib-net",
|
||||||
|
"rev": "0f3412ccb56d783b974341f5f83abfaed7aa8b5b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "reo101",
|
||||||
|
"repo": "nix-lib-net",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nix-select": {
|
"nix-select": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763303120,
|
"lastModified": 1763303120,
|
||||||
@@ -251,6 +282,7 @@
|
|||||||
"clan-core",
|
"clan-core",
|
||||||
"flake-parts"
|
"flake-parts"
|
||||||
],
|
],
|
||||||
|
"nix-lib-net": "nix-lib-net",
|
||||||
"nixos-mailserver": "nixos-mailserver",
|
"nixos-mailserver": "nixos-mailserver",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"clan-core",
|
"clan-core",
|
||||||
|
|||||||
@@ -8,37 +8,16 @@
|
|||||||
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
inputs.flake-parts.follows = "clan-core/flake-parts";
|
inputs.flake-parts.follows = "clan-core/flake-parts";
|
||||||
|
inputs.nix-lib-net.url = "github:reo101/nix-lib-net";
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
inputs@{
|
inputs@{
|
||||||
self,
|
self,
|
||||||
clan-core,
|
clan-core,
|
||||||
nixpkgs,
|
|
||||||
flake-parts,
|
flake-parts,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# Usage see: https://docs.clan.lol
|
|
||||||
clan = clan-core.lib.clan {
|
|
||||||
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"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
systems = [
|
systems = [
|
||||||
@@ -48,10 +27,14 @@
|
|||||||
"x86_64-darwin"
|
"x86_64-darwin"
|
||||||
];
|
];
|
||||||
|
|
||||||
flake = {
|
imports = [
|
||||||
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
inputs.clan-core.flakeModules.default
|
||||||
clan = clan.config;
|
|
||||||
};
|
./clan.nix
|
||||||
|
./modules/clan/flake-module.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
_module.args.libNet = inputs.nixpkgs.lib.extend inputs.nix-lib-net.overlays.raw;
|
||||||
|
|
||||||
perSystem =
|
perSystem =
|
||||||
{ system, ... }:
|
{ system, ... }:
|
||||||
|
|||||||
@@ -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,61 +20,60 @@
|
|||||||
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
|
||||||
wan.macAddress = "a8:b8:e0:01:06:87";
|
# trunkPorts = [
|
||||||
trunkPorts = [
|
# "enp2s0"
|
||||||
"enp2s0"
|
# # "enp3s0" # STAGING: serves as the uplink below until cutover
|
||||||
# "enp3s0" # STAGING: serves as the uplink below until cutover
|
# ];
|
||||||
];
|
# # 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";
|
# # 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.
|
# vlans = {
|
||||||
vlans = {
|
# mgmt = {
|
||||||
mgmt = {
|
# id = 10; # 10.1.10.0/24 — servers, APs, switches, Omada, admin
|
||||||
id = 10; # 10.1.10.0/24 — servers, APs, switches, Omada, admin
|
# dhcp.reservations.storinator01 = {
|
||||||
dhcp.reservations.storinator01 = {
|
# hwAddress = "7c:c2:55:e0:d6:40";
|
||||||
hwAddress = "7c:c2:55:e0:d6:40";
|
# ipAddress = "10.1.10.53";
|
||||||
ipAddress = "10.1.10.53";
|
# };
|
||||||
};
|
# };
|
||||||
};
|
# lan.id = 20; # 10.1.20.0/24 — trusted clients
|
||||||
lan.id = 20; # 10.1.20.0/24 — trusted clients
|
# iot.id = 40; # 10.1.40.0/24
|
||||||
iot.id = 40; # 10.1.40.0/24
|
# voip.id = 50; # 10.1.50.0/24
|
||||||
voip.id = 50; # 10.1.50.0/24
|
# dmz.id = 60; # 10.1.60.0/24
|
||||||
dmz.id = 60; # 10.1.60.0/24
|
# unit1.id = 110; # 10.1.110.0/24
|
||||||
unit1.id = 110; # 10.1.110.0/24
|
# unit2.id = 120; # 10.1.120.0/24
|
||||||
unit2.id = 120; # 10.1.120.0/24
|
# unit3.id = 130; # 10.1.130.0/24
|
||||||
unit3.id = 130; # 10.1.130.0/24
|
# unit4.id = 140; # 10.1.140.0/24
|
||||||
unit4.id = 140; # 10.1.140.0/24
|
# unit5 = {
|
||||||
unit5 = {
|
# id = 150; # 10.1.150.0/24
|
||||||
id = 150; # 10.1.150.0/24
|
# dhcp.reservations.newt = {
|
||||||
dhcp.reservations.newt = {
|
# hwAddress = "7c:d3:0a:21:58:0b";
|
||||||
hwAddress = "7c:d3:0a:21:58:0b";
|
# ipAddress = "10.1.150.22";
|
||||||
ipAddress = "10.1.150.22";
|
# };
|
||||||
};
|
# };
|
||||||
};
|
# };
|
||||||
};
|
# # This site runs the Omada controller for its APs/switches.
|
||||||
# This site runs the Omada controller for its APs/switches.
|
# omada.enable = true;
|
||||||
omada.enable = true;
|
#
|
||||||
|
# # Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky
|
||||||
# Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky
|
# # resolves the names to the router's LAN address for mgmt+lan clients.
|
||||||
# resolves the names to the router's LAN address for mgmt+lan clients.
|
# proxy = {
|
||||||
proxy = {
|
# enable = true;
|
||||||
enable = true;
|
# services.omada = {
|
||||||
services.omada = {
|
# # Omada's UI is HTTPS with a self-signed cert on the host network.
|
||||||
# Omada's UI is HTTPS with a self-signed cert on the host network.
|
# backend = "https://127.0.0.1:8043";
|
||||||
backend = "https://127.0.0.1:8043";
|
# insecureSkipVerify = true;
|
||||||
insecureSkipVerify = true;
|
# };
|
||||||
};
|
# };
|
||||||
};
|
# };
|
||||||
};
|
|
||||||
|
|
||||||
# STAGING (remove at cutover, and restore enp3s0 to trunkPorts): DHCP-client
|
# STAGING (remove at cutover, and restore enp3s0 to trunkPorts): DHCP-client
|
||||||
# uplink into the existing OPNsense LAN so the box has internet + mesh while
|
# uplink into the existing OPNsense LAN so the box has internet + mesh while
|
||||||
@@ -82,10 +81,10 @@
|
|||||||
# (it's in no VLAN zone); PPPoE simply retries until the WAN port is cabled.
|
# (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 —
|
# 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.
|
# Kea on tag 10 would fight the OPNsense LAN DHCP in one broadcast domain.
|
||||||
systemd.network.networks."05-staging" = {
|
# systemd.network.networks."05-staging" = {
|
||||||
matchConfig.Name = "enp3s0";
|
# matchConfig.Name = "enp3s0";
|
||||||
networkConfig.DHCP = "ipv4";
|
# 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;
|
||||||
|
|||||||
@@ -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,59 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
|
||||||
|
dir = config.clan.core.settings.directory;
|
||||||
|
instance = "zerotier";
|
||||||
|
|
||||||
|
networkId = builtins.readFile "${dir}/vars/shared/zerotier-network-${instance}/network-id/value";
|
||||||
|
full = "fd" + networkId + "9993";
|
||||||
|
hextet = i: builtins.substring (i * 4) 4 full;
|
||||||
|
subnetZtier = "${hextet 0}:${hextet 1}:${hextet 2}:${hextet 3}:${hextet 4}:${builtins.substring 20 2 full}00::/88";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
services.crowdsec = {
|
||||||
|
enable = true;
|
||||||
|
autoUpdateService = true;
|
||||||
|
hub.collections = [
|
||||||
|
"crowdsecurity/linux"
|
||||||
|
"crowdsecurity/sshd"
|
||||||
|
];
|
||||||
|
localConfig = {
|
||||||
|
acquisitions = [
|
||||||
|
{
|
||||||
|
source = "journalctl";
|
||||||
|
journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ];
|
||||||
|
labels.type = "syslog";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
# Never ban the ZeroTier mesh — it is the only admin path to these
|
||||||
|
# boxes (no public SSH), so a false positive would lock us out.
|
||||||
|
# Parser-stage whitelist: mesh events are dropped before any scenario.
|
||||||
|
parsers.s02Enrich = [
|
||||||
|
{
|
||||||
|
name = "cnx/mesh-whitelist";
|
||||||
|
description = "Whitelist the ZeroTier management mesh";
|
||||||
|
whitelist = {
|
||||||
|
reason = "ZeroTier mesh is the admin path";
|
||||||
|
cidr = [ subnetZtier ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.crowdsec-firewall-bouncer = {
|
||||||
|
enable = true;
|
||||||
|
registerBouncer.enable = true;
|
||||||
|
settings.mode = "nftables";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
_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.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
allowedWAN = 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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
enableOmada = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "TP-Link Omada SDN controller (podman container)";
|
||||||
|
};
|
||||||
|
|
||||||
|
speedtest = {
|
||||||
|
enable = {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Periodic WAN speed test so ISP degradation shows up as a trend. A timer runs librespeed-cli and writes the results as Prometheus metrics into node_exporter's textfile collector";
|
||||||
|
};
|
||||||
|
interval = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "hourly";
|
||||||
|
description = "systemd OnCalendar spec for the WAN speed test.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
proxy = {
|
||||||
|
|
||||||
|
enable = lib.mkEnableOption "internal reverse proxy (Caddy, wildcard cert via DNS-01)";
|
||||||
|
|
||||||
|
services = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule {
|
||||||
|
|
||||||
|
options = {
|
||||||
|
backend = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "https://127.0.0.1:8043";
|
||||||
|
description = "URL Caddy forwards to (internal/mesh address).";
|
||||||
|
};
|
||||||
|
insecureSkipVerify = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Skip TLS verification towards the backend.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
description = "Proxied services";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowVlans = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [
|
||||||
|
"mgmt"
|
||||||
|
"lan"
|
||||||
|
];
|
||||||
|
description = "VLANs whose clients may reach the proxy (443, plus 80 for the redirect).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
upLinkPorts = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
description = "DHCP-client uplink into the existing router LAN so this machine has internet";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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 "25-access-${iface}" {
|
||||||
|
matchConfig.Name = iface;
|
||||||
|
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
|
||||||
|
|
||||||
|
// lib.listToAttrs (
|
||||||
|
map (port: {
|
||||||
|
name = "15-uplink-${port}";
|
||||||
|
value = {
|
||||||
|
matchConfig.Name = "${port}";
|
||||||
|
networkConfig.DHCP = "ipv4";
|
||||||
|
};
|
||||||
|
}) settings.upLinkPorts
|
||||||
|
);
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
tcpdump
|
||||||
|
mtr
|
||||||
|
ethtool
|
||||||
|
conntrack-tools
|
||||||
|
knot-dns
|
||||||
|
iftop
|
||||||
|
librespeed-cli
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
./crowdsec.nix
|
||||||
|
./dns-dhcp.nix
|
||||||
|
./firewall.nix
|
||||||
|
./iperf.nix
|
||||||
|
./ipv6.nix
|
||||||
|
./omada.nix
|
||||||
|
./pppoe.nix
|
||||||
|
./proxy.nix
|
||||||
|
./speedtest.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
dhcpVlans = lib.filterAttrs (_: vlan: vlan.dhcp.enable) settings.vlans;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.kea.dhcp4 = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
interfaces-config.interfaces = lib.mapAttrsToList (name: _: "vlan-${name}") dhcpVlans;
|
||||||
|
lease-database = {
|
||||||
|
type = "memfile";
|
||||||
|
persist = true;
|
||||||
|
name = "/var/lib/kea/dhcp4.leases";
|
||||||
|
};
|
||||||
|
valid-lifetime = 86400;
|
||||||
|
subnet4 = lib.mapAttrsToList (name: vlan: {
|
||||||
|
id = vlan.id;
|
||||||
|
subnet = vlan.subnet;
|
||||||
|
interface = "vlan-${name}";
|
||||||
|
valid-lifetime = vlan.dhcp.leaseTime;
|
||||||
|
pools = [ { pool = "${vlan.dhcp.pool.from} - ${vlan.dhcp.pool.to}"; } ];
|
||||||
|
reservations = lib.mapAttrsToList (host: res: {
|
||||||
|
hostname = host;
|
||||||
|
hw-address = res.hwAddress;
|
||||||
|
ip-address = res.ipAddress;
|
||||||
|
}) vlan.dhcp.fixedIPs;
|
||||||
|
option-data = [
|
||||||
|
{
|
||||||
|
name = "routers";
|
||||||
|
data = vlan.address;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
|
data = vlan.address;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}) dhcpVlans;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.blocky = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
ports = {
|
||||||
|
dns = 53;
|
||||||
|
http = 4000;
|
||||||
|
};
|
||||||
|
upstreams.groups.default = [
|
||||||
|
"9.9.9.9"
|
||||||
|
"149.112.112.112"
|
||||||
|
"2620:fe::fe"
|
||||||
|
];
|
||||||
|
blocking = {
|
||||||
|
denylists.ads = [
|
||||||
|
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
|
||||||
|
];
|
||||||
|
clientGroupsBlock.default = [ "ads" ];
|
||||||
|
};
|
||||||
|
caching = {
|
||||||
|
minTime = "5m";
|
||||||
|
prefetching = true;
|
||||||
|
};
|
||||||
|
prometheus.enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.nameservers = [
|
||||||
|
"9.9.9.9"
|
||||||
|
"1.1.1.1"
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
|
||||||
|
dir = config.clan.core.settings.directory;
|
||||||
|
instance = "zerotier";
|
||||||
|
|
||||||
|
networkId = builtins.readFile "${dir}/vars/shared/zerotier-network-${instance}/network-id/value";
|
||||||
|
full = "fd" + networkId + "9993";
|
||||||
|
hextet = i: builtins.substring (i * 4) 4 full;
|
||||||
|
subnetZtier = "${hextet 0}:${hextet 1}:${hextet 2}:${hextet 3}:${hextet 4}:${builtins.substring 20 2 full}00::/88";
|
||||||
|
|
||||||
|
vlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") settings.vlans;
|
||||||
|
wanVlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") (
|
||||||
|
lib.filterAttrs (_: vlan: vlan.allowedWAN) settings.vlans
|
||||||
|
);
|
||||||
|
nonMgmtIfs = lib.filter (i: i != "vlan-mgmt") vlanIfs;
|
||||||
|
ifSet = ifs: "{ ${lib.concatStringsSep ", " (map (i: "\"${i}\"") ifs)} }";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
networking.nftables.enable = true;
|
||||||
|
|
||||||
|
services.openssh.openFirewall = false;
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
enable = true;
|
||||||
|
filterForward = true;
|
||||||
|
trustedInterfaces = [ "vlan-mgmt" ];
|
||||||
|
|
||||||
|
interfaces = lib.genAttrs nonMgmtIfs (_: {
|
||||||
|
allowedTCPPorts = [ 53 ];
|
||||||
|
allowedUDPPorts = [
|
||||||
|
53
|
||||||
|
67
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
extraInputRules = ''
|
||||||
|
ip6 saddr ${subnetZtier} tcp dport 22 accept comment "admin ssh over the mesh"
|
||||||
|
ip6 saddr ${subnetZtier} tcp dport 4000 accept comment "blocky metrics scrape from control"
|
||||||
|
'';
|
||||||
|
|
||||||
|
extraForwardRules = ''
|
||||||
|
tcp flags syn tcp option maxseg size set rt mtu comment "MSS clamp for PPPoE mtu 1492"
|
||||||
|
iifname "vlan-mgmt" accept comment "mgmt reaches all VLANs and the WAN"
|
||||||
|
iifname ${ifSet wanVlanIfs} oifname "ppp0" accept comment "LAN to internet"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.nat = {
|
||||||
|
enable = true;
|
||||||
|
externalInterface = "ppp0";
|
||||||
|
internalInterfaces = vlanIfs;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{ self, inputs, ... }:
|
||||||
|
let
|
||||||
|
module = ./default.nix;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
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,34 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
vlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") settings.vlans;
|
||||||
|
|
||||||
|
dir = config.clan.core.settings.directory;
|
||||||
|
instance = "zerotier";
|
||||||
|
|
||||||
|
networkId = builtins.readFile "${dir}/vars/shared/zerotier-network-${instance}/network-id/value";
|
||||||
|
full = "fd" + networkId + "9993";
|
||||||
|
hextet = i: builtins.substring (i * 4) 4 full;
|
||||||
|
subnetZtier = "${hextet 0}:${hextet 1}:${hextet 2}:${hextet 3}:${hextet 4}:${builtins.substring 20 2 full}00::/88";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.iperf3.enable = true;
|
||||||
|
|
||||||
|
networking.firewall.interfaces = lib.genAttrs vlanIfs (_: {
|
||||||
|
allowedTCPPorts = [ 5201 ];
|
||||||
|
allowedUDPPorts = [ 5201 ];
|
||||||
|
});
|
||||||
|
|
||||||
|
networking.firewall.extraInputRules = ''
|
||||||
|
ip6 saddr ${subnetZtier} tcp dport 5201 accept comment "iperf3 over the mesh"
|
||||||
|
ip6 saddr ${subnetZtier} udp dport 5201 accept comment "iperf3 over the mesh"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
|
||||||
|
systemd.network.networks."45-ppp0" = {
|
||||||
|
matchConfig.Name = "ppp0";
|
||||||
|
networkConfig = {
|
||||||
|
DHCP = "ipv6";
|
||||||
|
# pppd owns the v4 address/route on this link; don't let networkd
|
||||||
|
# tear them down.
|
||||||
|
KeepConfiguration = "static";
|
||||||
|
# Default v6 route comes from the ISP's RA when they send one.
|
||||||
|
IPv6AcceptRA = true;
|
||||||
|
};
|
||||||
|
# Many PPPoE ISPs never send an RA with the M flag; solicit regardless.
|
||||||
|
dhcpV6Config.WithoutRA = "solicit";
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
|
||||||
|
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = lib.mkDefault 1;
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
dir = config.clan.core.settings.directory;
|
||||||
|
instance = "zerotier";
|
||||||
|
|
||||||
|
networkId = builtins.readFile "${dir}/vars/shared/zerotier-network-${instance}/network-id/value";
|
||||||
|
full = "fd" + networkId + "9993";
|
||||||
|
hextet = i: builtins.substring (i * 4) 4 full;
|
||||||
|
subnetZtier = "${hextet 0}:${hextet 1}:${hextet 2}:${hextet 3}:${hextet 4}:${builtins.substring 20 2 full}00::/88";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf (settings.enableOmada) {
|
||||||
|
virtualisation.podman.enable = true;
|
||||||
|
virtualisation.oci-containers = {
|
||||||
|
backend = "podman";
|
||||||
|
containers.omada = {
|
||||||
|
image = "docker.io/mbentley/omada-controller:5.15";
|
||||||
|
extraOptions = [ "--network=host" ];
|
||||||
|
environment.TZ = config.time.timeZone;
|
||||||
|
volumes = [
|
||||||
|
"/var/lib/omada/data:/opt/tplink/EAPController/data"
|
||||||
|
"/var/lib/omada/logs:/opt/tplink/EAPController/logs"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.extraInputRules = ''
|
||||||
|
ip6 saddr ${subnetZtier} tcp dport 8043 accept comment "omada ui over the mesh"
|
||||||
|
'';
|
||||||
|
|
||||||
|
clan.core.state.omada.folders = [ "/var/lib/omada" ];
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{ config, ... }:
|
||||||
|
let
|
||||||
|
creds = config.clan.core.vars.generators.gw-router;
|
||||||
|
|
||||||
|
pppInterface = if settings.wan.vlanId == null then settings.wan.interface else "wan-vlan";
|
||||||
|
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 ${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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
config = lib.mkIf (settings.proxy.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = lib.all (v: settings.vlans ? ${v}) settings.proxy.allowVlans;
|
||||||
|
message = "cnx.router.proxy.allowVlans must name VLANs defined in cnx.router.vlans.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
roles.default.perInstance = { settings, ... }: {
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
textfileDir = "/var/lib/speedtest";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf (settings.speedtest.enable) {
|
||||||
|
services.prometheus.exporters.node.extraFlags = [
|
||||||
|
"--collector.textfile.directory=${textfileDir}"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.speedtest = {
|
||||||
|
description = "WAN speed test to Prometheus textfile metrics";
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
path = [
|
||||||
|
pkgs.librespeed-cli
|
||||||
|
pkgs.jq
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
StateDirectory = "speedtest";
|
||||||
|
# One test at boot would race PPPoE and log a spurious failure.
|
||||||
|
ExecCondition = "${pkgs.iproute2}/bin/ip link show ppp0";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
tmp="${textfileDir}/.speedtest.prom.tmp"
|
||||||
|
if result=$(librespeed-cli --json); then
|
||||||
|
jq -r '.[0]
|
||||||
|
| "speedtest_download_mbps \(.download)",
|
||||||
|
"speedtest_upload_mbps \(.upload)",
|
||||||
|
"speedtest_ping_ms \(.ping)",
|
||||||
|
"speedtest_jitter_ms \(.jitter)",
|
||||||
|
"speedtest_success 1"' <<<"$result" > "$tmp"
|
||||||
|
else
|
||||||
|
echo "speedtest_success 0" > "$tmp"
|
||||||
|
fi
|
||||||
|
mv "$tmp" "${textfileDir}/speedtest.prom"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.speedtest = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = settings.speedtest.interval;
|
||||||
|
RandomizedDelaySec = "10m";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user