feat: Kamailio SIP Proxy hinzugefügt (Dotty Phone)

Kamailio als TCP-only SIP Proxy für Voice-Interface mit Openclaw.
TLS-Terminierung via Traefik auf Port 5061 (Let's Encrypt).
Zwei SIP-Accounts: tobias (Linphone iOS) und dotty (Mac Mini Asterisk).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-04-16 19:48:27 +02:00
parent 6e6ff63f08
commit 39e0052eac
5 changed files with 292 additions and 2 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
# Secrets - never commit!
.env
kamailio/kamailio-local.cfg
# Local development
*.log

View File

@ -21,6 +21,11 @@ Docker Compose Stack für **toppyr.de** — Self-Hosted Collaboration Suite.
│ Gitea │ │OpenProj.│ │ XWiki │ │ Postiz │ │ Monitoring │
│ repo. │ │ project.│ │ wiki. │ │ postiz. │ │ status./netd. │
└───────┘ └─────────┘ └─────────┘ └─────────┘ └───────────────┘
┌─────────────────────────────────────────────────────────┐
│ Kamailio SIP Proxy (TCP, TLS via Traefik) │
│ sip.toppyr.de:5061 │
└─────────────────────────────────────────────────────────┘
```
## 📦 Services
@ -39,6 +44,7 @@ Docker Compose Stack für **toppyr.de** — Self-Hosted Collaboration Suite.
| **Uptime Kuma** | status.toppyr.de | Monitoring, Alerts |
| **Netdata** | netdata.toppyr.de | System Metrics |
| **lldap** | ldap.toppyr.de | LDAP Directory |
| **Kamailio** | sip.toppyr.de:5061 | SIP Proxy (Dotty Phone) |
## 🚀 Deployment
@ -46,7 +52,7 @@ Docker Compose Stack für **toppyr.de** — Self-Hosted Collaboration Suite.
- Docker & Docker Compose v2
- Domain mit DNS-Einträgen auf Server-IP
- Ports 80, 443, 2222 (SSH für Gitea) offen
- Ports 80, 443, 2222 (SSH für Gitea), 5061 (SIP TLS) offen
### Installation
@ -70,7 +76,7 @@ Alle Subdomains als A-Record auf die Server-IP:
```
@, www, sso, wiki, project, status, crm, marketing, support,
postiz, ldap, netdata, gruen, cloud, repo → <SERVER_IP>
postiz, ldap, netdata, gruen, cloud, repo, sip<SERVER_IP>
```
## 🔐 Authentifizierung
@ -86,6 +92,9 @@ toppyr-stack/
├── docker-compose.yml # Hauptkonfiguration
├── .env.example # Umgebungsvariablen-Template
├── .env # Echte Secrets (nicht committed!)
├── kamailio/
│ ├── kamailio.cfg # SIP Proxy Config (TCP-only)
│ └── kamailio-local.cfg.example # Credentials-Template
├── landing/
│ └── index.html # Landing Page toppyr.de
└── webhooks/

View File

@ -27,9 +27,11 @@ services:
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--log.level=INFO"
- "--entrypoints.siptls.address=:5061"
ports:
- "80:80"
- "443:443"
- "5061:5061/tcp"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
@ -851,6 +853,34 @@ services:
- collaboration
restart: unless-stopped
# =============================================================================
# KAMAILIO - SIP Proxy (Dotty Phone)
# =============================================================================
# TCP-only Proxy, TLS-Terminierung via Traefik auf Port 5061.
# Zwei SIP-Accounts: tobias (Linphone iOS) und dotty (Mac Mini Asterisk).
# Siehe: dotty-phone/README.md
# =============================================================================
kamailio:
image: kamailio/kamailio-ci:5.5.2-alpine
container_name: kamailio
restart: unless-stopped
deploy:
resources:
limits:
memory: 64m
volumes:
- ./kamailio/kamailio.cfg:/etc/kamailio/kamailio.cfg:ro
- ./kamailio/kamailio-local.cfg:/etc/kamailio/kamailio-local.cfg:ro
networks:
- collaboration
labels:
- "traefik.enable=true"
- "traefik.tcp.routers.sip.rule=HostSNI(`sip.toppyr.de`)"
- "traefik.tcp.routers.sip.entrypoints=siptls"
- "traefik.tcp.routers.sip.tls=true"
- "traefik.tcp.routers.sip.tls.certresolver=letsencrypt"
- "traefik.tcp.services.sip.loadbalancer.server.port=5060"
volumes:
gitea_db_data:
gitea_data:

View File

@ -0,0 +1,13 @@
# =============================================================================
# Kamailio Credentials
# =============================================================================
# HA1-Hashes generieren:
# echo -n "username:sip.toppyr.de:PASSWORT" | md5sum | awk '{print $1}'
#
# Kopiere diese Datei nach kamailio-local.cfg und trage echte Hashes ein.
# kamailio-local.cfg wird NICHT ins Git committed!
# =============================================================================
# Credentials werden in kamailio.cfg request_route geladen:
# $sht(credentials=>tobias) = "HASH_HIER_EINSETZEN";
# $sht(credentials=>dotty) = "HASH_HIER_EINSETZEN";

237
kamailio/kamailio.cfg Normal file
View File

@ -0,0 +1,237 @@
#!KAMAILIO
#
# Dotty Phone — Kamailio SIP Proxy
# Reiner Signaling-Proxy, kein Media-Handling.
# Zwei Accounts: tobias (Linphone iOS) und dotty (Mac Mini Asterisk)
#
####### Global Parameters #######
debug=2
log_stderror=yes
fork=yes
children=2
auto_aliases=no
# SIP Listen: TCP only (TLS via Traefik Reverse Proxy)
listen=tcp:0.0.0.0:5060
####### Modules Section #######
loadmodule "kex.so"
loadmodule "tm.so"
loadmodule "tmx.so"
loadmodule "sl.so"
loadmodule "rr.so"
loadmodule "pv.so"
loadmodule "maxfwd.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
loadmodule "nathelper.so"
loadmodule "auth.so"
loadmodule "htable.so"
loadmodule "pike.so"
####### Module Parameters #######
# --- User Location (in-memory, kein DB) ---
modparam("usrloc", "db_mode", 0)
# --- Registrar ---
modparam("registrar", "default_expires", 120)
modparam("registrar", "min_expires", 60)
modparam("registrar", "max_expires", 300)
# --- NAT Helper ---
modparam("nathelper", "natping_interval", 30)
modparam("nathelper", "sipping_bflag", 7)
modparam("nathelper", "sipping_from", "sip:keepalive@sip.toppyr.de")
modparam("nathelper", "received_avp", "$avp(RECEIVED)")
# --- htable für Credentials ---
# Format: username => ha1_hash
# HA1 = MD5(username:sip.toppyr.de:password)
modparam("htable", "htable", "credentials=>size=4;initval=0;")
# --- Credentials (HA1-Hashes) ---
# tobias (Linphone iOS): hash von tobias:sip.toppyr.de:8pjd6eskjKmCihsu
# dotty (Mac Mini): hash von dotty:sip.toppyr.de:XlF11A9eeBDXbWb3
# Diese werden im request_route geladen (siehe unten)
# --- Pike (Rate Limiting) ---
modparam("pike", "sampling_time_unit", 2)
modparam("pike", "reqs_density_per_unit", 30)
modparam("pike", "remove_latency", 4)
# --- Credentials laden ---
include_file "kamailio-local.cfg"
####### Routing Logic #######
request_route {
# --- Load Credentials on Startup ---
if ($sht(credentials=>tobias) == $null) {
$sht(credentials=>tobias) = "4190b31a0d1a3fde008eb04104f2dc31";
$sht(credentials=>dotty) = "1f044db626dc2f3d6c3865fa20ae130f";
}
# --- Max Forwards ---
if (!mf_process_maxfwd_header("10")) {
sl_send_reply("483", "Too Many Hops");
exit;
}
# --- Sanity Check ---
if (!sanity_check("17895", "7")) {
xlog("L_WARN", "Malformed SIP from $si:$sp\n");
exit;
}
# --- Rate Limiting ---
if (!pike_check_req()) {
xlog("L_WARN", "Pike blocked $si\n");
sl_send_reply("503", "Service Unavailable");
exit;
}
# --- NAT Detection ---
if (nat_uac_test("19")) {
force_rport();
if (is_method("REGISTER")) {
fix_nated_register();
} else {
fix_nated_contact();
}
setbflag(7);
}
# --- REGISTER ---
if (is_method("REGISTER")) {
route(AUTH);
if (!save("location")) {
sl_send_reply("500", "Server Error");
}
exit;
}
# --- Record-Route für Dialoge ---
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
# --- In-Dialog Requests ---
if (has_totag()) {
if (loose_route()) {
if (isbflagset(7)) {
add_rr_param(";nat=yes");
}
route(RELAY);
exit;
}
# ACK ohne Route-Header
if (is_method("ACK")) {
if (t_check_trans()) {
route(RELAY);
exit;
}
exit;
}
sl_send_reply("404", "Not Found");
exit;
}
# --- Authentifizierung für alles außer ACK/CANCEL ---
if (is_method("INVITE|MESSAGE|SUBSCRIBE|NOTIFY|OPTIONS")) {
route(AUTH);
}
# --- MESSAGE (SIP-Textnachricht) ---
if (is_method("MESSAGE")) {
if (!lookup("location")) {
sl_send_reply("404", "User Not Found");
exit;
}
route(RELAY);
exit;
}
# --- INVITE ---
if (is_method("INVITE")) {
if (!lookup("location")) {
# Mac Mini offline
sl_send_reply("480", "Temporarily Unavailable");
exit;
}
route(RELAY);
exit;
}
# --- CANCEL ---
if (is_method("CANCEL")) {
if (t_check_trans()) {
t_relay();
}
exit;
}
# --- OPTIONS (Keepalive) ---
if (is_method("OPTIONS")) {
sl_send_reply("200", "OK");
exit;
}
# --- Alles andere ---
route(RELAY);
}
# --- AUTH Route ---
route[AUTH] {
if ($sht(credentials=>$au) == $null) {
if (is_method("REGISTER")) {
www_challenge("sip.toppyr.de", "0");
} else {
proxy_challenge("sip.toppyr.de", "0");
}
exit;
}
if (is_method("REGISTER")) {
if (!pv_www_authenticate("sip.toppyr.de", "$sht(credentials=>$au)", "0")) {
www_challenge("sip.toppyr.de", "0");
exit;
}
} else {
if (!pv_proxy_authenticate("sip.toppyr.de", "$sht(credentials=>$au)", "0")) {
proxy_challenge("sip.toppyr.de", "0");
exit;
}
}
consume_credentials();
}
# --- RELAY Route ---
route[RELAY] {
if (isbflagset(7)) {
add_rr_param(";nat=yes");
}
if (!t_relay()) {
sl_send_reply("500", "Relay Error");
}
}
# --- Reply Route (NAT fix für Antworten) ---
onreply_route {
if (nat_uac_test("1")) {
fix_nated_contact();
}
}
# --- Failure Route ---
failure_route[FAIL_ROUTE] {
if (t_is_canceled()) {
exit;
}
}