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