Compare commits
16 commits
3c1d12ef82
...
f2fc22e8f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2fc22e8f7 | ||
|
|
e33af543c8 | ||
|
|
651a96a6d9 | ||
|
|
cd62b5bab8 | ||
|
|
7b61b01baa | ||
|
|
6ddac5f1e2 | ||
|
|
709af554b9 | ||
|
|
d8bc1c9762 | ||
|
|
bcacdc6609 | ||
|
|
4193e6b96a | ||
|
|
24fe84b86f | ||
|
|
6bb4707649 | ||
|
|
93e8d80d64 | ||
|
|
7e54c2eabf | ||
|
|
03eebb7123 | ||
|
|
3f6de04b84 |
20 changed files with 857 additions and 34 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,3 +7,5 @@ result*
|
|||
|
||||
# mdBook
|
||||
/book/*
|
||||
|
||||
/extensions.yaml
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
_inputs: {
|
||||
pbx = {
|
||||
system = "x86_64-linux";
|
||||
targetHost = "tel.weinturm.de";
|
||||
#targetHost = "tel.weinturm.de";
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
./services
|
||||
];
|
||||
|
||||
users.users.jalr.initialHashedPassword = "$y$j9T$jNVubFR0TeZOo2jm.aBke/$GPYbyIUGq2lFccC4fHZX61s3EwQ.dB8uUsTINeRLt/1";
|
||||
|
||||
weinturm = {
|
||||
impermanence = {
|
||||
enable = true;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: |-
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
imports = [
|
||||
./fieldpoc
|
||||
./public-ip4-tunnel.nix
|
||||
./unifi-controller
|
||||
./webserver.nix
|
||||
];
|
||||
}
|
||||
|
|
|
|||
63
hosts/pbx/services/fieldpoc/accounts.nix
Normal file
63
hosts/pbx/services/fieldpoc/accounts.nix
Normal file
|
|
@ -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}'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
100
hosts/pbx/services/fieldpoc/extensions.nix
Normal file
100
hosts/pbx/services/fieldpoc/extensions.nix
Normal file
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
162
hosts/pbx/services/fieldpoc/html/favicon.svg
Normal file
162
hosts/pbx/services/fieldpoc/html/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 38 KiB |
285
hosts/pbx/services/fieldpoc/mkphonebook.py
Normal file
285
hosts/pbx/services/fieldpoc/mkphonebook.py
Normal file
|
|
@ -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 = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Telefonbuch</title>
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="Telefonbuch" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 100%%;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--input-bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
table {
|
||||
width: 100%%;
|
||||
border-collapse: collapse;
|
||||
background: var(--table-bg);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 12px;
|
||||
}
|
||||
td.extension, td.vanity {
|
||||
font-weight: bold;
|
||||
}
|
||||
td.extension {
|
||||
text-align: right;
|
||||
}
|
||||
td > a {
|
||||
text-decoration: none;
|
||||
color: var(--fg);
|
||||
}
|
||||
td span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 12px 0px;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
th {
|
||||
background-color: var(--header-bg);
|
||||
color: var(--header-fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table.only-dialin .has-no-dialin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Farbthemen */
|
||||
:root {
|
||||
--bg: #f5f5f5;
|
||||
--fg: #000;
|
||||
--border: #ccc;
|
||||
--hover: #eee;
|
||||
--header-bg: #4CAF50;
|
||||
--header-fg: white;
|
||||
--table-bg: white;
|
||||
--input-bg: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #1e1e1e;
|
||||
--fg: #f5f5f5;
|
||||
--border: #444;
|
||||
--hover: #333;
|
||||
--header-bg: #2e7d32;
|
||||
--header-fg: white;
|
||||
--table-bg: #2a2a2a;
|
||||
--input-bg: #2f2f2f;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 0px;
|
||||
}
|
||||
th, td {
|
||||
padding: 0px 4px;
|
||||
}
|
||||
td span {
|
||||
padding: 6px 0px;
|
||||
}
|
||||
th.type, td.type {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function searchTable() {
|
||||
const input = document.getElementById("searchInput").value.toLowerCase();
|
||||
const rows = document.querySelectorAll("tbody tr");
|
||||
|
||||
rows.forEach(row => {
|
||||
const name = row.dataset.name.toLowerCase();
|
||||
const ext = row.dataset.ext.toLowerCase();
|
||||
const loc = row.dataset.location.toLowerCase();
|
||||
const match = name.includes(input) || ext.includes(input) || loc.includes(input);
|
||||
row.style.display = match ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
function sortTable(n) {
|
||||
const table = document.getElementById("phoneTable");
|
||||
let switching = true;
|
||||
let dir = "asc";
|
||||
let switchcount = 0;
|
||||
|
||||
while (switching) {
|
||||
switching = false;
|
||||
const rows = table.rows;
|
||||
let tableCol = (row, col) => rows[row].querySelectorAll('td')[col].querySelector('span');
|
||||
for (let i = 1; i < (rows.length - 1); i++) {
|
||||
const x = tableCol(i, n);
|
||||
const y = tableCol(i+1, n);
|
||||
const cmp = x.textContent.trim().localeCompare(y.textContent.trim(), 'de', { numeric: true });
|
||||
|
||||
if ((dir === "asc" && cmp > 0) || (dir === "desc" && cmp < 0)) {
|
||||
// Zeilen tauschen
|
||||
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
|
||||
switching = true;
|
||||
switchcount++;
|
||||
break; // Nur ein Tausch pro Durchlauf
|
||||
}
|
||||
}
|
||||
|
||||
if (switchcount === 0 && dir === "asc") {
|
||||
dir = "desc";
|
||||
switching = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterDialin(el) {
|
||||
const table = document.getElementById("phoneTable");
|
||||
if (el.checked) {
|
||||
table.classList.add('only-dialin');
|
||||
} else {
|
||||
table.classList.remove('only-dialin');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
filterDialin(document.getElementById('filterDialin'));
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📖 Telefonbuch</h1>
|
||||
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Suche nach Name, Nummer oder Standort...">
|
||||
<br>
|
||||
<input type="checkbox" id="filterDialin" onchange="filterDialin(this)">
|
||||
<label for="filterDialin">Nur Nummern mit externer Einwahl anzeigen</label>
|
||||
<div style="overflow-x: auto;">
|
||||
<table id="phoneTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th onclick="sortTable(5)"></th>
|
||||
<th class="extension" onclick="sortTable(1)"></th>
|
||||
<th onclick="sortTable(2)">🧑 Name</th>
|
||||
<th onclick="sortTable(3)">🔤 </th>
|
||||
<th onclick="sortTable(4)">📍 Ort</th>
|
||||
<th class="type" onclick="sortTable(5)">🔧 Typ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
# 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"<span>{name}</span>"
|
||||
if dialin:
|
||||
name_cell = f'<a href="tel:{dialin}">{name_cell}</a>'
|
||||
|
||||
html_rows += f"""
|
||||
<tr class="{'has-dialin' if dialin else 'has-no-dialin'}" data-name="{name}" data-ext="{ext}" data-location="{location}" style="background-color: {row_color}">
|
||||
<td><span>{icon}</span></td>
|
||||
<td class="extension"><a href="tel:{ext}"><span>{ext}</span></a></td>
|
||||
<td>{name_cell}</td>
|
||||
<td class="vanity"><span>{vanity}</span></td>
|
||||
<td class="location"><span>{location}</span></td>
|
||||
<td class="type"><span>{typ}</span></td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html_footer = """
|
||||
</tbody>
|
||||
</div>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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()
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
46
hosts/pbx/services/unifi-controller/default.nix
Normal file
46
hosts/pbx/services/unifi-controller/default.nix
Normal file
|
|
@ -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;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
24
hosts/pbx/services/unifi-controller/unpoller.nix
Normal file
24
hosts/pbx/services/unifi-controller/unpoller.nix
Normal file
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
3
justfile
3
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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
./nix.nix
|
||||
./security.nix
|
||||
./sshd.nix
|
||||
./unfree.nix
|
||||
./zram.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
|
||||
|
|
|
|||
7
modules/unfree.nix
Normal file
7
modules/unfree.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{lib, ...}: {
|
||||
nixpkgs.config.allowUnfreePredicate = pkg:
|
||||
lib.elem (lib.getName pkg) [
|
||||
"mongodb"
|
||||
"unifi-controller"
|
||||
];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue