initial commit
This commit is contained in:
128
modules/cloonar-assistant/default.nix
Normal file
128
modules/cloonar-assistant/default.nix
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{ config, options, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.cloonar-assistant;
|
||||||
|
|
||||||
|
vpn-client-opts = peerOpts = self: {
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
publicKey = mkOption {
|
||||||
|
example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
|
||||||
|
type = types.singleLineStr;
|
||||||
|
description = "The base64 public key of the peer.";
|
||||||
|
};
|
||||||
|
|
||||||
|
presharedKey = mkOption {
|
||||||
|
default = null;
|
||||||
|
example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
|
||||||
|
type = with types; nullOr str;
|
||||||
|
description = ''
|
||||||
|
Base64 preshared key generated by {command}`wg genpsk`.
|
||||||
|
Optional, and may be omitted. This option adds an additional layer of
|
||||||
|
symmetric-key cryptography to be mixed into the already existing
|
||||||
|
public-key cryptography, for post-quantum resistance.
|
||||||
|
|
||||||
|
Warning: Consider using presharedKeyFile instead if you do not
|
||||||
|
want to store the key in the world-readable Nix store.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
presharedKeyFile = mkOption {
|
||||||
|
default = null;
|
||||||
|
example = "/private/wireguard_psk";
|
||||||
|
type = with types; nullOr str;
|
||||||
|
description = ''
|
||||||
|
File pointing to preshared key as generated by {command}`wg genpsk`.
|
||||||
|
Optional, and may be omitted. This option adds an additional layer of
|
||||||
|
symmetric-key cryptography to be mixed into the already existing
|
||||||
|
public-key cryptography, for post-quantum resistance.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
allowedIPs = mkOption {
|
||||||
|
example = [
|
||||||
|
"10.192.122.3/32"
|
||||||
|
"10.192.124.1/24"
|
||||||
|
];
|
||||||
|
type = with types; listOf str;
|
||||||
|
description = ''
|
||||||
|
List of IP (v4 or v6) addresses with CIDR masks from
|
||||||
|
which this peer is allowed to send incoming traffic and to which
|
||||||
|
outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may
|
||||||
|
be specified for matching all IPv4 addresses, and ::/0 may be specified
|
||||||
|
for matching all IPv6 addresses.'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.cloonar-assistant = {
|
||||||
|
networkPrefix = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "10.42";
|
||||||
|
description = "First two octets of the network";
|
||||||
|
};
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "example.smart.cloonar.com";
|
||||||
|
description = "domain of the network";
|
||||||
|
};
|
||||||
|
updns = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable updns";
|
||||||
|
};
|
||||||
|
key = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "example";
|
||||||
|
description = "key for updns";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
vpn = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable VPN";
|
||||||
|
};
|
||||||
|
clients = mkOption {
|
||||||
|
default = [ ];
|
||||||
|
description = "VPN Clients";
|
||||||
|
type = with types; listOf (submodule vpn-client-opts);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
multiroom-audio = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable multiroom audio";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
firewall = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable firewall";
|
||||||
|
};
|
||||||
|
interfaces = {
|
||||||
|
wan = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "enp2s0";
|
||||||
|
description = "Network interface for WAN";
|
||||||
|
};
|
||||||
|
internal = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "enp3s0";
|
||||||
|
description = "Internal network interface";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
imports = [
|
||||||
|
# Include the results of the hardware scan.
|
||||||
|
./networking
|
||||||
|
./updns
|
||||||
|
./home-assistant
|
||||||
|
./multiroom-audio
|
||||||
|
];
|
||||||
|
}
|
||||||
249
modules/cloonar-assistant/home-assistant/default.nix
Normal file
249
modules/cloonar-assistant/home-assistant/default.nix
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
{ config, pkgs, ... }:
|
||||||
|
let
|
||||||
|
domain = "home-assistant.${config.cloonar-assistant.domain}";
|
||||||
|
pkgs-with-home-assistant = import (builtins.fetchGit {
|
||||||
|
name = "new-home-assistant";
|
||||||
|
url = "https://github.com/nixos/nixpkgs/";
|
||||||
|
rev = "18dd725c29603f582cf1900e0d25f9f1063dbf11";
|
||||||
|
}) {};
|
||||||
|
networkPrefix = config.networkPrefix;
|
||||||
|
home-assistant-config = config.home-assistant;
|
||||||
|
home-assistant-config.package = pkgs-with-home-assistant.home-assistant;
|
||||||
|
|
||||||
|
|
||||||
|
certDir = "/var/lib/ssl/home-assistant";
|
||||||
|
certFile = "${certDir}/selfsigned.crt";
|
||||||
|
keyFile = "${certDir}/selfsigned.key";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
users.users.hass = {
|
||||||
|
home = "/var/lib/hass";
|
||||||
|
createHome = true;
|
||||||
|
group = "hass";
|
||||||
|
uid = config.ids.uids.hass;
|
||||||
|
extraGroups = [ "dialout" ];
|
||||||
|
};
|
||||||
|
users.groups.hass.gid = config.ids.gids.hass;
|
||||||
|
|
||||||
|
security.acme.certs."${domain}" = {
|
||||||
|
group = "nginx";
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.secrets."home-assistant-secrets.yaml" = {
|
||||||
|
owner = "hass";
|
||||||
|
restartUnits = [ "container@hass.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
containers.hass = {
|
||||||
|
autoStart = true;
|
||||||
|
ephemeral = false;
|
||||||
|
privateNetwork = true;
|
||||||
|
hostBridge = "server";
|
||||||
|
hostAddress = "${networkPrefix}.97.1";
|
||||||
|
localAddress = "${networkPrefix}.97.20/24";
|
||||||
|
extraFlags = [
|
||||||
|
"--capability=CAP_NET_ADMIN"
|
||||||
|
"--capability=CAP_MKNOD"
|
||||||
|
];
|
||||||
|
bindMounts = {
|
||||||
|
"/etc/localtime" = {
|
||||||
|
hostPath = "/etc/localtime";
|
||||||
|
};
|
||||||
|
"/var/lib/hass" = {
|
||||||
|
hostPath = "/var/lib/hass/";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
"/var/lib/acme/hass/" = {
|
||||||
|
hostPath = "${config.security.acme.certs.${domain}.directory}";
|
||||||
|
};
|
||||||
|
"/var/lib/hass/secrets.yaml" = {
|
||||||
|
hostPath = config.sops.secrets."home-assistant-secrets.yaml".path;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config = { lib, config, pkgs, ... }: {
|
||||||
|
networkPrefix = networkPrefix;
|
||||||
|
imports = [
|
||||||
|
];
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = "home-assistant";
|
||||||
|
useHostResolvConf = false;
|
||||||
|
defaultGateway = {
|
||||||
|
address = "${networkPrefix}.96.1";
|
||||||
|
interface = "eth0";
|
||||||
|
};
|
||||||
|
firewall.enable = false;
|
||||||
|
nameservers = [ "${networkPrefix}.97.1" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.mariadb
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.generate-selfsigned-cert = {
|
||||||
|
description = "Generate/renew self-signed SSL certificate";
|
||||||
|
wantedBy = [ "nginx.service" ];
|
||||||
|
path = [ pkgs.openssl pkgs.gnugrep ];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
if [ -f ${certFile} ]; then
|
||||||
|
expiry=$(openssl x509 -enddate -noout -in ${certFile} | cut -d= -f2)
|
||||||
|
expiry_epoch=$(date -d "$expiry" +%s)
|
||||||
|
current_epoch=$(date +%s)
|
||||||
|
days_left=$(( (expiry_epoch - current_epoch) / 86400 ))
|
||||||
|
|
||||||
|
if [ $days_left -lt 30 ]; then # Regenerate if expiring in <30 days
|
||||||
|
echo "Certificate expiring soon, regenerating..."
|
||||||
|
rm ${certFile} ${keyFile}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f ${certFile} ] || [ ! -f ${keyFile} ]; then
|
||||||
|
openssl req -x509 -nodes -days 365 \
|
||||||
|
-newkey rsa:2048 \
|
||||||
|
-keyout ${keyFile} \
|
||||||
|
-out ${certFile} \
|
||||||
|
-subj "/CN=${domain}"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.enable = true;
|
||||||
|
services.nginx.virtualHosts."${domain}" = {
|
||||||
|
sslCertificate = certFile;
|
||||||
|
sslCertificateKey = keyFile;
|
||||||
|
forceSSL = true;
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_buffering off;
|
||||||
|
'';
|
||||||
|
locations."/".extraConfig = ''
|
||||||
|
proxy_pass http://127.0.0.1:8123;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_redirect http:// https://;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.home-assistant = home-assistant-config;
|
||||||
|
|
||||||
|
services.home-assistant.extraComponents = [
|
||||||
|
"mobile_app"
|
||||||
|
"backup"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.install-hacs = {
|
||||||
|
description = "Install HACS";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
set -e
|
||||||
|
HACS_VERSION="2.0.5" # Replace with the latest version
|
||||||
|
HACS_DIR="/var/lib/hass/custom_components/hacs"
|
||||||
|
|
||||||
|
mkdir -p "$HACS_DIR"
|
||||||
|
${pkgs.curl}/bin/curl -L "https://github.com/hacs/integration/releases/download/$HACS_VERSION/hacs.zip" -o /tmp/hacs.zip
|
||||||
|
${pkgs.unzip}/bin/unzip -o /tmp/hacs.zip -d "$HACS_DIR"
|
||||||
|
rm /tmp/hacs.zip
|
||||||
|
chown -R hass:hass "$HACS_DIR"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.home-assistant.extraPackages = ps: with ps; [
|
||||||
|
mysqlclient
|
||||||
|
];
|
||||||
|
|
||||||
|
services.mysql = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.mariadb;
|
||||||
|
ensureDatabases = [ "hass" ];
|
||||||
|
ensureUsers = [
|
||||||
|
{
|
||||||
|
name = "hass";
|
||||||
|
ensurePermissions = {
|
||||||
|
"hass.*" = "ALL PRIVILEGES";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
services.mysqlBackup = {
|
||||||
|
enable = true;
|
||||||
|
databases = [ "hass" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.home-assistant.config =
|
||||||
|
let
|
||||||
|
hiddenEntities = [
|
||||||
|
"sensor.last_boot"
|
||||||
|
"sensor.date"
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
recorder = {
|
||||||
|
db_url = "mysql://hass@localhost/hass?unix_socket=/var/run/mysqld/mysqld.sock";
|
||||||
|
};
|
||||||
|
homeassistant = {
|
||||||
|
name = "Home";
|
||||||
|
latitude = "!secret home_latitude";
|
||||||
|
longitude = "!secret home_longitude";
|
||||||
|
elevation = "!secret home_elevation";
|
||||||
|
unit_system = "metric";
|
||||||
|
currency = "EUR";
|
||||||
|
country = "AT";
|
||||||
|
time_zone = "Europe/Vienna";
|
||||||
|
external_url = "https://${domain}";
|
||||||
|
};
|
||||||
|
zone = {
|
||||||
|
name = "Home";
|
||||||
|
latitude = "!secret home_latitude";
|
||||||
|
longitude = "!secret home_longitude";
|
||||||
|
radius = 35;
|
||||||
|
icon = "mdi:account-multiple";
|
||||||
|
|
||||||
|
};
|
||||||
|
automation = "!include automations.yaml";
|
||||||
|
frontend = { };
|
||||||
|
http = {
|
||||||
|
use_x_forwarded_for = true;
|
||||||
|
trusted_proxies = [
|
||||||
|
"127.0.0.1"
|
||||||
|
"::1"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
api = { };
|
||||||
|
history.exclude = {
|
||||||
|
entities = hiddenEntities;
|
||||||
|
domains = [
|
||||||
|
"automation"
|
||||||
|
"updater"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"map" = { };
|
||||||
|
# logbook.exclude.entities = "hiddenEntities";
|
||||||
|
logger = {
|
||||||
|
default = "warning";
|
||||||
|
};
|
||||||
|
|
||||||
|
network = { };
|
||||||
|
zeroconf = { };
|
||||||
|
system_health = { };
|
||||||
|
default_config = { };
|
||||||
|
system_log = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.hass.extraGroups = [ "dialout" ];
|
||||||
|
system.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
7
modules/cloonar-assistant/networking/default.nix
Normal file
7
modules/cloonar-assistant/networking/default.nix
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{}: {
|
||||||
|
imports = [
|
||||||
|
./interfaces.nix
|
||||||
|
./dhcp.nix
|
||||||
|
./firewall.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
187
modules/cloonar-assistant/networking/dhcp.nix
Normal file
187
modules/cloonar-assistant/networking/dhcp.nix
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
services.kea.dhcp4 = lib.mkIf config.cloonar-assistant.firewall.enable {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
interfaces-config = {
|
||||||
|
interfaces = [
|
||||||
|
"lan"
|
||||||
|
"server"
|
||||||
|
"infrastructure"
|
||||||
|
"multimedia"
|
||||||
|
"smart"
|
||||||
|
"guest"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
lease-database = {
|
||||||
|
name = "/var/lib/kea/dhcp4.leases";
|
||||||
|
persist = true;
|
||||||
|
type = "memfile";
|
||||||
|
};
|
||||||
|
rebind-timer = 2000;
|
||||||
|
renew-timer = 1000;
|
||||||
|
subnet4 = [
|
||||||
|
{
|
||||||
|
id = 96;
|
||||||
|
pools = [
|
||||||
|
{
|
||||||
|
pool = "${config.networkPrefix}.96.100 - ${config.networkPrefix}.96.240";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subnet = "${config.networkPrefix}.96.0/24";
|
||||||
|
interface = "lan";
|
||||||
|
option-data = [
|
||||||
|
{
|
||||||
|
name = "routers";
|
||||||
|
data = "${config.networkPrefix}.96.1";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name";
|
||||||
|
data = config.cloonar-assistant.domain;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-search";
|
||||||
|
data = config.cloonar-assistant.domain;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
|
data = "${config.networkPrefix}.96.1";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
reservations = [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
id = 97;
|
||||||
|
pools = [
|
||||||
|
{
|
||||||
|
pool = "${config.networkPrefix}.97.100 - ${config.networkPrefix}.97.240";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subnet = "${config.networkPrefix}.97.0/24";
|
||||||
|
interface = "server";
|
||||||
|
option-data = [
|
||||||
|
{
|
||||||
|
name = "routers";
|
||||||
|
data = "${config.networkPrefix}.97.1";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name";
|
||||||
|
data = config.cloonar-assistant.domain;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
|
data = "${config.networkPrefix}.97.1";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
reservations = [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
id = 101;
|
||||||
|
pools = [
|
||||||
|
{
|
||||||
|
pool = "${config.networkPrefix}.101.100 - ${config.networkPrefix}.101.240";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subnet = "${config.networkPrefix}.101.0/24";
|
||||||
|
interface = "infrastructure";
|
||||||
|
option-data = [
|
||||||
|
{
|
||||||
|
name = "routers";
|
||||||
|
data = "${config.networkPrefix}.101.1";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name";
|
||||||
|
data = config.cloonar-assistant.domain;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
|
data = "${config.networkPrefix}.101.1";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "capwap-ac-v4";
|
||||||
|
code = 138;
|
||||||
|
data = "${config.networkPrefix}.97.2";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
reservations = [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
id = 99;
|
||||||
|
pools = [
|
||||||
|
{
|
||||||
|
pool = "${config.networkPrefix}.99.100 - ${config.networkPrefix}.99.240";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subnet = "${config.networkPrefix}.99.0/24";
|
||||||
|
interface = "multimedia";
|
||||||
|
option-data = [
|
||||||
|
{
|
||||||
|
name = "routers";
|
||||||
|
data = "${config.networkPrefix}.99.1";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name";
|
||||||
|
data = config.cloonar-assistant.domain;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
|
data = "${config.networkPrefix}.99.1";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
reservations = [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
id = 254;
|
||||||
|
pools = [
|
||||||
|
{
|
||||||
|
pool = "${config.networkPrefix}.254.10 - ${config.networkPrefix}.254.254";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subnet = "${config.networkPrefix}.254.0/24";
|
||||||
|
interface = "guest";
|
||||||
|
option-data = [
|
||||||
|
{
|
||||||
|
name = "routers";
|
||||||
|
data = "${config.networkPrefix}.254.1";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
|
data = "9.9.9.9";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
id = 100;
|
||||||
|
pools = [
|
||||||
|
{
|
||||||
|
pool = "${config.networkPrefix}.100.100 - ${config.networkPrefix}.100.240";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
subnet = "${config.networkPrefix}.100.0/24";
|
||||||
|
interface = "smart";
|
||||||
|
option-data = [
|
||||||
|
{
|
||||||
|
name = "routers";
|
||||||
|
data = "${config.networkPrefix}.100.1";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name";
|
||||||
|
data = config.cloonar-assistant.domain;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "domain-name-servers";
|
||||||
|
data = "${config.networkPrefix}.100.1";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
reservations = [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
valid-lifetime = 4000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
150
modules/cloonar-assistant/networking/firewall.nix
Normal file
150
modules/cloonar-assistant/networking/firewall.nix
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
forward-chain = ''
|
||||||
|
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
networking = {
|
||||||
|
firewall.checkReversePath = false;
|
||||||
|
nat.enable = false;
|
||||||
|
nftables = {
|
||||||
|
enable = true;
|
||||||
|
tables = {
|
||||||
|
"cloonar-fw" = {
|
||||||
|
family = "inet";
|
||||||
|
content = ''
|
||||||
|
chain output {
|
||||||
|
type filter hook output priority 100; policy accept;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain rpfilter {
|
||||||
|
type filter hook prerouting priority mangle + 10; policy drop;
|
||||||
|
meta nfproto ipv4 udp sport . udp dport { 68 . 67, 67 . 68 } accept comment "DHCPv4 client/server"
|
||||||
|
fib saddr . mark . iif oif exists accept
|
||||||
|
}
|
||||||
|
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority filter; policy drop;
|
||||||
|
iifname "lo" accept comment "trusted interfaces"
|
||||||
|
iifname "lan" counter accept comment "Spice"
|
||||||
|
ct state vmap { invalid : drop, established : accept, related : accept, new : jump input-allow, untracked : jump input-allow }
|
||||||
|
tcp flags syn / fin,syn,rst,ack log prefix "refused connection: " level info
|
||||||
|
}
|
||||||
|
|
||||||
|
chain input-allow {
|
||||||
|
udp dport != { 53, 5353 } ct state new limit rate over 1/second burst 10 packets drop comment "rate limit for new connections"
|
||||||
|
iifname lo accept
|
||||||
|
${lib.optionalString config.cloonar-assistant.vpn.enable ''
|
||||||
|
iifname "wan" udp dport 51820 counter accept comment "Wireguard traffic"
|
||||||
|
''}
|
||||||
|
${lib.optionalString config.cloonar-assistant.firewall.enable ''
|
||||||
|
iifname "wan" tcp dport 9273 counter accept comment "Prometheus traffic"
|
||||||
|
''}
|
||||||
|
${lib.optionalString config.cloonar-assistant.firewall.enable ''
|
||||||
|
iifname { "server", "vserver", "vm-*", "lan", "wg_cloonar" } counter accept comment "allow trusted to router"
|
||||||
|
iifname { "multimedia", "smart", "infrastructure", "podman0", "setup" } udp dport { 53, 5353 } counter accept comment "DNS"
|
||||||
|
''}
|
||||||
|
|
||||||
|
iifname { "wan", "multimedia" } icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
|
||||||
|
|
||||||
|
# Accept mDNS for avahi reflection
|
||||||
|
${lib.optionalString config.cloonar-assistant.multiroom-audio.enable ''
|
||||||
|
iifname "server" ip saddr ${config.networkPrefix}.97.20/32 tcp dport { llmnr } counter accept
|
||||||
|
iifname "server" ip saddr ${config.networkPrefix}.97.20/32 udp dport { mdns, llmnr } counter accept
|
||||||
|
''}
|
||||||
|
|
||||||
|
# Allow all returning traffic
|
||||||
|
ct state { established, related } counter accept
|
||||||
|
|
||||||
|
# Allow returning traffic from wan and drop everthing else
|
||||||
|
iifname "wan" ct state { established, related } accept comment "Allow established traffic"
|
||||||
|
iifname "wan" icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
|
||||||
|
iifname "wan" counter drop comment "Drop all other unsolicited traffic from wan"
|
||||||
|
|
||||||
|
limit rate 60/minute burst 100 packets log prefix "Input - Drop: " comment "Log any unmatched traffic"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority filter; policy drop;
|
||||||
|
|
||||||
|
iifname "wg_cloonar" counter accept comment "test wireguard"
|
||||||
|
|
||||||
|
${lib.optionalString config.cloonar-assistant.vpn.enable ''
|
||||||
|
iifname "wg_cloonar" oifname lo counter accept comment "wireguard to server"
|
||||||
|
''}
|
||||||
|
|
||||||
|
# enable flow offloading for better throughput
|
||||||
|
# ip protocol { tcp, udp } flow offload @f
|
||||||
|
|
||||||
|
# multimedia airplay
|
||||||
|
${lib.optionalString config.cloonar-assistant.multiroom-audio.enable ''
|
||||||
|
iifname "multimedia" oifname { "lan" } counter accept
|
||||||
|
iifname "multimedia" oifname "server" tcp dport { 1704, 1705 } counter accept
|
||||||
|
iifname "lan" oifname "server" udp dport { 5000, 5353, 6001 - 6011 } counter accept
|
||||||
|
# avahi
|
||||||
|
iifname "server" ip saddr ${config.networkPrefix}.97.20/32 oifname { "lan" } counter accept
|
||||||
|
''}
|
||||||
|
|
||||||
|
${lib.optionalString config.cloonar-assistant.firewall.enable ''
|
||||||
|
# smart home coap
|
||||||
|
iifname "smart" oifname "server" ip daddr ${config.networkPrefix}.97.20/32 udp dport { 5683 } counter accept
|
||||||
|
iifname "smart" oifname "server" ip daddr ${config.networkPrefix}.97.20/32 tcp dport { 1883 } counter accept
|
||||||
|
|
||||||
|
# lan and vpn to any
|
||||||
|
iifname { "lan", "server", "vserver", "wg_cloonar" } oifname { "lan", "vb-*", "vm-*", "server", "vserver", "infrastructure", "multimedia", "smart", "wg_cloonar", "guest", "setup" } counter accept
|
||||||
|
iifname { "infrastructure", "setup" } oifname { "server", "vserver" } counter accept
|
||||||
|
iifname { "lan", "wan" } udp dport { 8211, 27015 } counter accept comment "palworld"
|
||||||
|
''}
|
||||||
|
|
||||||
|
|
||||||
|
# allow all established, related
|
||||||
|
ct state { established, related } accept comment "Allow established traffic"
|
||||||
|
|
||||||
|
# Allow trusted network WAN access
|
||||||
|
${lib.optionalString config.cloonar-assistant.firewall.enable ''
|
||||||
|
iifname {
|
||||||
|
"lan",
|
||||||
|
"infrastructure",
|
||||||
|
"server",
|
||||||
|
"vserver",
|
||||||
|
"multimedia",
|
||||||
|
"smart",
|
||||||
|
"wg_cloonar",
|
||||||
|
"podman*",
|
||||||
|
"guest",
|
||||||
|
"setup",
|
||||||
|
"vb-*",
|
||||||
|
"vm-*",
|
||||||
|
} oifname {
|
||||||
|
"wan",
|
||||||
|
} counter accept comment "Allow trusted LAN to WAN"
|
||||||
|
''}
|
||||||
|
|
||||||
|
limit rate 60/minute burst 100 packets log prefix "Forward - Drop: " comment "Log any unmatched traffic"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"cloonar-nat" = {
|
||||||
|
family = "ip";
|
||||||
|
content = ''
|
||||||
|
chain prerouting {
|
||||||
|
type nat hook prerouting priority filter; policy accept;
|
||||||
|
iifname "server" ip daddr ${config.networkPrefix}.96.255 udp dport { 9 } dnat to ${config.networkPrefix}.96.255
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setup NAT masquerading on external interfaces
|
||||||
|
chain postrouting {
|
||||||
|
type nat hook postrouting priority filter; policy accept;
|
||||||
|
oifname { "wan" } masquerade
|
||||||
|
|
||||||
|
${lib.optionalString config.cloonar-assistant.vpn.enable ''
|
||||||
|
oifname { "wg_cloonar" } masquerade
|
||||||
|
''}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
112
modules/cloonar-assistant/networking/interfaces.nix
Normal file
112
modules/cloonar-assistant/networking/interfaces.nix
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
boot.kernel.sysctl = {
|
||||||
|
# if you use ipv4, this is all you need
|
||||||
|
"net.ipv4.conf.all.forwarding" = true;
|
||||||
|
# If you want to use it for ipv6
|
||||||
|
"net.ipv6.conf.all.forwarding" = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.network = {
|
||||||
|
enable = true;
|
||||||
|
wait-online.anyInterface = true;
|
||||||
|
links = {
|
||||||
|
"10-wan" = {
|
||||||
|
matchConfig.Name = cloonar-assistant.interfaces.wan;
|
||||||
|
linkConfig.Name = "wan";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
netdevs = {
|
||||||
|
"30-server".netdevConfig = {
|
||||||
|
Kind = "bridge";
|
||||||
|
Name = "server";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
networks = {
|
||||||
|
"31-server" = {
|
||||||
|
matchConfig.Name = [ "vserver" ];
|
||||||
|
# Attach to the bridge that was configured above
|
||||||
|
networkConfig.Bridge = "server";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking = if config.cloonar-assistant.firewall.enable then {
|
||||||
|
useDHCP = false;
|
||||||
|
# Define VLANS
|
||||||
|
nameservers = [ "${config.networkPrefix}.97.1" ];
|
||||||
|
# resolvconf.enable = false;
|
||||||
|
vlans = {
|
||||||
|
infrastructure = {
|
||||||
|
id = 101;
|
||||||
|
interface = config.cloonar-assistant.interfaces.internal;
|
||||||
|
};
|
||||||
|
lan = {
|
||||||
|
id = 96;
|
||||||
|
interface = config.cloonar-assistant.interfaces.internal;
|
||||||
|
};
|
||||||
|
vserver = {
|
||||||
|
id = 97;
|
||||||
|
interface = config.cloonar-assistant.interfaces.internal;
|
||||||
|
};
|
||||||
|
multimedia = {
|
||||||
|
id = 99;
|
||||||
|
interface = config.cloonar-assistant.interfaces.internal;
|
||||||
|
};
|
||||||
|
smart = {
|
||||||
|
id = 100;
|
||||||
|
interface = config.cloonar-assistant.interfaces.internal;
|
||||||
|
};
|
||||||
|
guest = {
|
||||||
|
id = 254;
|
||||||
|
interface = config.cloonar-assistant.interfaces.internal;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interfaces = {
|
||||||
|
# Don't request DHCP on the physical interfaces
|
||||||
|
internal.useDHCP = false;
|
||||||
|
|
||||||
|
# Handle the VLANs
|
||||||
|
wan.useDHCP = true;
|
||||||
|
lan = {
|
||||||
|
ipv4.addresses = [{
|
||||||
|
address = "${config.networkPrefix}.96.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
server = {
|
||||||
|
ipv4.addresses = [{
|
||||||
|
address = "${config.networkPrefix}.97.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
infrastructure = {
|
||||||
|
ipv4.addresses = [{
|
||||||
|
address = "${config.networkPrefix}.101.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
multimedia = {
|
||||||
|
ipv4.addresses = [{
|
||||||
|
address = "${config.networkPrefix}.99.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
smart = {
|
||||||
|
ipv4.addresses = [{
|
||||||
|
address = "${config.networkPrefix}.100.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
guest = {
|
||||||
|
ipv4.addresses = [{
|
||||||
|
address = "${config.networkPrefix}.254.1";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
useDHCP = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
88
modules/cloonar-assistant/updns/default.nix
Normal file
88
modules/cloonar-assistant/updns/default.nix
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{ config, pkgs, lib, ... }: {
|
||||||
|
### 1) Make sure we have the tools we need
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
curl
|
||||||
|
jq
|
||||||
|
];
|
||||||
|
|
||||||
|
sops.secrets.updns-client-token = {
|
||||||
|
owner = "updns-client";
|
||||||
|
restartUnits = [ "updns-client.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
### 3) Write the check‐script into /etc/external-ip/check.sh (0400, executable)
|
||||||
|
environment.etc."updns-client/run.sh".text = lib.mkIf config.cloonar-assistant.updns-client.enable lib.concatStringsSep "\n" [
|
||||||
|
"#!/usr/bin/env bash"
|
||||||
|
"set -euo pipefail"
|
||||||
|
""
|
||||||
|
"# Where our secret lives (encrypted)"
|
||||||
|
"SECRET=${config.sops.secrets.updns-client.path}"
|
||||||
|
"# Where we record the last‐seen IP"
|
||||||
|
"LAST_IP_FILE=/var/lib/updns-client/last-ip"
|
||||||
|
""
|
||||||
|
"# Decrypt the API key at runtime"
|
||||||
|
"API_KEY=$(cat \"$SECRET\")"
|
||||||
|
""
|
||||||
|
"# Fetch current external IP"
|
||||||
|
"IP=$(curl -fsSL https://ifconfig.me)"
|
||||||
|
""
|
||||||
|
"# Ensure state directory exists"
|
||||||
|
"mkdir -p \"$(dirname \"$LAST_IP_FILE\")\""
|
||||||
|
""
|
||||||
|
"# Read old IP (if any)"
|
||||||
|
"if [[ -f \"$LAST_IP_FILE\" ]]; then"
|
||||||
|
" OLD_IP=$(< \"$LAST_IP_FILE\")"
|
||||||
|
"else"
|
||||||
|
" OLD_IP=\"\""
|
||||||
|
"fi"
|
||||||
|
""
|
||||||
|
"# If it's changed, notify the API and update the file"
|
||||||
|
"if [[ \"$IP\" != \"$OLD_IP\" ]]; then"
|
||||||
|
""
|
||||||
|
" PAYLOAD=$(jq -n \\"
|
||||||
|
" --arg key \"${config.cloonar-assistant.updns-client.key}\" \\"
|
||||||
|
" --arg secret \"$SECRET\" \\"
|
||||||
|
" --arg host \"${config.cloonar-assistant.domain}\" \\"
|
||||||
|
" --arg ip \"$IP\" \\"
|
||||||
|
" '{key: $key, secret: $secret, host: $host, ip: $ip}')"
|
||||||
|
""
|
||||||
|
" curl -fsS -X POST https://updns-client.cloonar.com/update \\"
|
||||||
|
" -H \"Content-Type: application/json\" \\"
|
||||||
|
" -d \"$PAYLOAD\""
|
||||||
|
""
|
||||||
|
" echo \"$IP\" > \"$LAST_IP_FILE\""
|
||||||
|
"fi"
|
||||||
|
];
|
||||||
|
environment.etc."updns-client/run.sh".mode = "0500";
|
||||||
|
|
||||||
|
### 4) Ensure /var/lib/external-ip exists on boot
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
# path mode owner group age
|
||||||
|
"d /var/lib/updns-client 0755 root root -"
|
||||||
|
];
|
||||||
|
|
||||||
|
### 5) Define the oneshot service
|
||||||
|
systemd.services.updns-client = lib.mkIf config.cloonar-assistant.updns-client.enable {
|
||||||
|
description = "Check external IP and notify API on change";
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
WorkingDirectory = "/var/lib/updns-client";
|
||||||
|
ExecStart = "${pkgs.bash}/bin/bash /etc/updns-client/run.sh";
|
||||||
|
};
|
||||||
|
install.WantedBy = [ "multi-user.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
### 6) Define the timer (runs at boot + every 5 minutes)
|
||||||
|
systemd.timers.updns-client = lib.mkIf config.cloonar-assistant.updns-client.enable {
|
||||||
|
description = "Run updns-client.service every 5 minutes";
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnBootSec = "1min";
|
||||||
|
OnUnitActiveSec = "5min";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
unit = "updns-client.service";
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user