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:
parent
6e6ff63f08
commit
39e0052eac
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
# Secrets - never commit!
|
# Secrets - never commit!
|
||||||
.env
|
.env
|
||||||
|
kamailio/kamailio-local.cfg
|
||||||
|
|
||||||
# Local development
|
# Local development
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
13
README.md
13
README.md
@ -21,6 +21,11 @@ Docker Compose Stack für **toppyr.de** — Self-Hosted Collaboration Suite.
|
|||||||
│ Gitea │ │OpenProj.│ │ XWiki │ │ Postiz │ │ Monitoring │
|
│ Gitea │ │OpenProj.│ │ XWiki │ │ Postiz │ │ Monitoring │
|
||||||
│ repo. │ │ project.│ │ wiki. │ │ postiz. │ │ status./netd. │
|
│ repo. │ │ project.│ │ wiki. │ │ postiz. │ │ status./netd. │
|
||||||
└───────┘ └─────────┘ └─────────┘ └─────────┘ └───────────────┘
|
└───────┘ └─────────┘ └─────────┘ └─────────┘ └───────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Kamailio SIP Proxy (TCP, TLS via Traefik) │
|
||||||
|
│ sip.toppyr.de:5061 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📦 Services
|
## 📦 Services
|
||||||
@ -39,6 +44,7 @@ Docker Compose Stack für **toppyr.de** — Self-Hosted Collaboration Suite.
|
|||||||
| **Uptime Kuma** | status.toppyr.de | Monitoring, Alerts |
|
| **Uptime Kuma** | status.toppyr.de | Monitoring, Alerts |
|
||||||
| **Netdata** | netdata.toppyr.de | System Metrics |
|
| **Netdata** | netdata.toppyr.de | System Metrics |
|
||||||
| **lldap** | ldap.toppyr.de | LDAP Directory |
|
| **lldap** | ldap.toppyr.de | LDAP Directory |
|
||||||
|
| **Kamailio** | sip.toppyr.de:5061 | SIP Proxy (Dotty Phone) |
|
||||||
|
|
||||||
## 🚀 Deployment
|
## 🚀 Deployment
|
||||||
|
|
||||||
@ -46,7 +52,7 @@ Docker Compose Stack für **toppyr.de** — Self-Hosted Collaboration Suite.
|
|||||||
|
|
||||||
- Docker & Docker Compose v2
|
- Docker & Docker Compose v2
|
||||||
- Domain mit DNS-Einträgen auf Server-IP
|
- 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
|
### Installation
|
||||||
|
|
||||||
@ -70,7 +76,7 @@ Alle Subdomains als A-Record auf die Server-IP:
|
|||||||
|
|
||||||
```
|
```
|
||||||
@, www, sso, wiki, project, status, crm, marketing, support,
|
@, 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
|
## 🔐 Authentifizierung
|
||||||
@ -86,6 +92,9 @@ toppyr-stack/
|
|||||||
├── docker-compose.yml # Hauptkonfiguration
|
├── docker-compose.yml # Hauptkonfiguration
|
||||||
├── .env.example # Umgebungsvariablen-Template
|
├── .env.example # Umgebungsvariablen-Template
|
||||||
├── .env # Echte Secrets (nicht committed!)
|
├── .env # Echte Secrets (nicht committed!)
|
||||||
|
├── kamailio/
|
||||||
|
│ ├── kamailio.cfg # SIP Proxy Config (TCP-only)
|
||||||
|
│ └── kamailio-local.cfg.example # Credentials-Template
|
||||||
├── landing/
|
├── landing/
|
||||||
│ └── index.html # Landing Page toppyr.de
|
│ └── index.html # Landing Page toppyr.de
|
||||||
└── webhooks/
|
└── webhooks/
|
||||||
|
|||||||
@ -27,9 +27,11 @@ services:
|
|||||||
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
|
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
|
||||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||||
- "--log.level=INFO"
|
- "--log.level=INFO"
|
||||||
|
- "--entrypoints.siptls.address=:5061"
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
- "5061:5061/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- traefik_letsencrypt:/letsencrypt
|
- traefik_letsencrypt:/letsencrypt
|
||||||
@ -851,6 +853,34 @@ services:
|
|||||||
- collaboration
|
- collaboration
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
gitea_db_data:
|
gitea_db_data:
|
||||||
gitea_data:
|
gitea_data:
|
||||||
|
|||||||
13
kamailio/kamailio-local.cfg.example
Normal file
13
kamailio/kamailio-local.cfg.example
Normal 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
237
kamailio/kamailio.cfg
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user