refactor: many changes

This commit is contained in:
2025-06-06 22:38:16 +02:00
parent e46f2a4ee7
commit 7611a8daf3
14 changed files with 797 additions and 410 deletions

10
.sops.yaml Normal file
View File

@@ -0,0 +1,10 @@
keys:
- &dominik age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
- &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
creation_rules:
- path_regex: example/[^/]+\.yaml$
key_groups:
- age:
- *dominik
- *dominik2

58
example/configuration.nix Normal file
View File

@@ -0,0 +1,58 @@
{ config, pkgs, ... }:
{
imports = [
# Import the main module
../modules/cloonar-assistant
# Include your hardware-configuration.nix and other custom modules
./hardware-configuration.nix
# ...
];
sops.defaultSopsFile = ./secrets.yaml;
# --- Configure Cloonar Assistant Options ---
cloonar-assistant = {
# Required: Define the first two octets for your internal networks
networkPrefix = "10.42"; # Example: Results in 10.42.96.0/24, 10.42.97.0/24, etc.
# Required: Define the domain name for local services and DDNS
domain = "home.example.com"; # Example
# Required: Define the network interface connected to the WAN/Internet
firewall.interfaces.wan = "eth0"; # Example
# Required: Define the network interface for internal VLANs
# Set to null if you only have one interface (WAN)
firewall.interfaces.internal = null; # Example
# Enable VPN Server
vpn.enable = true;
vpn.privateKeyFile = "/path/to/your/wireguard_private_key"; # Store securely!
vpn.clients = [
{
name = "myphone";
publicKey = "...";
allowedIPs = [ "${config.cloonar-assistant.networkPrefix}.98.2/32" ];
}
];
# Enable Dynamic DNS Updates
updns-client.enable = true;
updns-client.key = "your-updns-key"; # Key provided by updns-client.cloonar.com
updns-client.secretFile = "/path/to/your/updns_secret"; # Store securely!
# Enable setup mode (allows WAN access for initial setup - disable for production)
setup = false;
# ... other options can be configured as needed.
};
# --- Other System Configuration ---
networking.hostName = "myrouter"; # Example hostname
# Ensure necessary packages for fetching are available if not using flakes
environment.systemPackages = [ pkgs.nix ];
system.stateVersion = "23.11"; # Set to your NixOS version
}

View File

@@ -0,0 +1,46 @@
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.loader.systemd-boot = {
enable = true;
configurationLimit = 5;
};
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.extraModulePackages = [ ];
fileSystems."/boot" = {
device = "/dev/disk/by-label/boot";
fsType = "vfat";
};
boot.kernelParams = [
"tpm_tis.interrupts=0"
];
boot.initrd = {
luks.devices.root = {
device = "/dev/disk/by-label/root";
allowDiscards = true;
keyFile = "/dev/zero";
keyFileSize = 1;
};
};
fileSystems."/" = {
device = "/dev/mapper/root";
fsType = "ext4";
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

34
example/secrets.yaml Normal file
View File

@@ -0,0 +1,34 @@
hello: ENC[AES256_GCM,data:TBaGXwx/14vVACdJEkaCzh7vUbz4qt+UTgRC/lx07BDOpfn5qU7iVlcygoM7bw==,iv:9wPWbD+gqwc/V6Fb6PJDd/uqhMWzyS5iAyYeIgcGDL8=,tag:M1+2LNuSEKcTKdyc8N4qYg==,type:str]
example_key: ENC[AES256_GCM,data:dzPkbNkMZIxBwC34uA==,iv:E9QAYMyK41ivCTgZbnqapVA94nimTGK3pMuAtVGoDEA=,tag:TACnRUxHssf12vDgxXwWFQ==,type:str]
#ENC[AES256_GCM,data:t7yKBwiyR308q/L+1IG27Q==,iv:TraZXiXP8/3xLxtxzH4H6s/hmJuDmIMBnC9OefKoQ40=,tag:Rs6VCBZDVmuD+6w3Lmqtjg==,type:comment]
example_array:
- ENC[AES256_GCM,data:ogLj4WGKhiSv8pyBlKQ=,iv:NTm56BY6Cq/GkFsm0MUKERprqFKbuZqboD3xKT5UvWI=,tag:313/jWFs71v5Oegm1rwUbw==,type:str]
- ENC[AES256_GCM,data:qNkbU2m4bDBwFYmJcso=,iv:ZoKmDp/Qa9omGBcpfKKIDhM/vyqrXTLy0Z3106CXX7c=,tag:R7aYLK1gt/18r4s3K4/FnA==,type:str]
example_number: ENC[AES256_GCM,data:/tSOtRzuyL1COw==,iv:a8UsDlda41qt++4fAV6GvY+yO3mo+0mNdMgkFT9Jd74=,tag:GbDrOCNNPLW7roGLDtU9Sg==,type:float]
example_booleans:
- ENC[AES256_GCM,data:QEO7nFs=,iv:DD2UVhTtg8spaHGsXSi+keFUGiIF1Jd12KSwnX56C5k=,tag:MBRNjjCEDbvHKgXUOlwtKQ==,type:bool]
- ENC[AES256_GCM,data:dBdohUs=,iv:5DCn9JzK6lMmkhOlNrXLE9hP3rnwavPI3wULLAvOkg0=,tag:0rwH3CRu2XZvxmxe/2Jw/g==,type:bool]
sops:
age:
- recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSWFhvYVFHNHZiRzhIbSsv
ZlFlYkJvK2Q3cDFmYlE3TlhsR1RYRnkvOFc0CldnekxmbXVpUUhLc1BPWU5oeEM0
T3ZPNFVXYkYyTnFGRUVNVGRDeGx5akkKLS0tIHgrZ2M4SlhWZEtBc01ycHRsNmpl
RFRabFBzaEU0WW02cGRJOU52ai9vdXMKq6IVYKnK04G+jZrQRotr14Sod9nBXkSC
THSJ2o78nWZu2itGJOqn3O8TUJo3jXOhJVWOka4HlT2b49IjNYcg4A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMMjY4NTd3NUY1WElQbW42
OWlFM1A4Vmd0Yy8yR2FwL3grNWJYVzBYaHpBCjZvQ3dOKy9ZcHR1bGRnNDBCYkxN
TkU3RG45MW8wblc3V3A2WDZrSEdXYVkKLS0tIGNKRUUrK0VycVl4Z04xOVpMMmFq
WXg4NUJwSThJT1JHU3ErU0pWdGJZSjAKZxFJSvuuDcarCWK8Prgopfix4Q6HVQ7F
SvUqD3AX3h+48T7v0LPYau46hbaAkbDNFEmLvgCDxxGmOj6UMH5ASA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-06-05T20:17:52Z"
mac: ENC[AES256_GCM,data:XN9hYAP1sZDxx/FDjuLaSKE37gPbe8PNcxJwogWeJeq5Ht/kyQC7stKOTna6aTnXenvjzPJCvbeEeeVETM3nPv0zc5g6kHw+PDxtB6R5j47BuYX9uUAmr0m1nWbLIUqz3k5X7cPR4xmB7hYiojyTQVvwmWXf15I0m6qAVfb+HwU=,iv:CQ/X1CIMg+KEQGjwH19h9akwR1WJIPLMSYX+g0boGQI=,tag:neYchyyQP5cii7uzZJog2w==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2

View File

@@ -1,5 +1,8 @@
{ config, pkgs, ... }: { { config, pkgs, ... }: {
boot.loader.systemd-boot.enable = true; boot.loader.systemd-boot = {
enable = true;
configurationLimit = 5;
};
fileSystems."/boot" = { fileSystems."/boot" = {
device = "/dev/disk/by-label/boot"; device = "/dev/disk/by-label/boot";

View File

@@ -4,186 +4,31 @@ with lib;
let let
cfg = config.cloonar-assistant; cfg = config.cloonar-assistant;
vpn-client-opts =
{ ... }:
{
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 { in {
options.cloonar-assistant = { options.cloonar-assistant = {
setup = lib.mkOption { setup = mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = "Enable access from Wan to Setup"; description = "Enable access from Wan to Setup";
}; };
networkPrefix = lib.mkOption { networkPrefix = mkOption {
type = lib.types.str; type = types.str;
default = "10.10"; default = "10.10";
example = "10.10"; example = "10.10";
description = "First two octets of the network"; description = "First two octets of the network";
}; };
domain = lib.mkOption { domain = mkOption {
type = lib.types.str; type = types.str;
example = "example.smart.cloonar.com"; example = "example.smart.cloonar.com";
description = "domain of the network"; description = "domain of the network";
}; };
updns-client = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable updns";
};
key = lib.mkOption {
type = with types; nullOr str;
example = "example";
description = "key for updns";
};
secretFile = lib.mkOption {
type = with types; nullOr str;
example = "/private/updns_secret";
description = "File pointing to secret as generated by {command}`wg genpsk`.";
};
};
vpn = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable VPN";
};
privateKeyFile = lib.mkOption {
type = with types; nullOr str;
example = "/private/wireguard_private_key";
description = "File pointing to private key as generated by {command}`wg genkey`.";
};
clients = mkOption {
default = [ ];
description = "VPN Clients";
type = with types; listOf (submodule vpn-client-opts);
};
};
multiroom-audio = { multiroom-audio = {
enable = lib.mkOption { enable = mkOption {
type = lib.types.bool; type = types.bool;
default = false; default = false;
description = "Enable multiroom audio"; description = "Enable multiroom audio";
}; };
}; };
firewall = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable firewall";
};
ipv4 = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable firewall";
};
ipv6 = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable firewall";
};
interfaces = {
wan = lib.mkOption {
type = lib.types.str;
example = "enp2s0";
description = "Network interface for WAN";
};
internal = lib.mkOption {
type = with types; nullOr str;
example = "enp3s0";
description = "Internal network interface";
};
};
custom-rules = {
input = lib.mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
iifname "lan" udp dport 22 counter accept comment "Wireguard traffic"
iifname "lan" udp dport 80 counter accept comment "Wireguard traffic"
'';
description = "Custom iptables rules for INPUT chain";
};
forward = lib.mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
iifname "lan" oifname "server" tcp dport { 22 } counter accept
iifname "lan" oifname "server" tcp dport { 80 } counter accept
'';
description = "Custom iptables rules for FORWARD chain";
};
prerouting = lib.mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
iifname "server" ip daddr 10.0.96.255 udp dport { 9 } dnat to 10.0.96.255
'';
description = "Custom iptables rules for nat chain";
};
postrouting = lib.mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
oifname { "wan" } masquerade
'';
description = "Custom iptables rules for nat chain";
};
};
};
}; };
imports = [ imports = [
# Include the results of the hardware scan. # Include the results of the hardware scan.

View File

@@ -1,4 +1,7 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with lib;
let let
domain = config.cloonar-assistant.domain; domain = config.cloonar-assistant.domain;
pkgs-with-home-assistant = import (builtins.fetchGit { pkgs-with-home-assistant = import (builtins.fetchGit {
@@ -12,7 +15,7 @@ let
"backup" "backup"
]; ];
ha-config = lib.recursiveUpdate config.services.home-assistant.config { ha-config = recursiveUpdate config.services.home-assistant.config {
recorder = { recorder = {
db_url = "mysql://hass@localhost/hass?unix_socket=/var/run/mysqld/mysqld.sock"; db_url = "mysql://hass@localhost/hass?unix_socket=/var/run/mysqld/mysqld.sock";
}; };
@@ -72,6 +75,8 @@ let
gid = config.ids.gids.hass; gid = config.ids.gids.hass;
in in
{ {
services.home-assistant.config = mkDefault { };
users.users.hass = { users.users.hass = {
home = "/var/lib/hass"; home = "/var/lib/hass";
createHome = true; createHome = true;

View File

@@ -1,6 +1,4 @@
{ ... }: { { ... }: {
networking.hostName = "fw";
imports = [ imports = [
./interfaces.nix ./interfaces.nix
./dhcp.nix ./dhcp.nix

View File

@@ -1,159 +1,231 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let
forward-chain = ''
with lib;
let
cfg = config.cloonar-assistant.firewall;
networkPrefix = config.cloonar-assistant.networkPrefix;
forward-chain = ''
''; '';
in { in {
networking = { options.cloonar-assistant.firewall = {
firewall.checkReversePath = false; enable = mkOption {
nat.enable = false; type = types.bool;
nftables = { default = false;
enable = true; description = "Enable firewall";
tables = { };
"cloonar-fw" = { ipv4 = mkOption {
family = "inet"; type = types.bool;
content = '' default = true;
chain output { description = "Enable ipv4";
type filter hook output priority 100; policy accept; };
} ipv6 = mkOption {
type = types.bool;
default = false;
description = "Enable ipv6";
};
interfaces = {
wan = mkOption {
type = types.str;
example = "enp2s0";
description = "Network interface for WAN";
};
internal = mkOption {
type = with types; nullOr str;
example = "enp3s0";
description = "Internal network interface";
};
};
custom-rules = {
input = mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
iifname "lan" udp dport 22 counter accept comment "Wireguard traffic"
iifname "lan" udp dport 80 counter accept comment "Wireguard traffic"
'';
description = "Custom iptables rules for INPUT chain";
};
forward = mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
iifname "lan" oifname "server" tcp dport { 22 } counter accept
iifname "lan" oifname "server" tcp dport { 80 } counter accept
'';
description = "Custom iptables rules for FORWARD chain";
};
prerouting = mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
iifname "server" ip daddr 10.0.96.255 udp dport { 9 } dnat to 10.0.96.255
'';
description = "Custom iptables rules for nat chain";
};
postrouting = mkOption {
type = with types; nullOr lines;
default = '''';
example = ''
oifname { "wan" } masquerade
'';
description = "Custom iptables rules for nat chain";
};
};
};
chain rpfilter { config = {
type filter hook prerouting priority mangle + 10; policy drop; networking = {
meta nfproto ipv4 udp sport . udp dport { 68 . 67, 67 . 68 } accept comment "DHCPv4 client/server" firewall.checkReversePath = false;
fib saddr . mark . iif oif exists accept nat.enable = false;
} nftables = {
enable = true;
tables = {
"cloonar-fw" = {
family = "inet";
content = ''
chain output {
type filter hook output priority 100; policy accept;
}
chain input { chain rpfilter {
type filter hook input priority filter; policy drop; type filter hook prerouting priority mangle + 10; policy drop;
iifname "lo" accept comment "trusted interfaces" meta nfproto ipv4 udp sport . udp dport { 68 . 67, 67 . 68 } accept comment "DHCPv4 client/server"
iifname "lan" counter accept comment "Spice" fib saddr . mark . iif oif exists accept
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 { chain input {
udp dport != { 53, 5353 } ct state new limit rate over 1/second burst 10 packets drop comment "rate limit for new connections" type filter hook input priority filter; policy drop;
iifname lo accept iifname "lo" accept comment "trusted interfaces"
${lib.optionalString config.cloonar-assistant.setup '' iifname "lan" counter accept comment "Spice"
iifname "wan" accept 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
${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" 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
${optionalString config.cloonar-assistant.setup ''
iifname "wan" accept
''}
${optionalString config.cloonar-assistant.vpn.enable ''
iifname "wan" udp dport 51820 counter accept comment "Wireguard traffic"
''}
${optionalString cfg.enable ''
iifname "wan" tcp dport 9273 counter accept comment "Prometheus traffic"
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 { "multimedia" } icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
''}
# Accept mDNS for avahi reflection iifname "wan" icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
${lib.optionalString config.cloonar-assistant.multiroom-audio.enable ''
iifname "server" ip saddr ${config.cloonar-assistant.networkPrefix}.97.20/32 tcp dport { llmnr } counter accept
iifname "server" ip saddr ${config.cloonar-assistant.networkPrefix}.97.20/32 udp dport { mdns, llmnr } counter accept
''}
# Allow all returning traffic # Accept mDNS for avahi reflection
ct state { established, related } counter accept ${optionalString config.cloonar-assistant.multiroom-audio.enable ''
iifname "server" ip saddr ${networkPrefix}.97.20/32 tcp dport { llmnr } counter accept
iifname "server" ip saddr ${networkPrefix}.97.20/32 udp dport { mdns, llmnr } counter accept
''}
# Allow returning traffic from wan and drop everthing else # Allow all returning traffic
iifname "wan" ct state { established, related } accept comment "Allow established traffic" ct state { established, related } counter accept
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"
${config.cloonar-assistant.firewall.custom-rules.input} # 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" ${cfg.custom-rules.input}
}
limit rate 60/minute burst 100 packets log prefix "Input - Drop: " comment "Log any unmatched traffic"
}
chain forward { chain forward {
type filter hook forward priority filter; policy drop; 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
# allow home assistant to wan
iifname "ve-hass" oifname wan counter accept comment "Allow home assistant to WAN"
# 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.cloonar-assistant.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.cloonar-assistant.networkPrefix}.97.20/32 udp dport { 5683 } counter accept
iifname "smart" oifname "server" ip daddr ${config.cloonar-assistant.networkPrefix}.97.20/32 tcp dport { 1883 } counter accept
# lan and vpn to any
iifname { "lan", "server", "vserver", "wg_cloonar" } oifname { "lan", "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"
${config.cloonar-assistant.firewall.custom-rules.forward}
''}
# allow all established, related ${lib.optionalString config.cloonar-assistant.vpn.enable ''
ct state { established, related } accept comment "Allow established traffic" iifname "wg_cloonar" counter accept comment "test wireguard"
iifname "wg_cloonar" oifname lo counter accept comment "wireguard to server"
''}
# Allow trusted network WAN access # enable flow offloading for better throughput
${lib.optionalString config.cloonar-assistant.firewall.enable '' # ip protocol { tcp, udp } flow offload @f
iifname {
"lan",
"infrastructure",
"server",
"vserver",
"multimedia",
"smart",
"wg_cloonar",
"podman*",
"guest",
"setup",
} 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" # allow home assistant to wan
} iifname "ve-hass" oifname wan counter accept comment "Allow home assistant to WAN"
'';
};
"cloonar-nat" = {
family = "ip";
content = ''
chain prerouting {
type nat hook prerouting priority filter; policy accept;
iifname "server" ip daddr ${config.cloonar-assistant.networkPrefix}.96.255 udp dport { 9 } dnat to ${config.cloonar-assistant.networkPrefix}.96.255
${config.cloonar-assistant.firewall.custom-rules.prerouting}
}
# Setup NAT masquerading on external interfaces # multimedia airplay
chain postrouting { ${lib.optionalString config.cloonar-assistant.multiroom-audio.enable ''
type nat hook postrouting priority filter; policy accept; iifname "multimedia" oifname { "lan" } counter accept
oifname { "wan" } masquerade 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 ${networkPrefix}.97.20/32 oifname { "lan" } counter accept
''}
${lib.optionalString config.cloonar-assistant.vpn.enable '' ${lib.optionalString config.cloonar-assistant.firewall.enable ''
oifname { "wg_cloonar" } masquerade # smart home coap
''} iifname "smart" oifname "server" ip daddr ${networkPrefix}.97.20/32 udp dport { 5683 } counter accept
iifname "smart" oifname "server" ip daddr ${networkPrefix}.97.20/32 tcp dport { 1883 } counter accept
${config.cloonar-assistant.firewall.custom-rules.postrouting} # lan and vpn to any
} iifname { "lan", "server", "vserver", "wg_cloonar" } oifname { "lan", "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"
${cfg.custom-rules.forward}
''}
# allow all established, related
ct state { established, related } accept comment "Allow established traffic"
# Allow trusted network WAN access
${lib.optionalString cfg.enable ''
iifname {
"lan",
"infrastructure",
"server",
"vserver",
"multimedia",
"smart",
"wg_cloonar",
"podman*",
"guest",
"setup",
} 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 ${networkPrefix}.96.255 udp dport { 9 } dnat to ${networkPrefix}.96.255
${cfg.custom-rules.prerouting}
}
# 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
''}
${cfg.custom-rules.postrouting}
}
'';
};
}; };
}; };
}; };

View File

@@ -1,5 +1,11 @@
{ config, lib, ... }: { config, lib, ... }:
{
with lib;
let
cfg = config.cloonar-assistant.firewall.interfaces;
networkPrefix = config.cloonar-assistant.networkPrefix;
in {
boot.kernel.sysctl = { boot.kernel.sysctl = {
# if you use ipv4, this is all you need # if you use ipv4, this is all you need
"net.ipv4.conf.all.forwarding" = true; "net.ipv4.conf.all.forwarding" = true;
@@ -12,9 +18,13 @@
wait-online.anyInterface = true; wait-online.anyInterface = true;
links = { links = {
"10-wan" = { "10-wan" = {
matchConfig.Name = config.cloonar-assistant.firewall.interfaces.wan; matchConfig.Name = cfg.wan;
linkConfig.Name = "wan"; linkConfig.Name = "wan";
}; };
"20-internal" = mkIf (cfg.internal != null) {
matchConfig.Name = cfg.internal;
linkConfig.Name = "internal";
};
}; };
netdevs = { netdevs = {
"30-server".netdevConfig = { "30-server".netdevConfig = {
@@ -34,32 +44,32 @@
networking = if config.cloonar-assistant.firewall.enable then { networking = if config.cloonar-assistant.firewall.enable then {
useDHCP = false; useDHCP = false;
# Define VLANS # Define VLANS
nameservers = [ "${config.cloonar-assistant.networkPrefix}.97.1" ]; nameservers = [ "${networkPrefix}.97.1" ];
# resolvconf.enable = false; # resolvconf.enable = false;
vlans = { vlans = mkIf (cfg.internal != null) {
infrastructure = { infrastructure = {
id = 101; id = 101;
interface = config.cloonar-assistant.firewall.interfaces.internal; interface = cfg.internal;
}; };
lan = { lan = {
id = 96; id = 96;
interface = config.cloonar-assistant.firewall.interfaces.internal; interface = cfg.internal;
}; };
vserver = { vserver = {
id = 97; id = 97;
interface = config.cloonar-assistant.firewall.interfaces.internal; interface = cfg.internal;
}; };
multimedia = { multimedia = {
id = 99; id = 99;
interface = config.cloonar-assistant.firewall.interfaces.internal; interface = cfg.internal;
}; };
smart = { smart = {
id = 100; id = 100;
interface = config.cloonar-assistant.firewall.interfaces.internal; interface = cfg.internal;
}; };
guest = { guest = {
id = 254; id = 254;
interface = config.cloonar-assistant.firewall.interfaces.internal; interface = cfg.internal;
}; };
}; };
@@ -71,37 +81,37 @@
wan.useDHCP = true; wan.useDHCP = true;
lan = { lan = {
ipv4.addresses = [{ ipv4.addresses = [{
address = "${config.cloonar-assistant.networkPrefix}.96.1"; address = "${networkPrefix}.96.1";
prefixLength = 24; prefixLength = 24;
}]; }];
}; };
server = { server = {
ipv4.addresses = [{ ipv4.addresses = [{
address = "${config.cloonar-assistant.networkPrefix}.97.1"; address = "${networkPrefix}.97.1";
prefixLength = 24; prefixLength = 24;
}]; }];
}; };
infrastructure = { infrastructure = {
ipv4.addresses = [{ ipv4.addresses = [{
address = "${config.cloonar-assistant.networkPrefix}.101.1"; address = "${networkPrefix}.101.1";
prefixLength = 24; prefixLength = 24;
}]; }];
}; };
multimedia = { multimedia = {
ipv4.addresses = [{ ipv4.addresses = [{
address = "${config.cloonar-assistant.networkPrefix}.99.1"; address = "${networkPrefix}.99.1";
prefixLength = 24; prefixLength = 24;
}]; }];
}; };
smart = { smart = {
ipv4.addresses = [{ ipv4.addresses = [{
address = "${config.cloonar-assistant.networkPrefix}.100.1"; address = "${networkPrefix}.100.1";
prefixLength = 24; prefixLength = 24;
}]; }];
}; };
guest = { guest = {
ipv4.addresses = [{ ipv4.addresses = [{
address = "${config.cloonar-assistant.networkPrefix}.254.1"; address = "${networkPrefix}.254.1";
prefixLength = 24; prefixLength = 24;
}]; }];
}; };

View File

@@ -1,10 +1,91 @@
{ config, lib, ... }: { { config, lib, ... }:
networking.wireguard.interfaces = lib.mkIf config.cloonar-assistant.vpn.enable {
wg_cloonar = { with lib;
ips = [ "${config.networkPrefix}.98.1/24" ];
listenPort = 51820; let
privateKeyFile = config.cloonar-assistant.vpn.privateKeyFile; cfg = config.cloonar-assistant.vpn;
peers = config.cloonar-assistant.vpn.clients;
vpn-client-opts =
{ ... }:
{
options = {
name = mkOption {
type = types.str;
example = "myphone";
description = "Name of the VPN client peer.";
};
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.vpn = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable VPN";
};
privateKeyFile = mkOption {
type = with types; nullOr str;
example = "/private/wireguard_private_key";
description = "File pointing to private key as generated by {command}`wg genkey`.";
};
clients = mkOption {
default = [ ];
description = "VPN Clients";
type = with types; listOf (submodule vpn-client-opts);
};
};
config = {
networking.wireguard.interfaces = mkIf cfg.enable {
wg_cloonar = {
ips = [ "${config.cloonar-assistant.networkPrefix}.98.1/24" ];
listenPort = 51820;
privateKeyFile = cfg.privateKeyFile;
peers = cfg.clients;
};
}; };
}; };
} }

View File

@@ -1,95 +1,115 @@
{ config, pkgs, lib, ... }: { { config, lib, pkgs, ... }:
### 1) Make sure we have the tools we need
environment.systemPackages = with pkgs; [
curl
jq
];
users.users.updns-client = { with lib;
isSystemUser = true;
group = "updns-client";
};
users.groups.updns-client = { };
sops.secrets.updns-client = { let
owner = "updns-client"; cfg = config.cloonar-assistant.updns-client;
restartUnits = [ "updns-client.service" ]; in {
}; options.cloonar-assistant.updns-client = {
enable = mkOption {
### 3) Write the checkscript into /etc/external-ip/check.sh (0400, executable) type = types.bool;
environment.etc."updns-client/run.sh".text = lib.mkIf config.cloonar-assistant.updns-client.enable '' default = false;
#!/usr/bin/env bash description = "Enable updns";
set -euo pipefail };
key = mkOption {
# Where our secret lives (encrypted) type = with types; nullOr str;
SECRET=${config.cloonar-assistant.updns-client.secretFile} example = "example";
# Where we record the lastseen IP description = "key for updns";
LAST_IP_FILE=/var/lib/updns-client/last-ip };
secretFile = mkOption {
# Decrypt the API key at runtime type = with types; nullOr str;
API_KEY=$(cat "$SECRET") example = "/private/updns_secret";
description = "File pointing to secret as generated by {command}`wg genpsk`.";
# 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";
User = "updns-client";
}; };
wantedBy = [ "multi-user.target" ];
}; };
### 6) Define the timer (runs at boot + every 5 minutes) config = {
systemd.timers.updns-client = lib.mkIf config.cloonar-assistant.updns-client.enable { environment.systemPackages = with pkgs; [
description = "Run updns-client.service every 5 minutes"; curl
wantedBy = [ "timers.target" ]; jq
timerConfig = { ];
OnBootSec = "1min";
OnUnitActiveSec = "5min"; users.users.updns-client = {
Persistent = true; isSystemUser = true;
Unit = "updns-client.service"; group = "updns-client";
};
users.groups.updns-client = { };
sops.secrets.updns-client = {
owner = "updns-client";
restartUnits = [ "updns-client.service" ];
};
environment.etc."updns-client/run.sh".text = mkIf cfg.enable ''
#!/usr/bin/env bash
set -euo pipefail
# Where our secret lives (encrypted)
SECRET=${cfg.secretFile}
# Where we record the lastseen 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 \"${cfg.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";
systemd.tmpfiles.rules = [
"d /var/lib/updns-client 0755 root root -"
];
systemd.services.updns-client = mkIf cfg.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";
User = "updns-client";
};
wantedBy = [ "multi-user.target" ];
};
systemd.timers.updns-client = mkIf cfg.enable {
description = "Run updns-client.service every 5 minutes";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "1min";
OnUnitActiveSec = "5min";
Persistent = true;
Unit = "updns-client.service";
};
}; };
}; };
} }

145
scripts/run-vm Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -Euo pipefail
# -----------------------------------------------------------------------------
# This script sets up and launches a QEMU virtual machine with OVMF (UEFI).
# It checks for the necessary files, creates directories/images as needed,
# and provides clear, user-friendly output along the way.
# Usage:
# ./run-vm.sh [install]
# - Pass "install" to attach the ISO as a CD-ROM for installation.
# -----------------------------------------------------------------------------
# Paths to OVMF firmware (pflash)
OVMF_CODE="/run/libvirt/nix-ovmf/OVMF_CODE.fd"
OVMF_VARS_DEFAULT="/run/libvirt/nix-ovmf/OVMF_VARS.fd"
# Determine where this script lives and compute related paths
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
TARGET_DIR=$(readlink -f "$SCRIPT_DIR/../.vm")
OVMF_VARS_PATH=$(readlink -f "$SCRIPT_DIR/../.vm/OVMF_VARS-myvm.fd")
IMG_PATH=$(readlink -f "$SCRIPT_DIR/../.vm/disk.img")
ISO_DIR=$(readlink -f "$SCRIPT_DIR/../iso/result/iso")
echo
echo "============================================================"
echo " QEMU VM Setup and Launch Script"
echo "============================================================"
echo
# -----------------------------------------------------------------------------
# 1. Locate the ISO file
# -----------------------------------------------------------------------------
echo "[1/6] Locating ISO file in: $ISO_DIR"
ISO_FILE=$(ls "$ISO_DIR"/*.iso 2>/dev/null | head -n 1 || true)
if [ -z "$ISO_FILE" ]; then
echo " ❌ Error: No ISO found in $ISO_DIR"
echo " Please verify the directory and try again."
exit 1
else
echo " ✅ Found ISO: $ISO_FILE"
fi
echo
# -----------------------------------------------------------------------------
# 2. Ensure target VM directory exists
# -----------------------------------------------------------------------------
echo "[2/6] Checking VM target directory: $TARGET_DIR"
if [ ! -d "$TARGET_DIR" ]; then
echo " 📁 Directory does not exist; creating: $TARGET_DIR"
mkdir -p "$TARGET_DIR"
echo " ✅ Directory created."
else
echo " ✅ Directory already exists."
fi
echo
# -----------------------------------------------------------------------------
# 3. Prepare OVMF variables file (OVMF_VARS-myvm.fd)
# -----------------------------------------------------------------------------
echo "[3/6] Preparing OVMF variables file"
if [ ! -f "$OVMF_VARS_PATH" ]; then
echo " Copying default vars to: $OVMF_VARS_PATH"
cp "$OVMF_VARS_DEFAULT" "$OVMF_VARS_PATH"
chmod 600 "$OVMF_VARS_PATH"
echo " ✅ OVMF_VARS-myvm.fd created and permissions set."
else
echo " ✅ OVMF_VARS-myvm.fd already exists."
fi
echo
# -----------------------------------------------------------------------------
# 4. Create QCOW2 disk image if missing
# -----------------------------------------------------------------------------
echo "[4/6] Ensuring QCOW2 disk image exists at: $IMG_PATH"
if [ ! -f "$IMG_PATH" ]; then
echo " 💾 Creating a new 64G QCOW2 image..."
qemu-img create -f qcow2 "$IMG_PATH" "64G"
echo " ✅ Disk image created: $IMG_PATH (64G)"
else
echo " ✅ Disk image already exists."
fi
echo
# -----------------------------------------------------------------------------
# 5. Parse arguments (install mode or normal boot)
# -----------------------------------------------------------------------------
INSTALL_MODE=0
if [ "${1-}" = "install" ]; then
INSTALL_MODE=1
fi
if [ "$INSTALL_MODE" -eq 1 ]; then
echo "[5/6] Install mode enabled: CD-ROM will be attached"
CDROM_OPTS="-drive file=\"$ISO_FILE\",format=raw,if=none,media=cdrom,id=cd1,readonly=on -device ahci,id=ahci0 -device ide-cd,bus=ahci0.0,drive=cd1,bootindex=1"
else
echo "[5/6] Normal boot mode: No CD-ROM attached"
CDROM_OPTS=""
fi
echo
# -----------------------------------------------------------------------------
# 6. Launch QEMU
# -----------------------------------------------------------------------------
echo "[6/6] Launching QEMU VM now..."
echo "------------------------------------------------------------"
echo " • Machine: q35, KVM acceleration"
echo " • Memory: 4096 MB"
echo " • CPU: host"
echo " • OVMF_CODE (readonly): $OVMF_CODE"
echo " • OVMF_VARS: $OVMF_VARS_PATH"
echo " • Disk image: $IMG_PATH"
if [ "$INSTALL_MODE" -eq 1 ]; then
echo " • CD-ROM (ISO): $ISO_FILE"
fi
echo " • Networking: user-mode with hostfwd ssh (host:2222 → guest:22)"
echo " • Connect to VM via: ssh -p 2222 root@localhost"
echo " • VGA: virtio"
echo "------------------------------------------------------------"
echo
# Construct network options
NET_OPTS="-netdev user,id=net0,hostfwd=tcp::2222-:22 -device e1000,netdev=net0"
# Run QEMU using eval to allow variable expansion in CDROM_OPTS
eval qemu-system-x86_64 \
-machine type=q35,accel=kvm \
-m 4096 \
-cpu host \
\
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
\
-drive if=pflash,format=raw,file="$OVMF_VARS_PATH" \
\
-drive file="$IMG_PATH",format=qcow2 \
\
$CDROM_OPTS \
\
$NET_OPTS \
-vga virtio
echo
echo "============================================================"
echo " QEMU VM has exited"
echo "============================================================"

60
scripts/test-configuration Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -Euo pipefail
VERBOSE=false
SHOW_TRACE_OPT=""
# Parse options
#
if [ "$#" -gt 0 ]; then
if [[ "$1" == "-v" || "$1" == "--verbose" ]]; then
VERBOSE=true
SHOW_TRACE_OPT="--show-trace"
shift # Remove the verbose flag from arguments
fi
fi
# Check if 'nixos-rebuild' command is available
if ! command -v nixos-rebuild > /dev/null; then
echo "ERROR: 'nixos-rebuild' command not found. Please ensure it is installed and in your PATH." >&2
exit 1
fi
# Determine the absolute directory where the script itself is located
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
# Construct the absolute path to the configuration file
# and resolve it to a canonical path
CONFIG_PATH=$(readlink -f "$SCRIPT_DIR/../example/configuration.nix")
CHANNEL=$(cat "$SCRIPT_DIR/../example/channel")
# Verify that the CONFIG_PATH exists and is a regular file
if [ ! -f "$CONFIG_PATH" ]; then
echo "ERROR: Configuration file not found at '$CONFIG_PATH'." >&2
exit 1
fi
echo "INFO: Attempting dry-build for using configuration '$CONFIG_PATH'..."
if [ "$VERBOSE" = true ]; then
echo "INFO: Verbose mode enabled, --show-trace will be used."
fi
# Execute nixos-rebuild dry-build
# Store the output and error streams, and the exit code
NIX_OUTPUT_ERR=$(nixos-rebuild dry-build $SHOW_TRACE_OPT -I nixpkgs=$CHANNEL/nixexprs.tar.xz -I nixos-config="$CONFIG_PATH" 2>&1)
NIX_EXIT_STATUS=$?
# Check the exit status
if [ "$NIX_EXIT_STATUS" -eq 0 ]; then
echo "INFO: Dry-build for completed successfully."
if [ "$VERBOSE" = true ]; then
echo "Output from nixos-rebuild:"
echo "$NIX_OUTPUT_ERR"
fi
exit 0
else
echo "ERROR: Dry-build for failed. 'nixos-rebuild' exited with status $NIX_EXIT_STATUS." >&2
echo "Output from nixos-rebuild:" >&2
echo "$NIX_OUTPUT_ERR" >&2
exit "$NIX_EXIT_STATUS"
fi