refactor: many changes

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

10
.sops.yaml Normal file
View File

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

58
example/configuration.nix Normal file
View File

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

View File

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

34
example/secrets.yaml Normal file
View File

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

View File

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

View File

@@ -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.

View File

@@ -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;

View File

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

View File

@@ -1,9 +1,81 @@
{ config, lib, pkgs, ... }:
let
forward-chain = ''
with lib;
let
cfg = config.cloonar-assistant.firewall;
networkPrefix = config.cloonar-assistant.networkPrefix;
forward-chain = ''
'';
in {
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";
};
};
};
config = {
networking = {
firewall.checkReversePath = false;
nat.enable = false;
@@ -34,26 +106,25 @@ in {
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 ''
${optionalString config.cloonar-assistant.setup ''
iifname "wan" accept
''}
${lib.optionalString config.cloonar-assistant.vpn.enable ''
${optionalString config.cloonar-assistant.vpn.enable ''
iifname "wan" udp dport 51820 counter accept comment "Wireguard traffic"
''}
${lib.optionalString config.cloonar-assistant.firewall.enable ''
${optionalString cfg.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 { "multimedia" } icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
''}
iifname { "wan", "multimedia" } icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP"
iifname "wan" 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
${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 all returning traffic
@@ -64,7 +135,7 @@ in {
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}
${cfg.custom-rules.input}
limit rate 60/minute burst 100 packets log prefix "Input - Drop: " comment "Log any unmatched traffic"
}
@@ -73,9 +144,9 @@ in {
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" counter accept comment "test wireguard"
iifname "wg_cloonar" oifname lo counter accept comment "wireguard to server"
''}
@@ -91,20 +162,20 @@ in {
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
iifname "server" ip saddr ${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
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
# 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}
${cfg.custom-rules.forward}
''}
@@ -112,7 +183,7 @@ in {
ct state { established, related } accept comment "Allow established traffic"
# Allow trusted network WAN access
${lib.optionalString config.cloonar-assistant.firewall.enable ''
${lib.optionalString cfg.enable ''
iifname {
"lan",
"infrastructure",
@@ -138,8 +209,8 @@ in {
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}
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
@@ -151,11 +222,12 @@ in {
oifname { "wg_cloonar" } masquerade
''}
${config.cloonar-assistant.firewall.custom-rules.postrouting}
${cfg.custom-rules.postrouting}
}
'';
};
};
};
};
};
}

View File

@@ -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;
}];
};

View File

@@ -1,10 +1,91 @@
{ config, lib, ... }: {
networking.wireguard.interfaces = lib.mkIf config.cloonar-assistant.vpn.enable {
{ 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.networkPrefix}.98.1/24" ];
ips = [ "${config.cloonar-assistant.networkPrefix}.98.1/24" ];
listenPort = 51820;
privateKeyFile = config.cloonar-assistant.vpn.privateKeyFile;
peers = config.cloonar-assistant.vpn.clients;
privateKeyFile = cfg.privateKeyFile;
peers = cfg.clients;
};
};
};
}

View File

@@ -1,5 +1,29 @@
{ config, pkgs, lib, ... }: {
### 1) Make sure we have the tools we need
{ config, lib, pkgs, ... }:
with lib;
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`.";
};
};
config = {
environment.systemPackages = with pkgs; [
curl
jq
@@ -16,13 +40,12 @@
restartUnits = [ "updns-client.service" ];
};
### 3) Write the checkscript into /etc/external-ip/check.sh (0400, executable)
environment.etc."updns-client/run.sh".text = lib.mkIf config.cloonar-assistant.updns-client.enable ''
environment.etc."updns-client/run.sh".text = mkIf cfg.enable ''
#!/usr/bin/env bash
set -euo pipefail
# Where our secret lives (encrypted)
SECRET=${config.cloonar-assistant.updns-client.secretFile}
SECRET=${cfg.secretFile}
# Where we record the lastseen IP
LAST_IP_FILE=/var/lib/updns-client/last-ip
@@ -46,7 +69,7 @@
if [[ "$IP" != "$OLD_IP" ]]; then
PAYLOAD=$(jq -n \
--arg key \"${config.cloonar-assistant.updns-client.key}" \
--arg key \"${cfg.key}" \
--arg secret "$SECRET" \
--arg host "${config.cloonar-assistant.domain}" \
--arg ip "$IP" \
@@ -61,14 +84,11 @@
'';
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 {
systemd.services.updns-client = mkIf cfg.enable {
description = "Check external IP and notify API on change";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
@@ -81,8 +101,7 @@
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 {
systemd.timers.updns-client = mkIf cfg.enable {
description = "Run updns-client.service every 5 minutes";
wantedBy = [ "timers.target" ];
timerConfig = {
@@ -92,4 +111,5 @@
Unit = "updns-client.service";
};
};
};
}

145
scripts/run-vm Executable file
View File

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

60
scripts/test-configuration Executable file
View File

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