diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..b7d94e4 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,10 @@ +keys: + - &dominik age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + - &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch + +creation_rules: + - path_regex: example/[^/]+\.yaml$ + key_groups: + - age: + - *dominik + - *dominik2 diff --git a/example/configuration.nix b/example/configuration.nix new file mode 100644 index 0000000..96b6dec --- /dev/null +++ b/example/configuration.nix @@ -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 +} diff --git a/example/hardware-configuration.nix b/example/hardware-configuration.nix new file mode 100644 index 0000000..48a36a8 --- /dev/null +++ b/example/hardware-configuration.nix @@ -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; +} diff --git a/example/secrets.yaml b/example/secrets.yaml new file mode 100644 index 0000000..4b15b2f --- /dev/null +++ b/example/secrets.yaml @@ -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 diff --git a/iso/hardware-configuration.nix b/iso/hardware-configuration.nix index ee3a8b6..8b73b01 100644 --- a/iso/hardware-configuration.nix +++ b/iso/hardware-configuration.nix @@ -1,5 +1,8 @@ { config, pkgs, ... }: { - boot.loader.systemd-boot.enable = true; + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 5; + }; fileSystems."/boot" = { device = "/dev/disk/by-label/boot"; diff --git a/modules/cloonar-assistant/default.nix b/modules/cloonar-assistant/default.nix index bb97c25..b6086d2 100644 --- a/modules/cloonar-assistant/default.nix +++ b/modules/cloonar-assistant/default.nix @@ -4,186 +4,31 @@ with lib; let 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 { options.cloonar-assistant = { - setup = lib.mkOption { + setup = mkOption { type = lib.types.bool; default = false; description = "Enable access from Wan to Setup"; }; - networkPrefix = lib.mkOption { - type = lib.types.str; + networkPrefix = mkOption { + type = types.str; default = "10.10"; example = "10.10"; description = "First two octets of the network"; }; - domain = lib.mkOption { - type = lib.types.str; + domain = mkOption { + type = types.str; example = "example.smart.cloonar.com"; 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 = { - enable = lib.mkOption { - type = lib.types.bool; + enable = mkOption { + type = types.bool; default = false; 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 = [ # Include the results of the hardware scan. diff --git a/modules/cloonar-assistant/home-assistant/default.nix b/modules/cloonar-assistant/home-assistant/default.nix index f233cbf..1e4e5d4 100644 --- a/modules/cloonar-assistant/home-assistant/default.nix +++ b/modules/cloonar-assistant/home-assistant/default.nix @@ -1,4 +1,7 @@ { config, lib, pkgs, ... }: + +with lib; + let domain = config.cloonar-assistant.domain; pkgs-with-home-assistant = import (builtins.fetchGit { @@ -12,7 +15,7 @@ let "backup" ]; - ha-config = lib.recursiveUpdate config.services.home-assistant.config { + ha-config = recursiveUpdate config.services.home-assistant.config { recorder = { db_url = "mysql://hass@localhost/hass?unix_socket=/var/run/mysqld/mysqld.sock"; }; @@ -72,6 +75,8 @@ let gid = config.ids.gids.hass; in { + services.home-assistant.config = mkDefault { }; + users.users.hass = { home = "/var/lib/hass"; createHome = true; diff --git a/modules/cloonar-assistant/networking/default.nix b/modules/cloonar-assistant/networking/default.nix index ae5942d..94eff10 100644 --- a/modules/cloonar-assistant/networking/default.nix +++ b/modules/cloonar-assistant/networking/default.nix @@ -1,6 +1,4 @@ { ... }: { - networking.hostName = "fw"; - imports = [ ./interfaces.nix ./dhcp.nix diff --git a/modules/cloonar-assistant/networking/firewall.nix b/modules/cloonar-assistant/networking/firewall.nix index 09ea055..fab67dd 100644 --- a/modules/cloonar-assistant/networking/firewall.nix +++ b/modules/cloonar-assistant/networking/firewall.nix @@ -1,159 +1,231 @@ { config, lib, pkgs, ... }: -let - forward-chain = '' +with lib; + +let + cfg = config.cloonar-assistant.firewall; + networkPrefix = config.cloonar-assistant.networkPrefix; + + 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; - } + options.cloonar-assistant.firewall = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable firewall"; + }; + ipv4 = mkOption { + type = types.bool; + default = true; + description = "Enable ipv4"; + }; + 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 { - 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 - } + config = { + 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 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 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-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.setup '' - iifname "wan" 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" - ''} + 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 + } - 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 - ${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 - ''} + iifname "wan" icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP" - # Allow all returning traffic - ct state { established, related } counter accept + # Accept mDNS for avahi reflection + ${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 - 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" + # Allow all returning traffic + ct state { established, related } counter accept - ${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 { - 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} - ''} + chain forward { + type filter hook forward priority filter; policy drop; - # allow all established, related - ct state { established, related } accept comment "Allow established traffic" + ${lib.optionalString config.cloonar-assistant.vpn.enable '' + iifname "wg_cloonar" counter accept comment "test wireguard" + iifname "wg_cloonar" oifname lo counter accept comment "wireguard to server" + ''} - # Allow trusted network WAN access - ${lib.optionalString config.cloonar-assistant.firewall.enable '' - iifname { - "lan", - "infrastructure", - "server", - "vserver", - "multimedia", - "smart", - "wg_cloonar", - "podman*", - "guest", - "setup", - } oifname { - "wan", - } counter accept comment "Allow trusted LAN to WAN" - ''} + # enable flow offloading for better throughput + # ip protocol { tcp, udp } flow offload @f - 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.cloonar-assistant.networkPrefix}.96.255 udp dport { 9 } dnat to ${config.cloonar-assistant.networkPrefix}.96.255 - ${config.cloonar-assistant.firewall.custom-rules.prerouting} - } + # allow home assistant to wan + iifname "ve-hass" oifname wan counter accept comment "Allow home assistant to WAN" - # Setup NAT masquerading on external interfaces - chain postrouting { - type nat hook postrouting priority filter; policy accept; - oifname { "wan" } masquerade + # 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 ${networkPrefix}.97.20/32 oifname { "lan" } counter accept + ''} - ${lib.optionalString config.cloonar-assistant.vpn.enable '' - oifname { "wg_cloonar" } masquerade - ''} + ${lib.optionalString config.cloonar-assistant.firewall.enable '' + # 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} + } + ''; + }; }; }; }; diff --git a/modules/cloonar-assistant/networking/interfaces.nix b/modules/cloonar-assistant/networking/interfaces.nix index c117d57..030d5f4 100644 --- a/modules/cloonar-assistant/networking/interfaces.nix +++ b/modules/cloonar-assistant/networking/interfaces.nix @@ -1,5 +1,11 @@ { config, lib, ... }: -{ + +with lib; + +let + cfg = config.cloonar-assistant.firewall.interfaces; + networkPrefix = config.cloonar-assistant.networkPrefix; +in { boot.kernel.sysctl = { # if you use ipv4, this is all you need "net.ipv4.conf.all.forwarding" = true; @@ -12,9 +18,13 @@ wait-online.anyInterface = true; links = { "10-wan" = { - matchConfig.Name = config.cloonar-assistant.firewall.interfaces.wan; + matchConfig.Name = cfg.wan; linkConfig.Name = "wan"; }; + "20-internal" = mkIf (cfg.internal != null) { + matchConfig.Name = cfg.internal; + linkConfig.Name = "internal"; + }; }; netdevs = { "30-server".netdevConfig = { @@ -34,32 +44,32 @@ networking = if config.cloonar-assistant.firewall.enable then { useDHCP = false; # Define VLANS - nameservers = [ "${config.cloonar-assistant.networkPrefix}.97.1" ]; + nameservers = [ "${networkPrefix}.97.1" ]; # resolvconf.enable = false; - vlans = { + vlans = mkIf (cfg.internal != null) { infrastructure = { id = 101; - interface = config.cloonar-assistant.firewall.interfaces.internal; + interface = cfg.internal; }; lan = { id = 96; - interface = config.cloonar-assistant.firewall.interfaces.internal; + interface = cfg.internal; }; vserver = { id = 97; - interface = config.cloonar-assistant.firewall.interfaces.internal; + interface = cfg.internal; }; multimedia = { id = 99; - interface = config.cloonar-assistant.firewall.interfaces.internal; + interface = cfg.internal; }; smart = { id = 100; - interface = config.cloonar-assistant.firewall.interfaces.internal; + interface = cfg.internal; }; guest = { id = 254; - interface = config.cloonar-assistant.firewall.interfaces.internal; + interface = cfg.internal; }; }; @@ -71,37 +81,37 @@ wan.useDHCP = true; lan = { ipv4.addresses = [{ - address = "${config.cloonar-assistant.networkPrefix}.96.1"; + address = "${networkPrefix}.96.1"; prefixLength = 24; }]; }; server = { ipv4.addresses = [{ - address = "${config.cloonar-assistant.networkPrefix}.97.1"; + address = "${networkPrefix}.97.1"; prefixLength = 24; }]; }; infrastructure = { ipv4.addresses = [{ - address = "${config.cloonar-assistant.networkPrefix}.101.1"; + address = "${networkPrefix}.101.1"; prefixLength = 24; }]; }; multimedia = { ipv4.addresses = [{ - address = "${config.cloonar-assistant.networkPrefix}.99.1"; + address = "${networkPrefix}.99.1"; prefixLength = 24; }]; }; smart = { ipv4.addresses = [{ - address = "${config.cloonar-assistant.networkPrefix}.100.1"; + address = "${networkPrefix}.100.1"; prefixLength = 24; }]; }; guest = { ipv4.addresses = [{ - address = "${config.cloonar-assistant.networkPrefix}.254.1"; + address = "${networkPrefix}.254.1"; prefixLength = 24; }]; }; diff --git a/modules/cloonar-assistant/networking/wireguard.nix b/modules/cloonar-assistant/networking/wireguard.nix index a356fef..2528141 100644 --- a/modules/cloonar-assistant/networking/wireguard.nix +++ b/modules/cloonar-assistant/networking/wireguard.nix @@ -1,10 +1,91 @@ -{ config, lib, ... }: { - networking.wireguard.interfaces = lib.mkIf config.cloonar-assistant.vpn.enable { - wg_cloonar = { - ips = [ "${config.networkPrefix}.98.1/24" ]; - listenPort = 51820; - privateKeyFile = config.cloonar-assistant.vpn.privateKeyFile; - peers = config.cloonar-assistant.vpn.clients; +{ config, lib, ... }: + +with lib; + +let + cfg = config.cloonar-assistant.vpn; + + 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; + }; }; }; } diff --git a/modules/cloonar-assistant/updns/default.nix b/modules/cloonar-assistant/updns/default.nix index 811fb17..8d5ad64 100644 --- a/modules/cloonar-assistant/updns/default.nix +++ b/modules/cloonar-assistant/updns/default.nix @@ -1,95 +1,115 @@ -{ config, pkgs, lib, ... }: { - ### 1) Make sure we have the tools we need - environment.systemPackages = with pkgs; [ - curl - jq - ]; +{ config, lib, pkgs, ... }: - users.users.updns-client = { - isSystemUser = true; - group = "updns-client"; - }; - users.groups.updns-client = { }; +with lib; - sops.secrets.updns-client = { - 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 '' - #!/usr/bin/env bash - set -euo pipefail - - # Where our secret lives (encrypted) - SECRET=${config.cloonar-assistant.updns-client.secretFile} - # 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"; - User = "updns-client"; +let + cfg = config.cloonar-assistant.updns-client; +in { + options.cloonar-assistant.updns-client = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable updns"; + }; + key = mkOption { + type = with types; nullOr str; + example = "example"; + description = "key for updns"; + }; + secretFile = mkOption { + type = with types; nullOr str; + example = "/private/updns_secret"; + description = "File pointing to secret as generated by {command}`wg genpsk`."; }; - 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"; + config = { + environment.systemPackages = with pkgs; [ + curl + jq + ]; + + users.users.updns-client = { + isSystemUser = true; + 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 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 \"${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"; + }; }; }; } diff --git a/scripts/run-vm b/scripts/run-vm new file mode 100755 index 0000000..f3d1850 --- /dev/null +++ b/scripts/run-vm @@ -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 "============================================================" diff --git a/scripts/test-configuration b/scripts/test-configuration new file mode 100755 index 0000000..e73d293 --- /dev/null +++ b/scripts/test-configuration @@ -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