From 3f6de04b84c96710fd86b1400faeb459b003013a Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 23 Jul 2025 05:05:29 +0200 Subject: [PATCH 01/16] Add easybell lines --- hosts/pbx/secrets.yaml | 14 +++++- hosts/pbx/services/fieldpoc/accounts.nix | 63 ++++++++++++++++++++++++ hosts/pbx/services/fieldpoc/default.nix | 4 ++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 hosts/pbx/services/fieldpoc/accounts.nix diff --git a/hosts/pbx/secrets.yaml b/hosts/pbx/secrets.yaml index fc47f86..bb57413 100644 --- a/hosts/pbx/secrets.yaml +++ b/hosts/pbx/secrets.yaml @@ -3,6 +3,16 @@ fieldpoc: 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 +24,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-23T09:35:37Z" + mac: ENC[AES256_GCM,data:e1hoBiXA1BrLVTaf/siFWwjDSPvgaWYmfzMBjoIqShj1MnUg8vXBfPR89bhsPNtOkW7s0HVsgFeKBMFm0++xkDOb/Xy7gdzPltF4f8P0D5SrlcEoeHgRQWLCgxJLB4suKUBhUauccKKg1NlIVXw3MgizBjG7+bTfGDXZfVGGJy4=,iv:O0JE5V6rVkPnCpxVsGJUpeQZsmJF4ZxPTnqnLwZZnlg=,tag:AnejfZw44+8CnoDHS1KIsg==,type:str] pgp: - created_at: "2025-07-18T23:14:45Z" enc: |- 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..17c9af7 100644 --- a/hosts/pbx/services/fieldpoc/default.nix +++ b/hosts/pbx/services/fieldpoc/default.nix @@ -5,6 +5,10 @@ to = 11250; }; in { + imports = [ + ./accounts.nix + ]; + sops.secrets."fieldpoc/omm" = { sopsFile = ../../secrets.yaml; owner = "fieldpoc"; From 03eebb71235690eb03183a5f53d8c3ef0a8c5e9f Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 23 Jul 2025 22:19:11 +0200 Subject: [PATCH 02/16] Add nftables rule for PPPoE MSS --- hosts/pbx/networking.nix | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/hosts/pbx/networking.nix b/hosts/pbx/networking.nix index 363d878..7679d3f 100644 --- a/hosts/pbx/networking.nix +++ b/hosts/pbx/networking.nix @@ -1,4 +1,8 @@ -{pkgs, ...}: { +{ + pkgs, + config, + ... +}: { networking = { hostName = "pbx"; useDHCP = false; @@ -76,6 +80,22 @@ "voice" ]; }; + 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} + } + ''; + }; defaultGateway.address = "192.168.100.1"; nameservers = [ "9.9.9.9" From 7e54c2eabfe5e0758744cb466b6b984985f7a40d Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 23 Jul 2025 23:20:58 +0200 Subject: [PATCH 03/16] Disable mutable users if impermanence is used --- modules/impermanence.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/impermanence.nix b/modules/impermanence.nix index 70f99bb..c498b4f 100644 --- a/modules/impermanence.nix +++ b/modules/impermanence.nix @@ -24,6 +24,8 @@ cfg = config.weinturm.impermanence; in lib.mkIf cfg.enable { + users.mutableUsers = false; + fileSystems."/persist".neededForBoot = true; environment.persistence."/persist".directories = [ From 93e8d80d6468072515d9bf18c490c3fbebeb2dbb Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 23 Jul 2025 23:20:44 +0200 Subject: [PATCH 04/16] Add password for jalr --- hosts/pbx/configuration.nix | 2 ++ 1 file changed, 2 insertions(+) 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; From 6bb470764929c0195c16b9dbbf69d4da712790f1 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 23 Jul 2025 22:57:37 +0200 Subject: [PATCH 05/16] Add unifi controller --- docs/hp-switch.md | 7 ++- hosts/pbx/networking.nix | 32 +++++++++++++ hosts/pbx/secrets.yaml | 5 +- hosts/pbx/services/default.nix | 1 + .../pbx/services/unifi-controller/default.nix | 46 +++++++++++++++++++ .../services/unifi-controller/unpoller.nix | 24 ++++++++++ modules/default.nix | 1 + modules/unfree.nix | 7 +++ 8 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 hosts/pbx/services/unifi-controller/default.nix create mode 100644 hosts/pbx/services/unifi-controller/unpoller.nix create mode 100644 modules/unfree.nix 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/pbx/networking.nix b/hosts/pbx/networking.nix index 7679d3f..acb383c 100644 --- a/hosts/pbx/networking.nix +++ b/hosts/pbx/networking.nix @@ -1,4 +1,5 @@ { + lib, pkgs, config, ... @@ -173,6 +174,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 bb57413..65e6b3c 100644 --- a/hosts/pbx/secrets.yaml +++ b/hosts/pbx/secrets.yaml @@ -1,3 +1,4 @@ +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] @@ -24,8 +25,8 @@ sops: TFN1ZFJ2cEZmcHoxSmU1c3o0Q0w1cnMKkT8uBrgL9zyL5PAcqJqQerUdJN8yieVO JwJvcU3I6reHuVkeNKGCZXdYrNMGeFPWwL88yHJW9MYjhO6xfDo8WQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-07-23T09:35:37Z" - mac: ENC[AES256_GCM,data:e1hoBiXA1BrLVTaf/siFWwjDSPvgaWYmfzMBjoIqShj1MnUg8vXBfPR89bhsPNtOkW7s0HVsgFeKBMFm0++xkDOb/Xy7gdzPltF4f8P0D5SrlcEoeHgRQWLCgxJLB4suKUBhUauccKKg1NlIVXw3MgizBjG7+bTfGDXZfVGGJy4=,iv:O0JE5V6rVkPnCpxVsGJUpeQZsmJF4ZxPTnqnLwZZnlg=,tag:AnejfZw44+8CnoDHS1KIsg==,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/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/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/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" + ]; +} From 24fe84b86fe7c2c00c8175c5deae1dfa01606c68 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Thu, 24 Jul 2025 01:04:13 +0200 Subject: [PATCH 06/16] Make /var/lib/acme a persist path --- modules/impermanence.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/impermanence.nix b/modules/impermanence.nix index c498b4f..1df8dad 100644 --- a/modules/impermanence.nix +++ b/modules/impermanence.nix @@ -30,6 +30,7 @@ environment.persistence."/persist".directories = [ "/var/lib/nixos" + "/var/lib/acme" ]; boot.initrd.postDeviceCommands = let From 4193e6b96af5c6785052c720d332db49db8d1476 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Thu, 24 Jul 2025 01:49:57 +0200 Subject: [PATCH 07/16] Remove static default gateway Use DHCP instead --- hosts/pbx/networking.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/hosts/pbx/networking.nix b/hosts/pbx/networking.nix index acb383c..31d9731 100644 --- a/hosts/pbx/networking.nix +++ b/hosts/pbx/networking.nix @@ -97,7 +97,6 @@ } ''; }; - defaultGateway.address = "192.168.100.1"; nameservers = [ "9.9.9.9" "149.112.112.112" From bcacdc6609d51d49aed0f698a3e5df5f4bfc7710 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Thu, 24 Jul 2025 03:44:00 +0200 Subject: [PATCH 08/16] Use tunnel as gateway for VoIP traffic --- hosts/pbx/networking.nix | 2 +- hosts/pbx/services/public-ip4-tunnel.nix | 72 +++++++++++++++++++++--- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/hosts/pbx/networking.nix b/hosts/pbx/networking.nix index 31d9731..e46f25f 100644 --- a/hosts/pbx/networking.nix +++ b/hosts/pbx/networking.nix @@ -9,7 +9,7 @@ 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]; diff --git a/hosts/pbx/services/public-ip4-tunnel.nix b/hosts/pbx/services/public-ip4-tunnel.nix index ffef316..1246da8 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,16 @@ in { sopsFile = ../secrets.yaml; }; + services.yate.config.ysipchan = { + general.localaddress = externalIp; + "listener external" = { + enable = "true"; + type = "udp"; + addr = externalIp; + port = 5060; + }; + }; + networking = { iproute2 = { enable = true; @@ -24,18 +35,61 @@ 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" + "fwmark 0x1 to 192.168.0.0/16 table main priority 10" + "fwmark 0x1 to 10.0.0.0/8 table main priority 10" + + "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 192.168.98.0/24 table ${interface} priority 20" + + "fwmark 0x1 table ${interface} 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 table inet wg_filter-${interface} + nft add chain inet wg_filter-${interface} forward '{type filter hook forward priority 0; policy accept;}' + + nft add table inet marks-${interface} + nft add chain inet marks-${interface} output '{type route hook output priority mangle;}' + nft add rule inet marks-${interface} output meta skuid yate mark set 0x1 + '' + ]; + 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} + + nft delete table inet marks-${interface} + '' + ]; peers = [ { inherit publicKey; From d8bc1c9762a9c73ff0540f7d899cedcd399b0c1a Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 28 Jul 2025 01:58:18 +0200 Subject: [PATCH 09/16] SIP calls from internet have audio now --- hosts/default.nix | 2 +- hosts/pbx/services/public-ip4-tunnel.nix | 17 ++++------------- 2 files changed, 5 insertions(+), 14 deletions(-) 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/services/public-ip4-tunnel.nix b/hosts/pbx/services/public-ip4-tunnel.nix index 1246da8..061b2db 100644 --- a/hosts/pbx/services/public-ip4-tunnel.nix +++ b/hosts/pbx/services/public-ip4-tunnel.nix @@ -19,11 +19,10 @@ in { }; services.yate.config.ysipchan = { - general.localaddress = externalIp; - "listener external" = { - enable = "true"; - type = "udp"; + general.rtp_localip = externalIp; + general = { addr = externalIp; + type = "udp"; port = 5060; }; }; @@ -40,15 +39,13 @@ in { 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" - "fwmark 0x1 to 192.168.0.0/16 table main priority 10" - "fwmark 0x1 to 10.0.0.0/8 table main priority 10" "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 192.168.98.0/24 table ${interface} priority 20" - "fwmark 0x1 table ${interface} priority 20" + "from ${externalIp} lookup ${rtTable.name}" ]; addRule = rule: "ip rule add " + rule; deleteRule = rule: "ip rule delete " + rule; @@ -72,10 +69,6 @@ in { nft add table inet wg_filter-${interface} nft add chain inet wg_filter-${interface} forward '{type filter hook forward priority 0; policy accept;}' - - nft add table inet marks-${interface} - nft add chain inet marks-${interface} output '{type route hook output priority mangle;}' - nft add rule inet marks-${interface} output meta skuid yate mark set 0x1 '' ]; postShutdown = lib.concatLines [ @@ -86,8 +79,6 @@ in { '' nft delete table ip wg_nat-${interface} nft delete table inet wg_filter-${interface} - - nft delete table inet marks-${interface} '' ]; peers = [ From 709af554b98e2fff2a6b42c5eba40a92f5b8346c Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 28 Jul 2025 02:10:59 +0200 Subject: [PATCH 10/16] Add listener on voice interface --- hosts/pbx/services/fieldpoc/default.nix | 15 ++++++++++++--- hosts/pbx/services/public-ip4-tunnel.nix | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/hosts/pbx/services/fieldpoc/default.nix b/hosts/pbx/services/fieldpoc/default.nix index 17c9af7..bd63f23 100644 --- a/hosts/pbx/services/fieldpoc/default.nix +++ b/hosts/pbx/services/fieldpoc/default.nix @@ -45,9 +45,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/public-ip4-tunnel.nix b/hosts/pbx/services/public-ip4-tunnel.nix index 061b2db..93ab81e 100644 --- a/hosts/pbx/services/public-ip4-tunnel.nix +++ b/hosts/pbx/services/public-ip4-tunnel.nix @@ -19,11 +19,11 @@ in { }; services.yate.config.ysipchan = { - general.rtp_localip = externalIp; general = { addr = externalIp; type = "udp"; port = 5060; + rtp_localip = externalIp; }; }; From 6ddac5f1e25c83e478bdb7ec46a60282c353db84 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 28 Jul 2025 20:01:20 +0200 Subject: [PATCH 11/16] Add missing rule for 10.0.0.0/8 --- hosts/pbx/services/public-ip4-tunnel.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/hosts/pbx/services/public-ip4-tunnel.nix b/hosts/pbx/services/public-ip4-tunnel.nix index 93ab81e..5f130aa 100644 --- a/hosts/pbx/services/public-ip4-tunnel.nix +++ b/hosts/pbx/services/public-ip4-tunnel.nix @@ -66,6 +66,7 @@ in { 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;}' From 7b61b01baa1e51c753ecdd230849a93133b18a05 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 28 Jul 2025 20:01:39 +0200 Subject: [PATCH 12/16] Add forwarding firewall rules --- hosts/pbx/networking.nix | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/hosts/pbx/networking.nix b/hosts/pbx/networking.nix index e46f25f..8fef211 100644 --- a/hosts/pbx/networking.nix +++ b/hosts/pbx/networking.nix @@ -11,9 +11,18 @@ # Fix Intel NIC e1000e hardware unit hang 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 = { From cd62b5bab8a86d0de9d43254b0e83e2cab235370 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 28 Jul 2025 20:27:01 +0200 Subject: [PATCH 13/16] Fix rules for tunnel --- hosts/pbx/services/public-ip4-tunnel.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hosts/pbx/services/public-ip4-tunnel.nix b/hosts/pbx/services/public-ip4-tunnel.nix index 5f130aa..9cf2dc8 100644 --- a/hosts/pbx/services/public-ip4-tunnel.nix +++ b/hosts/pbx/services/public-ip4-tunnel.nix @@ -39,13 +39,13 @@ in { 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 192.168.98.0/24 table ${interface} priority 20" - "from ${externalIp} lookup ${rtTable.name}" + "from ${externalIp} oif ${interface} lookup ${rtTable.name} priority 20" ]; addRule = rule: "ip rule add " + rule; deleteRule = rule: "ip rule delete " + rule; From 651a96a6d9fbe010e34844116529111827e7cfae Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Tue, 29 Jul 2025 21:23:06 +0200 Subject: [PATCH 14/16] Add phonebook --- hosts/pbx/services/fieldpoc/default.nix | 1 + hosts/pbx/services/fieldpoc/extensions.nix | 47 ++++ hosts/pbx/services/fieldpoc/mkphonebook.py | 279 +++++++++++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 hosts/pbx/services/fieldpoc/extensions.nix create mode 100644 hosts/pbx/services/fieldpoc/mkphonebook.py diff --git a/hosts/pbx/services/fieldpoc/default.nix b/hosts/pbx/services/fieldpoc/default.nix index bd63f23..9aa1bec 100644 --- a/hosts/pbx/services/fieldpoc/default.nix +++ b/hosts/pbx/services/fieldpoc/default.nix @@ -7,6 +7,7 @@ in { imports = [ ./accounts.nix + ./extensions.nix ]; sops.secrets."fieldpoc/omm" = { diff --git a/hosts/pbx/services/fieldpoc/extensions.nix b/hosts/pbx/services/fieldpoc/extensions.nix new file mode 100644 index 0000000..bf28d01 --- /dev/null +++ b/hosts/pbx/services/fieldpoc/extensions.nix @@ -0,0 +1,47 @@ +{pkgs, ...}: let + 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}" + ''; + }; +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 + '' + ) + ]; +} diff --git a/hosts/pbx/services/fieldpoc/mkphonebook.py b/hosts/pbx/services/fieldpoc/mkphonebook.py new file mode 100644 index 0000000..94221bf --- /dev/null +++ b/hosts/pbx/services/fieldpoc/mkphonebook.py @@ -0,0 +1,279 @@ +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() From e33af543c8389c19a83e9e760d855755327b79e4 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 30 Jul 2025 01:42:27 +0200 Subject: [PATCH 15/16] Add extension upload --- .gitignore | 2 ++ justfile | 3 +++ 2 files changed, 5 insertions(+) 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/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 From f2fc22e8f7cfffaec687b13369668044adc1da30 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 30 Jul 2025 13:42:08 +0200 Subject: [PATCH 16/16] Add favicon --- hosts/pbx/services/fieldpoc/extensions.nix | 55 ++++++- hosts/pbx/services/fieldpoc/html/favicon.svg | 162 +++++++++++++++++++ hosts/pbx/services/fieldpoc/mkphonebook.py | 6 + hosts/pbx/services/webserver.nix | 12 +- 4 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 hosts/pbx/services/fieldpoc/html/favicon.svg diff --git a/hosts/pbx/services/fieldpoc/extensions.nix b/hosts/pbx/services/fieldpoc/extensions.nix index bf28d01..160b7f5 100644 --- a/hosts/pbx/services/fieldpoc/extensions.nix +++ b/hosts/pbx/services/fieldpoc/extensions.nix @@ -1,4 +1,9 @@ -{pkgs, ...}: let +{ + lib, + pkgs, + ... +}: let + domain = "tel.weinturm.de"; mkphonebook = pkgs.python3.pkgs.buildPythonPackage { pname = "fieldpoc-mkphonebook"; version = "1.0"; @@ -21,6 +26,45 @@ --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 = [ ( @@ -44,4 +88,13 @@ in { '' ) ]; + + 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 index 94221bf..5deff0b 100644 --- a/hosts/pbx/services/fieldpoc/mkphonebook.py +++ b/hosts/pbx/services/fieldpoc/mkphonebook.py @@ -35,6 +35,12 @@ def generate_html(extensions): Telefonbuch + + + + + +