diff --git a/modules/cloonar-assistant/default.nix b/modules/cloonar-assistant/default.nix index aa1d3c3..85bd894 100644 --- a/modules/cloonar-assistant/default.nix +++ b/modules/cloonar-assistant/default.nix @@ -78,6 +78,11 @@ in { 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 { diff --git a/modules/cloonar-assistant/home-assistant/default.nix b/modules/cloonar-assistant/home-assistant/default.nix index 8ec44af..42f1557 100644 --- a/modules/cloonar-assistant/home-assistant/default.nix +++ b/modules/cloonar-assistant/home-assistant/default.nix @@ -1,34 +1,51 @@ { config, pkgs, ... }: let - domain = "home-assistant.${config.cloonar-assistant.domain}"; + domain = config.cloonar-assistant.domain; pkgs-with-home-assistant = import (builtins.fetchGit { name = "new-home-assistant"; url = "https://github.com/nixos/nixpkgs/"; rev = "18dd725c29603f582cf1900e0d25f9f1063dbf11"; }) {}; - networkPrefix = config.networkPrefix; + home-assistant-config = config.home-assistant; home-assistant-config.package = pkgs-with-home-assistant.home-assistant; - - - certDir = "/var/lib/ssl/home-assistant"; - certFile = "${certDir}/selfsigned.crt"; - keyFile = "${certDir}/selfsigned.key"; + uid = config.ids.uids.hass; + gid = config.ids.gids.hass; in { users.users.hass = { home = "/var/lib/hass"; createHome = true; group = "hass"; - uid = config.ids.uids.hass; + uid = uid; extraGroups = [ "dialout" ]; }; - users.groups.hass.gid = config.ids.gids.hass; + users.groups.hass.gid = gid; + + services.nginx.enable = true; + services.nginx.virtualHosts."${domain}" = { + forceSSL = true; + extraConfig = '' + proxy_buffering off; + ''; + locations."/".extraConfig = '' + proxy_pass http://10.233.0.2:8123; + proxy_set_header Host $host; + proxy_redirect http:// https://; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + ''; + }; security.acme.certs."${domain}" = { - group = "nginx"; + group = "ssl-users"; + allowKeysForGroup = true; }; + users.groups.ssl-users = {}; + sops.secrets."home-assistant-secrets.yaml" = { owner = "hass"; restartUnits = [ "container@hass.service" ]; @@ -38,13 +55,8 @@ in autoStart = true; ephemeral = false; privateNetwork = true; - hostBridge = "server"; - hostAddress = "${networkPrefix}.97.1"; - localAddress = "${networkPrefix}.97.20/24"; - extraFlags = [ - "--capability=CAP_NET_ADMIN" - "--capability=CAP_MKNOD" - ]; + hostAddress = "10.233.0.1"; + localAddress = "10.233.0.2"; bindMounts = { "/etc/localtime" = { hostPath = "/etc/localtime"; @@ -53,85 +65,18 @@ in hostPath = "/var/lib/hass/"; isReadOnly = false; }; - "/var/lib/acme/hass/" = { - hostPath = "${config.security.acme.certs.${domain}.directory}"; - }; "/var/lib/hass/secrets.yaml" = { hostPath = config.sops.secrets."home-assistant-secrets.yaml".path; }; }; config = { lib, config, pkgs, ... }: { - networkPrefix = networkPrefix; imports = [ ]; - networking = { - hostName = "home-assistant"; - useHostResolvConf = false; - defaultGateway = { - address = "${networkPrefix}.96.1"; - interface = "eth0"; - }; - firewall.enable = false; - nameservers = [ "${networkPrefix}.97.1" ]; - }; - environment.systemPackages = [ pkgs.mariadb ]; - systemd.services.generate-selfsigned-cert = { - description = "Generate/renew self-signed SSL certificate"; - wantedBy = [ "nginx.service" ]; - path = [ pkgs.openssl pkgs.gnugrep ]; - - script = '' - if [ -f ${certFile} ]; then - expiry=$(openssl x509 -enddate -noout -in ${certFile} | cut -d= -f2) - expiry_epoch=$(date -d "$expiry" +%s) - current_epoch=$(date +%s) - days_left=$(( (expiry_epoch - current_epoch) / 86400 )) - - if [ $days_left -lt 30 ]; then # Regenerate if expiring in <30 days - echo "Certificate expiring soon, regenerating..." - rm ${certFile} ${keyFile} - fi - fi - - if [ ! -f ${certFile} ] || [ ! -f ${keyFile} ]; then - openssl req -x509 -nodes -days 365 \ - -newkey rsa:2048 \ - -keyout ${keyFile} \ - -out ${certFile} \ - -subj "/CN=${domain}" - fi - ''; - - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - }; - - services.nginx.enable = true; - services.nginx.virtualHosts."${domain}" = { - sslCertificate = certFile; - sslCertificateKey = keyFile; - forceSSL = true; - extraConfig = '' - proxy_buffering off; - ''; - locations."/".extraConfig = '' - proxy_pass http://127.0.0.1:8123; - proxy_set_header Host $host; - proxy_redirect http:// https://; - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - ''; - }; - services.home-assistant = home-assistant-config; services.home-assistant.extraComponents = [ @@ -243,6 +188,7 @@ in }; users.users.hass.extraGroups = [ "dialout" ]; + networking.firewall.allowedTCPPorts = [ 8123 ]; system.stateVersion = "23.05"; }; }; diff --git a/modules/cloonar-assistant/networking/default.nix b/modules/cloonar-assistant/networking/default.nix index d71cc2a..f41957b 100644 --- a/modules/cloonar-assistant/networking/default.nix +++ b/modules/cloonar-assistant/networking/default.nix @@ -3,6 +3,7 @@ ./interfaces.nix ./dhcp.nix ./firewall.nix + ./unbound.nix ./wireguard.nix ]; } diff --git a/modules/cloonar-assistant/networking/firewall.nix b/modules/cloonar-assistant/networking/firewall.nix index 2cb4aa1..a502d24 100644 --- a/modules/cloonar-assistant/networking/firewall.nix +++ b/modules/cloonar-assistant/networking/firewall.nix @@ -77,6 +77,9 @@ in { # 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 @@ -92,7 +95,7 @@ in { iifname "smart" oifname "server" ip daddr ${config.networkPrefix}.97.20/32 tcp dport { 1883 } counter accept # lan and vpn to any - iifname { "lan", "server", "vserver", "wg_cloonar" } oifname { "lan", "vb-*", "vm-*", "server", "vserver", "infrastructure", "multimedia", "smart", "wg_cloonar", "guest", "setup" } counter accept + 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" ''} @@ -114,8 +117,6 @@ in { "podman*", "guest", "setup", - "vb-*", - "vm-*", } oifname { "wan", } counter accept comment "Allow trusted LAN to WAN" diff --git a/modules/cloonar-assistant/networking/unbound.nix b/modules/cloonar-assistant/networking/unbound.nix new file mode 100644 index 0000000..de30531 --- /dev/null +++ b/modules/cloonar-assistant/networking/unbound.nix @@ -0,0 +1,142 @@ +{ config, ... }: { + +} + +{ config, pkgs, ... }: +let + cfg = { + remote-control.control-enable = true; + server = { + interface = [ "0.0.0.0" "::0" ]; + interface-automatic = "yes"; + access-control = [ + "127.0.0.0/8 allow" + "${config.networkPrefix}.96.0/24 allow" + "${config.networkPrefix}.97.0/24 allow" + "${config.networkPrefix}.98.0/24 allow" + "${config.networkPrefix}.99.0/24 allow" + "${config.networkPrefix}.101.0/24 allow" + "0.0.0.0/0 allow" + ]; + tls-cert-bundle = "/etc/ssl/certs/ca-certificates.crt"; + local-zone = "\"${config.cloonar-assistant.domain}\" transparent"; + local-data = [ + "\"localhost A 127.0.0.1\"" + "\"localhost.${config.cloonar-assistant.domain} A 127.0.0.1\"" + "\"localhost AAAA ::1\"" + "\"localhost.${config.cloonar-assistant.domain} AAAA ::1\"" + "\"fw.${config.cloonar-assistant.domain} A ${config.networkPrefix}.97.1\"" + "\"fw A ${config.networkPrefix}.97.1\"" + + "\"mopidy.${config.cloonar-assistant.domain} IN A ${config.networkPrefix}.97.21\"" + "\"snapcast.${config.cloonar-assistant.domain} IN A ${config.networkPrefix}.97.21\"" + "\"home-assistant.${config.cloonar-assistant.domain} IN A ${config.networkPrefix}.97.20\"" + ]; + local-data-ptr = [ + "\"127.0.0.1 localhost\"" + "\"::1 localhost\"" + "\"${config.networkPrefix}.97.1 fw.${config.cloonar-assistant.domain}\"" + "\"${config.networkPrefix}.97.20 home-assistant.${config.cloonar-assistant.domain}\"" + "\"${config.networkPrefix}.97.21 snapcast.${config.cloonar-assistant.domain}\"" + ]; + # ssl-upstream = "yes"; + }; + forward-zone = [ + { + name = "."; + forward-tls-upstream = "yes"; + forward-first = "no"; + forward-addr = [ + "9.9.9.9@853#dns9.quad9.net" + "149.112.112.11@853#dns11.quad9.net" + ]; + } + ]; + }; +in { + users.users.unbound = { + group = "unbound"; + isSystemUser = true; + extraGroups = [ "ssl-users" ]; + }; + users.groups.unbound = { }; + + services.resolved.enable = false; + + services.unbound = { + enable = true; + settings = cfg; + }; + + systemd.services.unbound-sync = lib.mkIf config.cloonar-assistant.firewall.enable { + enable = true; + path = with pkgs; [ unbound inotify-tools ]; + script = '' + function readFile() { + if [[ "''\$2" == "A" ]] ; then + cat "''\$1" | tail -n +2 | while IFS=, read -r address hwaddr client_id valid_lifetime expire subnet_id fqdn_fwd fqdn_rev hostname state user_context + do + echo "''\${address},''\${hostname}" + done + else + cat "''\$1" | tail -n +2 | while IFS=, read -r address duid valid_lifetime expire subnet_id pref_lifetime lease_type iaid prefix_len fqdn_fwd fqdn_rev hostname hwaddr state user_context hwtype hwaddr_source + do + echo "''\${address},''\${hostname}" + done + fi + } + + function readFileUnique() { + readFile "''\$1" ''\$2 | uniq | while IFS=, read -r address hostname + do + if echo "''\${1}" | grep -Eq '.*\.(${config.cloonar-assistant.domain})'; then + echo ''\${hostname} ''\$2 ''\${address} + unbound-control local_data ''\${hostname} ''\$2 ''\${address} > /dev/null 2>&1 + if [[ "''\$2" == "A" ]] ; then + echo ''\${address} | while IFS=. read -r ip0 ip1 ip2 ip3 + do + unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.ip4.arpa. PTR ''\${hostname} > /dev/null 2>&1 + unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.in-addr.arpa. PTR ''\${hostname} > /dev/null 2>&1 + done + fi + else + if [[ "''\$2" == "A" ]] ; then + echo ''\${address} | while IFS=. read -r ip0 ip1 ip2 ip3 + do + if [[ "''\${hostname}" != "" ]]; then + domain=${config.cloonar-assistant.domain} + if [[ "''\${hostname}" != *. ]]; then + unbound-control local_data ''\${hostname}.''\${domain} ''\$2 ''\${address} > /dev/null 2>&1 + else + unbound-control local_data ''\${hostname}''\${domain} ''\$2 ''\${address} > /dev/null 2>&1 + fi + + fi + unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.ip4.arpa. PTR ''\${hostname} > /dev/null 2>&1 + unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.in-addr.arpa. PTR ''\${hostname} > /dev/null 2>&1 + done + fi + fi + done + } + + function syncFile() { + # readFileUnique "''\$1" "''\$2" + while true; do + readFileUnique "''\$1" "''\$2" + sleep 10 + done + } + + syncFile "/var/lib/kea/dhcp4.leases" A & + # syncFile "/var/lib/kea/dhcp6.leases" AAAA & + wait + ''; + wants = [ "network-online.target" "unbound.service" ]; + after = [ "network-online.target" "unbound.service" ]; + partOf = [ "unbound.service" ]; + wantedBy = [ "multi-user.target" ]; + }; + + networking.firewall.allowedUDPPorts = [ 53 5353 ]; +} diff --git a/modules/cloonar-assistant/updns/default.nix b/modules/cloonar-assistant/updns/default.nix index c17f5e1..a89386e 100644 --- a/modules/cloonar-assistant/updns/default.nix +++ b/modules/cloonar-assistant/updns/default.nix @@ -16,7 +16,7 @@ "set -euo pipefail" "" "# Where our secret lives (encrypted)" - "SECRET=${config.sops.secrets.updns-client.path}" + "SECRET=${config.cloonar-assistant.updns-client.secretFile}" "# Where we record the last‐seen IP" "LAST_IP_FILE=/var/lib/updns-client/last-ip" ""