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, ... }: {
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.systemd-boot = {
|
||||
enable = true;
|
||||
configurationLimit = 5;
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-label/boot";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{ ... }: {
|
||||
networking.hostName = "fw";
|
||||
|
||||
imports = [
|
||||
./interfaces.nix
|
||||
./dhcp.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}
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
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