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!
|
||||
.env
|
||||
kamailio/kamailio-local.cfg
|
||||
|
||||
# Local development
|
||||
*.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 │
|
||||
│ 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/
|
||||
|
||||
@ -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:
|
||||
|
||||
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