diff --git a/.gitignore b/.gitignore index 537d029..9d21165 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ result* # mdBook /book/* + +/extensions.yaml diff --git a/docs/hp-switch.md b/docs/hp-switch.md index be66b38..17cdb50 100644 --- a/docs/hp-switch.md +++ b/docs/hp-switch.md @@ -51,12 +51,12 @@ vlan 2 tagged 23,24 vlan 6 name public-event vlan 6 qos priority 0 -vlan 6 tagged 21-24 +vlan 6 tagged 13,15,21-24 vlan 7 name weinturm vlan 7 qos priority 1 vlan 7 tagged 21-23 -vlan 7 untagged 1-12,24 +vlan 7 untagged 1-12,13,15,24 vlan 8 name voice vlan 8 qos priority 5 @@ -66,6 +66,9 @@ vlan 8 voice interface ethernet 1-12 enable +interface ethernet 13,15 enable +interface ethernet 13,15 name WLAN + interface ethernet 17,19 enable interface ethernet 17,19 name dect diff --git a/hosts/default.nix b/hosts/default.nix index 40bae16..4960303 100644 --- a/hosts/default.nix +++ b/hosts/default.nix @@ -1,6 +1,6 @@ _inputs: { pbx = { system = "x86_64-linux"; - targetHost = "tel.weinturm.de"; + #targetHost = "tel.weinturm.de"; }; } diff --git a/hosts/pbx/configuration.nix b/hosts/pbx/configuration.nix index 15d1460..1942dff 100644 --- a/hosts/pbx/configuration.nix +++ b/hosts/pbx/configuration.nix @@ -6,6 +6,8 @@ ./services ]; + users.users.jalr.initialHashedPassword = "$y$j9T$jNVubFR0TeZOo2jm.aBke/$GPYbyIUGq2lFccC4fHZX61s3EwQ.dB8uUsTINeRLt/1"; + weinturm = { impermanence = { enable = true; diff --git a/hosts/pbx/networking.nix b/hosts/pbx/networking.nix index 363d878..8fef211 100644 --- a/hosts/pbx/networking.nix +++ b/hosts/pbx/networking.nix @@ -1,14 +1,28 @@ -{pkgs, ...}: { +{ + lib, + pkgs, + config, + ... +}: { networking = { hostName = "pbx"; useDHCP = false; # Fix Intel NIC e1000e hardware unit hang - localCommands = "${pkgs.ethtool}/bin/ethtool -K enp0s25 tso off gso off"; + localCommands = lib.mkBefore "${pkgs.ethtool}/bin/ethtool -K enp0s25 tso off gso off"; - firewall.interfaces = { - weinturm.allowedUDPPorts = [53 67]; - public-event.allowedUDPPorts = [53 67]; + firewall = { + interfaces = { + weinturm.allowedUDPPorts = [53 67]; + public-event.allowedUDPPorts = [53 67]; + }; + filterForward = true; + extraForwardRules = '' + oifname { "jugendtreff", "public-ip4" } meta l4proto tcp tcp dport 25 drop comment "Block outgoing SMTP (TCP/25)" + oifname { "jugendtreff", "public-ip4" } meta l4proto tcp tcp dport { 135, 137, 138, 139, 445 } drop comment "Block MS RPC/NetBIOS/SMB (TCP)" + oifname { "jugendtreff", "public-ip4" } meta l4proto udp udp dport { 135, 137, 138, 139, 445 } drop comment "Block MS RPC/NetBIOS/SMB (UDP)" + oifname { "jugendtreff", "public-ip4" } meta l4proto udp udp dport 1900 drop comment "Block SSDP (UPnP, UDP/1900)" + ''; }; vlans = { @@ -76,7 +90,22 @@ "voice" ]; }; - defaultGateway.address = "192.168.100.1"; + nftables.tables.pppoe = { + family = "ip"; + content = let + headerSize = { + ipv4 = 20; + tcp = 20; + pppoe = 8; + }; + maxsegSize = with headerSize; 1500 - ipv4 - tcp - pppoe; + in '' + chain clamp { + type filter hook forward priority mangle; + oifname "${config.networking.nat.externalInterface}" tcp flags syn tcp option maxseg size set ${toString maxsegSize} + } + ''; + }; nameservers = [ "9.9.9.9" "149.112.112.112" @@ -153,6 +182,37 @@ ]; } ]; + + option-def = lib.lists.optional config.services.unifi.enable { + name = "unifi-address"; + code = 1; + space = "ubnt"; + type = "ipv4-address"; + encapsulate = ""; + }; + + client-classes = lib.lists.optional config.services.unifi.enable { + name = "ubnt"; + test = "(option[vendor-class-identifier].text == 'ubnt')"; + option-def = [ + { + name = "vendor-encapsulated-options"; + code = 43; + type = "empty"; + encapsulate = "ubnt"; + } + ]; + option-data = [ + { + name = "unifi-address"; + space = "ubnt"; + data = "192.168.96.1"; + } + { + name = "vendor-encapsulated-options"; + } + ]; + }; }; }; diff --git a/hosts/pbx/secrets.yaml b/hosts/pbx/secrets.yaml index fc47f86..65e6b3c 100644 --- a/hosts/pbx/secrets.yaml +++ b/hosts/pbx/secrets.yaml @@ -1,8 +1,19 @@ +unpoller: ENC[AES256_GCM,data:w1PvLyJlUP+hsJFcgW9hKD/CvTQzSin+,iv:LuSbsN6Hg9XOc1SCYTBjQNXtqlg5tfHutzTNt4dm20I=,tag:BLBmfB0OwhR3VZzvVyd4IQ==,type:str] fieldpoc: omm: ENC[AES256_GCM,data:vOoow2CTJKfCiml5t0k=,iv:BTnf2ASndaNgjYtikTl/B3a5wSRh37epSDT0eGZpLkI=,tag:XOFlh+Ut3JKPd5AUPtrBMw==,type:str] sip: ENC[AES256_GCM,data:B82q2sD5I6NUa+RphJL+f1IT5qpZYlpMunZUaN5JJ5I=,iv:YzDg/g1C1z7kV2R5LLNMhe2UvaRaurQKaq4SbGfFKmQ=,tag:NuWn3D8u6jiJFZFTaFvv3g==,type:str] wireguard: public-ip4: ENC[AES256_GCM,data:NifuhsgDA+/4c+Op9CAg4jhizFdup7FL9jQt4VLGqGzOaY9lMpAFvrWiW2o=,iv:zKN7QTIEo8+KjwtNPxhUVwD+6Xmz48gp9nDAg3bOazo=,tag:GQCBEFAD2en33gKXraXArw==,type:str] +yate: + accounts: + easybell-2: ENC[AES256_GCM,data:jPyZY87r++dNLZCv,iv:BMVICnZujyIbE4IYi+Z9tqn5rbWwnEcoHm9/jWAAhsc=,tag:tk/Vs0tOt6p+a3vD0bJMfw==,type:str] + easybell-3: ENC[AES256_GCM,data:JNQKClwQtYm4GMRp,iv:WsYzrY4vDPQ5voGkQsnOTFTeo09XbE1SfOT6cPv6NJw=,tag:M0hvDGUnFM8lRxRiXNMOUA==,type:str] + easybell-4: ENC[AES256_GCM,data:+bvA76qDKPfSwF/j,iv:Dtnn8JOnIHEXfwjqIWnNlAWdCVIzDbuz1VT6YVPo62w=,tag:NQrKHSV93u5ZAnwYu8EDHQ==,type:str] + easybell-5: ENC[AES256_GCM,data:yj8BuiShAb7gRapp,iv:R0Rj6+Bd54nb4vGfv2yD+H5miWaxLIiMwozgsq/cGN8=,tag:/iQ0r1rTOvDsb+Ik1Rg6oA==,type:str] + easybell-6: ENC[AES256_GCM,data:aAgbSrXbReqUkFq0,iv:VcAsb+246Qys0BJGpTwxTaj5LpQ5fuyJNys3EOMzt5k=,tag:7zXTQNtdsadRiZ/7DrtnHg==,type:str] + easybell-7: ENC[AES256_GCM,data:XU+9wmOTck+xXedv,iv:2ehV8RDzGY68BmlJf5u1oCG/G80uDtFBiA4MGotrFgU=,tag:+mGKoOBjmrB70iOoBTTsKA==,type:str] + easybell-8: ENC[AES256_GCM,data:mVjh6ybvPnT8YhXy,iv:l6RXSdK7Jq/ObOc0gx2fw/9SoZNyGaIAjsl9wBiI7UI=,tag:eIOi54s6RsmYHvrG15pPYQ==,type:str] + easybell-9: ENC[AES256_GCM,data:v0fo8FFrfQQn1H29,iv:jmDFvuRb4W12D9Gh6CLArymyf7efMvsQiELGksTa6Lk=,tag:wylK++mO98LZCsWd1DIT1Q==,type:str] sops: age: - recipient: age16s0cyttcsp40jup9vnreck6mw500ae8j4ayrmf0tg79ukhgua3vsf4m5j4 @@ -14,8 +25,8 @@ sops: TFN1ZFJ2cEZmcHoxSmU1c3o0Q0w1cnMKkT8uBrgL9zyL5PAcqJqQerUdJN8yieVO JwJvcU3I6reHuVkeNKGCZXdYrNMGeFPWwL88yHJW9MYjhO6xfDo8WQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-07-22T09:02:55Z" - mac: ENC[AES256_GCM,data:EYfRNPGHQYmxYPswTozFpd7Vp9j7PhV/Vop8dvvdr3JeAUGoHF2FHZt2Xxrni/wu3CSFW2jGLpMPXigiCxZndbGZhREjCaFrvtNIL/5fhmFV9hoAuW7jp8ydRbHoSB2wJ0d+O/YO4Y5uoKO+pnbmvWgMpHllrBvMMJ/+1tBgh5g=,iv:48VMeGQvhVTAgrtKNbyE9YTQLsp7vYlRPrm9cUMBC24=,tag:j9PPD8B7CYiojNKf6BhG+w==,type:str] + lastmodified: "2025-07-23T20:25:37Z" + mac: ENC[AES256_GCM,data:fuTK5OV8mL8xe23/IkwDHiseSvfZ7BteR88k40rVCQaHOtVU66BteffEzxB6oHTQdmr4Ni8S7lrT2s3Y5oUpKe8oy6a7fbDL8fSipiKXzrUDvmnIr02Cp3UkUeEZrZXgClp31YRLtL00u1qvgSOxSBGCHXJwY1Xyoy9T5u0PNtQ=,iv:wQNa9COOvgoEmbPbCr1p/51158B9/97iqKGmvfYRti4=,tag:TEheul0eeir06sRGHm1NvQ==,type:str] pgp: - created_at: "2025-07-18T23:14:45Z" enc: |- diff --git a/hosts/pbx/services/default.nix b/hosts/pbx/services/default.nix index d22355b..4a97612 100644 --- a/hosts/pbx/services/default.nix +++ b/hosts/pbx/services/default.nix @@ -2,6 +2,7 @@ imports = [ ./fieldpoc ./public-ip4-tunnel.nix + ./unifi-controller ./webserver.nix ]; } diff --git a/hosts/pbx/services/fieldpoc/accounts.nix b/hosts/pbx/services/fieldpoc/accounts.nix new file mode 100644 index 0000000..0bf2ea2 --- /dev/null +++ b/hosts/pbx/services/fieldpoc/accounts.nix @@ -0,0 +1,63 @@ +{ + config, + lib, + pkgs, + ... +}: { + sops.secrets = lib.listToAttrs ( + map + (number: + lib.nameValuePair "yate/accounts/easybell-${toString number}" { + sopsFile = ../../secrets.yaml; + owner = "yate"; + }) + (lib.lists.range 2 9) + ); + + environment.etc."yate/accfile.conf" = { + mode = "symlink"; + source = "/var/run/yate/accfile.conf"; + }; + + systemd.services.yate.serviceConfig = let + easybellAccount = name: username: let + title = "easybell-${toString name}"; + secretPath = config.sops.secrets."yate/accounts/${title}".path; + in '' + [${title}] + enabled=yes + protocol=sip + username=${username} + password=$(cat "${secretPath}") + registrar=pbx.easybell.de + ''; + accounts = [ + (easybellAccount 2 "CPBX-61tkfwsx-000004") + (easybellAccount 3 "CPBX-61tkfwsx-000005") + (easybellAccount 4 "CPBX-61tkfwsx-000006") + (easybellAccount 5 "CPBX-61tkfwsx-000007") + (easybellAccount 6 "CPBX-61tkfwsx-000008") + (easybellAccount 7 "CPBX-61tkfwsx-000009") + (easybellAccount 8 "CPBX-61tkfwsx-000010") + (easybellAccount 9 "CPBX-61tkfwsx-000011") + ]; + in { + RuntimeDirectory = "yate"; + RuntimeDirectoryMode = lib.mkForce "2750"; + ExecStartPre = pkgs.writeShellScript "yate-pre-start" '' + cat > "$RUNTIME_DIRECTORY/accfile.conf" << EOF + ${lib.concatStringsSep "\n" accounts} + EOF + ''; + }; + + services.yate.config = { + yate.modules."regexroute.yate" = "enable"; + regexroute.default = let + matchCalled = account: ''''${called}^${account}$''; + in { + "${matchCalled "CPBX-61tkfwsx-000004"}" = "sip/sip:1337@192.168.98.11"; + #"^.*$" = ''echo REGEXROUTE DEBUG called=''${called} address=''${address} callsource=''${callsource} formats=''${formats} id=''${id} peerid=''${peerid} ip_host=''${ip_host} ip_port=''${ip_port} overlapped=''${overlapped} rtp_forward=''${rtp_forward} type=''${type} username=''${username} line=''${line} account=''${account} caller=''${caller} called=''${called} module=''${module}''; + }; + }; +} diff --git a/hosts/pbx/services/fieldpoc/default.nix b/hosts/pbx/services/fieldpoc/default.nix index 949f1bd..9aa1bec 100644 --- a/hosts/pbx/services/fieldpoc/default.nix +++ b/hosts/pbx/services/fieldpoc/default.nix @@ -5,6 +5,11 @@ to = 11250; }; in { + imports = [ + ./accounts.nix + ./extensions.nix + ]; + sops.secrets."fieldpoc/omm" = { sopsFile = ../../secrets.yaml; owner = "fieldpoc"; @@ -41,9 +46,18 @@ in { ]; services = { - yate.config.yrtpchan.general = { - minport = rtpPorts.from; - maxport = rtpPorts.to; + yate.config = { + yrtpchan.general = { + minport = rtpPorts.from; + maxport = rtpPorts.to; + }; + ysipchan = { + "listener voice" = { + addr = (builtins.elemAt config.networking.interfaces.voice.ipv4.addresses 0).address; + type = "udp"; + port = 5060; + }; + }; }; fieldpoc = { diff --git a/hosts/pbx/services/fieldpoc/extensions.nix b/hosts/pbx/services/fieldpoc/extensions.nix new file mode 100644 index 0000000..160b7f5 --- /dev/null +++ b/hosts/pbx/services/fieldpoc/extensions.nix @@ -0,0 +1,100 @@ +{ + lib, + pkgs, + ... +}: let + domain = "tel.weinturm.de"; + mkphonebook = pkgs.python3.pkgs.buildPythonPackage { + pname = "fieldpoc-mkphonebook"; + version = "1.0"; + + src = ./mkphonebook.py; + + dontUnpack = true; + + propagatedBuildInputs = [pkgs.makeWrapper]; + + format = "other"; + + installPhase = '' + mkdir -p $out/lib/mkphonebook + mkdir -p $out/bin + cp $src $out/lib/mkphonebook/script.py + + makeWrapper ${pkgs.python3.interpreter} $out/bin/mkphonebook \ + --add-flags "$out/lib/mkphonebook/script.py" \ + --set PYTHONPATH "${pkgs.python3.pkgs.pyyaml}/${pkgs.python3.sitePackages}" + ''; + }; + webmanifest = lib.generators.toJSON {} { + name = "Weinturm"; + short_name = "Telefonbuch"; + icons = [ + { + src = "/web-app-manifest-192x192.png"; + sizes = "192x192"; + type = "image/png"; + purpose = "maskable"; + } + { + src = "/web-app-manifest-512x512.png"; + sizes = "512x512"; + type = "image/png"; + purpose = "maskable"; + } + ]; + theme_color = "#ffffff"; + background_color = "#300a8d"; + display = "standalone"; + }; + webmanifestFile = pkgs.writeText "site.webmanifest" webmanifest; + webroot = pkgs.stdenvNoCC.mkDerivation { + name = "webroot-${domain}"; + src = ./html; + dontBuild = true; + installPhase = '' + export PATH="$PATH:${pkgs.lib.makeBinPath [pkgs.imagemagick]}" + mkdir $out + cp "$src/favicon.svg" "$out/favicon.svg" + convert -background transparent "$src/favicon.svg" -define icon:auto-resize=16,24,32,48,64,72,96,128,256 "$out/favicon.ico" + convert -background transparent "$src/favicon.svg" -resize 180x180 "$out/apple-touch-icon.png" + convert -background transparent "$src/favicon.svg" -resize 96x96 "$out/favicon-96x96.png" + convert -background transparent "$src/favicon.svg" -resize 192x192 "$out/web-app-manifest-192x192.png" + convert -background transparent "$src/favicon.svg" -resize 512x512 "$out/web-app-manifest-512x512.png" + cp "${webmanifestFile}" "$out/site.webmanifest" + ln -s /persist/html/index.html "$out/index.html" + ''; + }; +in { + environment.systemPackages = [ + ( + pkgs.writeShellScriptBin "fieldpoc-load-extensions" '' + set -e + + tmpfile="$(mktemp -p /tmp tmp.extensions.XXXXXXXXXX.json)" + trap "rm -f $tmpfile" 0 2 3 15 + + ${pkgs.yq}/bin/yq \ + '.extensions[] |= with_entries(select(.key | IN("name", "type", "dialout_allowed", "trunk", "static_target", "callgroup_members", "sip_password", "dect_ipei")))' \ + "$1" > $tmpfile + + cat "$tmpfile" | /run/wrappers/bin/sudo -u fieldpoc tee /var/lib/fieldpoc/extensions.json >/dev/null + + curl -s --fail --json '{}' http://127.0.0.1:9437/reload + + ${mkphonebook}/bin/mkphonebook "$1" "/persist/html/index.html" + + rm -f $tmpfile + '' + ) + ]; + + services.nginx.virtualHosts = { + "${domain}" = { + serverAliases = ["tel.weinturm-open-air.de"]; + enableACME = true; + forceSSL = true; + root = webroot; + }; + }; +} diff --git a/hosts/pbx/services/fieldpoc/html/favicon.svg b/hosts/pbx/services/fieldpoc/html/favicon.svg new file mode 100644 index 0000000..4ed0da0 --- /dev/null +++ b/hosts/pbx/services/fieldpoc/html/favicon.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosts/pbx/services/fieldpoc/mkphonebook.py b/hosts/pbx/services/fieldpoc/mkphonebook.py new file mode 100644 index 0000000..5deff0b --- /dev/null +++ b/hosts/pbx/services/fieldpoc/mkphonebook.py @@ -0,0 +1,285 @@ +import os +import sys +import yaml +from collections import defaultdict + + +def generate_html(extensions): + # Icons per Typ + type_icons = { + "sip": "πŸ“±", + "dect": "πŸ“ž", + "static": "πŸ“Œ", + "callgroup": "πŸ‘₯", + "temp": "⏳", + "default": "☎️", + } + + def make_color(idx): + hue = (idx * 30 + idx % 2 * 180) % 360 + return f"hsl({hue}, 50%, 95%)" + + emergency_color = "hsl(0, 75%, 75%)" + + # Generiere Farbzuordnung je Location + locations = sorted(set(info.get("location", "") for info in extensions.values())) + location_colors = {} + for idx, loc in enumerate(locations): + location_colors[loc] = make_color(idx + 1) + + # Generiere HTML + html_header = """ + + + + + + Telefonbuch + + + + + + + + + + +

πŸ“– Telefonbuch

+ +
+ + +
+ + + + + + + + + + + + +""" + + # Sortiere EintrΓ€ge: + # 1. Notrufnummern (3-Stellig) numerisch sortiert + # 2. alle weiteren Extensions: nach Location, dann Name + rows = sorted( + extensions.items(), + key=lambda x: ( + (0, x[0], "") + if len(x[0]) < 4 + else (1, x[1].get("location", ""), x[1].get("name", "")) + ), + ) + html_rows = "" + + for ext, info in rows: + name = info.get("name", "") + typ = info.get("type", "default") + location = info.get("location", "") + vanity = info.get("vanity", "") + dialin = info.get("dialin", "") + icon = type_icons.get(typ, type_icons["default"]) + row_color = ( + emergency_color if len(ext) < 4 else location_colors.get(location, "#fff") + ) + + name_cell = f"{name}" + if dialin: + name_cell = f'{name_cell}' + + html_rows += f""" + + + + + + + + +""" + + html_footer = """ + + +
πŸ§‘ NameπŸ”€ πŸ“ OrtπŸ”§ Typ
{icon}{ext}{name_cell}{vanity}{location}{typ}
+ + +""" + + return html_header + html_rows + html_footer + + +def main(): + INPUT_FILE, OUTPUT_FILE = sys.argv[1:] + + with open(INPUT_FILE, "r") as f: + data = yaml.safe_load(f) + + extensions = data.get("extensions", {}) + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + f.write(generate_html(extensions)) + + +if __name__ == "__main__": + main() diff --git a/hosts/pbx/services/public-ip4-tunnel.nix b/hosts/pbx/services/public-ip4-tunnel.nix index ffef316..9cf2dc8 100644 --- a/hosts/pbx/services/public-ip4-tunnel.nix +++ b/hosts/pbx/services/public-ip4-tunnel.nix @@ -1,5 +1,6 @@ { config, + lib, pkgs, ... }: let @@ -17,6 +18,15 @@ in { sopsFile = ../secrets.yaml; }; + services.yate.config.ysipchan = { + general = { + addr = externalIp; + type = "udp"; + port = 5060; + rtp_localip = externalIp; + }; + }; + networking = { iproute2 = { enable = true; @@ -24,18 +34,54 @@ in { ${toString rtTable.id} ${rtTable.name} ''; }; - wireguard.interfaces."${interface}" = { + + wireguard.interfaces."${interface}" = let + rules = [ + "from 192.168.98.0/24 to 10.0.0.0/8 table main priority 10" + "from 192.168.98.0/24 to 192.168.0.0/16 table main priority 10" + "from 192.168.98.0/24 lookup ${rtTable.name} priority 20" + + "from ${externalIp} to 10.0.0.0/8 table main priority 10" + "from ${externalIp} to 192.168.0.0/16 table main priority 10" + "from ${externalIp} table ${rtTable.name} priority 20" + + "from ${externalIp} oif ${interface} lookup ${rtTable.name} priority 20" + ]; + addRule = rule: "ip rule add " + rule; + deleteRule = rule: "ip rule delete " + rule; + path = pkgs.lib.makeBinPath [pkgs.iproute2 pkgs.nftables]; + in { ips = ["${externalIp}/32"]; privateKeyFile = config.sops.secrets."wireguard/${interface}".path; table = rtTable.name; - postSetup = '' - ${pkgs.iproute2}/bin/ip rule add from ${externalIp} to 192.168.0.0/16 table main priority 10 - ${pkgs.iproute2}/bin/ip rule add from ${externalIp} table ${rtTable.name} priority 20 - ''; - postShutdown = '' - ${pkgs.iproute2}/bin/ip rule del from ${externalIp} to 192.168.0.0/16 table main priority 10 - ${pkgs.iproute2}/bin/ip rule del from ${externalIp} table ${rtTable.name} priority 20 - ''; + postSetup = lib.concatLines [ + "export PATH=${path}" + "set -x" + (lib.concatMapStringsSep "\n" addRule rules) + /* + ip route change default dev ${interface} src ${externalIp} table ${rtTable.name} + */ + '' + + nft add table ip wg_nat-${interface} + nft add chain ip wg_nat-${interface} postrouting '{type nat hook postrouting priority srcnat;}' + nft add rule ip wg_nat-${interface} postrouting ip saddr 192.168.0.0/16 oifname "public-ip4" counter snat to ${externalIp} + nft add rule ip wg_nat-${interface} postrouting ip saddr 10.0.0.0/8 oifname "public-ip4" counter snat to ${externalIp} + + nft add table inet wg_filter-${interface} + nft add chain inet wg_filter-${interface} forward '{type filter hook forward priority 0; policy accept;}' + '' + ]; + postShutdown = lib.concatLines [ + "export PATH=${path}" + "set -x" + (lib.concatMapStringsSep "\n" deleteRule rules) + + '' + nft delete table ip wg_nat-${interface} + nft delete table inet wg_filter-${interface} + '' + ]; peers = [ { inherit publicKey; diff --git a/hosts/pbx/services/unifi-controller/default.nix b/hosts/pbx/services/unifi-controller/default.nix new file mode 100644 index 0000000..baea9e9 --- /dev/null +++ b/hosts/pbx/services/unifi-controller/default.nix @@ -0,0 +1,46 @@ +args: let + domain = "unifi.weinturm.de"; +in { + imports = [ + (import ./unpoller.nix (args // {inherit domain;})) + ]; + + services.unifi.enable = true; + + networking.firewall.interfaces.weinturm = { + # https://help.ubnt.com/hc/en-us/articles/218506997 + allowedTCPPorts = [ + 8080 # Port for UAP to inform controller. + 8880 # Port for HTTP portal redirect, if guest portal is enabled. + 8843 # Port for HTTPS portal redirect, ditto. + 6789 # Port for UniFi mobile speed test. + ]; + allowedUDPPorts = [ + 3478 # UDP port used for STUN. + 10001 # UDP port used for device discovery. + ]; + }; + + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/unifi"; + user = "unifi"; + group = "unifi"; + mode = "u=rwx,g=rx,o=rx"; + } + ]; + + services.nginx.virtualHosts = { + "${domain}" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "https://127.0.0.1:8443"; + recommendedProxySettings = true; + extraConfig = '' + proxy_ssl_verify off; + ''; + }; + }; + }; +} diff --git a/hosts/pbx/services/unifi-controller/unpoller.nix b/hosts/pbx/services/unifi-controller/unpoller.nix new file mode 100644 index 0000000..e89e089 --- /dev/null +++ b/hosts/pbx/services/unifi-controller/unpoller.nix @@ -0,0 +1,24 @@ +{ + config, + domain, + ... +}: { + sops.secrets.unpoller = { + owner = config.services.prometheus.exporters.unpoller.user; + sopsFile = ../../secrets.yaml; + }; + + services.prometheus.exporters.unpoller = { + enable = true; + controllers = [ + { + user = "unpoller"; + url = "https://${domain}"; + pass = config.sops.secrets.unpoller.path; + verify_ssl = false; + hash_pii = true; + } + ]; + log.prometheusErrors = true; + }; +} diff --git a/hosts/pbx/services/webserver.nix b/hosts/pbx/services/webserver.nix index 583b446..0e460dc 100644 --- a/hosts/pbx/services/webserver.nix +++ b/hosts/pbx/services/webserver.nix @@ -1,6 +1,4 @@ -{config, ...}: let - domain = "tel.weinturm.de"; -in { +{config, ...}: { networking.firewall.allowedTCPPorts = [ config.services.nginx.defaultHTTPListenPort config.services.nginx.defaultSSLListenPort @@ -21,13 +19,5 @@ in { add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; ''; - virtualHosts = { - "${domain}" = { - serverAliases = ["tel.weinturm-open-air.de"]; - enableACME = true; - forceSSL = true; - root = "/persist/html"; - }; - }; }; } diff --git a/justfile b/justfile index 684e369..11d0dc2 100644 --- a/justfile +++ b/justfile @@ -7,3 +7,6 @@ repl: pkgs = inputs.nixpkgs.legacyPackages."\${builtins.currentSystem}".extend(import ./pkgs inputs); \ }) \ " +upload-extensions: + scp extensions.yaml tel.weinturm.de: + ssh tel.weinturm.de fieldpoc-load-extensions extensions.yaml diff --git a/modules/default.nix b/modules/default.nix index 6497baa..41909df 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -10,6 +10,7 @@ ./nix.nix ./security.nix ./sshd.nix + ./unfree.nix ./zram.nix ]; diff --git a/modules/impermanence.nix b/modules/impermanence.nix index 70f99bb..1df8dad 100644 --- a/modules/impermanence.nix +++ b/modules/impermanence.nix @@ -24,10 +24,13 @@ cfg = config.weinturm.impermanence; in lib.mkIf cfg.enable { + users.mutableUsers = false; + fileSystems."/persist".neededForBoot = true; environment.persistence."/persist".directories = [ "/var/lib/nixos" + "/var/lib/acme" ]; boot.initrd.postDeviceCommands = let diff --git a/modules/unfree.nix b/modules/unfree.nix new file mode 100644 index 0000000..b900b5f --- /dev/null +++ b/modules/unfree.nix @@ -0,0 +1,7 @@ +{lib, ...}: { + nixpkgs.config.allowUnfreePredicate = pkg: + lib.elem (lib.getName pkg) [ + "mongodb" + "unifi-controller" + ]; +}