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

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,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}
}
'';
};
};
};
};

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 {
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;
};
};
};
}

View File

@@ -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 checkscript 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 lastseen IP
LAST_IP_FILE=/var/lib/updns-client/last-ip
# Decrypt the API key at runtime
API_KEY=$(cat "$SECRET")
# Fetch current external IP
IP=$(curl -fsSL https://ifconfig.me)
# Ensure state directory exists
mkdir -p "$(dirname \"$LAST_IP_FILE\")"
# Read old IP (if any)
if [[ -f "$LAST_IP_FILE" ]]; then
OLD_IP=$(< "$LAST_IP_FILE")
else
OLD_IP=""
fi
# If it's changed, notify the API and update the file
if [[ "$IP" != "$OLD_IP" ]]; then
PAYLOAD=$(jq -n \
--arg key \"${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 lastseen IP
LAST_IP_FILE=/var/lib/updns-client/last-ip
# Decrypt the API key at runtime
API_KEY=$(cat "$SECRET")
# Fetch current external IP
IP=$(curl -fsSL https://ifconfig.me)
# Ensure state directory exists
mkdir -p "$(dirname \"$LAST_IP_FILE\")"
# Read old IP (if any)
if [[ -f "$LAST_IP_FILE" ]]; then
OLD_IP=$(< "$LAST_IP_FILE")
else
OLD_IP=""
fi
# If it's changed, notify the API and update the file
if [[ "$IP" != "$OLD_IP" ]]; then
PAYLOAD=$(jq -n \
--arg key \"${cfg.key}" \
--arg secret "$SECRET" \
--arg host "${config.cloonar-assistant.domain}" \
--arg ip "$IP" \
'{key: $key, secret: $secret, host: $host, ip: $ip}')
curl -fsS -X POST https://updns-client.cloonar.com/update \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
echo "$IP" > "$LAST_IP_FILE"
fi
'';
environment.etc."updns-client/run.sh".mode = "0500";
systemd.tmpfiles.rules = [
"d /var/lib/updns-client 0755 root root -"
];
systemd.services.updns-client = mkIf cfg.enable {
description = "Check external IP and notify API on change";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
WorkingDirectory = "/var/lib/updns-client";
ExecStart = "${pkgs.bash}/bin/bash /etc/updns-client/run.sh";
User = "updns-client";
};
wantedBy = [ "multi-user.target" ];
};
systemd.timers.updns-client = mkIf cfg.enable {
description = "Run updns-client.service every 5 minutes";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "1min";
OnUnitActiveSec = "5min";
Persistent = true;
Unit = "updns-client.service";
};
};
};
}