refactor: many changes
This commit is contained in:
10
.sops.yaml
Normal file
10
.sops.yaml
Normal 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
58
example/configuration.nix
Normal 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
|
||||||
|
}
|
||||||
46
example/hardware-configuration.nix
Normal file
46
example/hardware-configuration.nix
Normal 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
34
example/secrets.yaml
Normal 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
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
{ ... }: {
|
{ ... }: {
|
||||||
networking.hostName = "fw";
|
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
./interfaces.nix
|
./interfaces.nix
|
||||||
./dhcp.nix
|
./dhcp.nix
|
||||||
|
|||||||
@@ -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}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}];
|
}];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 check‐script 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 last‐seen 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 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";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
145
scripts/run-vm
Executable file
145
scripts/run-vm
Executable 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
60
scripts/test-configuration
Executable 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
|
||||||
Reference in New Issue
Block a user