Zum Blog
12. Mai 2026·15 Min. Lesezeit

KI-Telefonassistent selber bauen: FreeSWITCH, SIP-Trunk und eine eigene Voice-Pipeline

Wenn jemand meine geschäftliche Nummer anruft, hebt nicht ich ab — sondern Hank, mein selbst gebauter KI-Telefonassistent. Hank führt ein natürliches Gespräch in Echtzeit, hat Zugriff auf meine Kontakte und kann Anrufe übergeben oder auflegen. Hier beschreibe ich Schritt für Schritt, wie so ein Setup auf einem eigenen VPS aufgebaut ist — ohne Twilio, ohne Vapi, ohne fremden Voice-Agent-Anbieter.

Der Stack auf einen Blick

Vier Schichten greifen ineinander. Ganz unten der SIP-Trunk bei einem österreichischen Telekom-Anbieter (peoplefone) mit einer österreichweiten 0720-Nummer. Darüber FreeSWITCH als Open-Source-Telefonanlage, das den SIP-Verkehr terminiert und Audio-Frames an das Backend weiterreicht. Im Backend läuft die Voice-Pipeline: Speech-to-Text (Google Cloud), ein LLM-Provider deiner Wahl (Anthropic, Groq, OpenAI, …) und Text-to-Speech (Google Cloud). Den Abschluss bildet die Agent-Logik mit Tool-Use, Kontakt-Lookup und Pre-rendered Greetings.

Alles läuft auf einem einzigen Hetzner-VPS. Keine Cloud-Funktionen, keine Twilio- Gebühren pro Minute, keine fremde TLS-Terminierung für Audio. Der Aufwand zahlt sich erst aus, wenn man entweder Datenschutz-Anforderungen hat, die kein SaaS abdecken kann, oder wenn man Provider-Lock-in (Latenz, LLM-Wahl, Sprachen) vermeiden will.

Schritt 1 — Der SIP-Trunk: peoplefone

Ein SIP-Trunk ersetzt die alte Telefonleitung durch eine VoIP-Verbindung übers Internet. Ich nutze peoplefone (AT) — österreichischer Provider, klare CIDR-Bereiche zum Whitelisten, IP-Authentifizierung möglich, gute SIP-Standardkonformität. Trunk-Host ist sips.peoplefone.at (95.128.80.3), Failover-Range in Deutschland (185.190.125.0/27). DID: eine 0720-Nummer (österreichweit, ortsunabhängig), die Hardware liegt komplett bei mir.

Praktisch ist nicht der Trunk-Provider, sondern welche IP-Bereiche er veröffentlicht. Peoplefone listet alle Outbound-IPs in der Knowledge-Base — das ist die wichtigste Information für die spätere Firewall-Konfiguration. Wer einen Provider ohne dokumentierte CIDRs nimmt, kann den Trunk später nicht sauber whitelisten.

Schritt 2 — FreeSWITCH als SIP-Brücke

FreeSWITCH ist seit über 15 Jahren die Standard-Open-Source-Telefonanlage und kann praktisch alles: SIP-Trunks terminieren, Codecs aushandeln, Dialpläne ausführen, Audio-Streams aufsplitten. Ich betreibe es als Docker-Container im host network mode, damit es direkt auf den öffentlichen SIP-Ports (5060/5061) lauscht. Das Image stammt aus einem Standard-Build, die Konfiguration lebt in einem Host-Bind-Mount.

Drei Konfigurationsdateien sind zentral. Erstens das externe SIP-Profil (sip_profiles/external.xml): bindet die Codecs (G.711a/μ, OPUS) und setzt die Inbound-ACL. Zweitens die Gateway-Datei mit den Trunk-Credentials. Drittens der öffentliche Dialplan (dialplan/public/...) für eingehende Anrufe und ein Default-Dialplan für ausgehende Anrufe.

Audio wird über das Modul mod_audio_fork via WebSocket an das Backend gestreamt. Das Backend bekommt PCM-8-kHz-Frames in Echtzeit, schickt synthetisierte Antworten zurück, und FreeSWITCH spielt sie in den laufenden Anruf ein. Latenz im Optimalfall: 60–100 ms zwischen Sprech-Ende und Audio-Frame-Empfang im Backend.

Stolperfallen aus der Praxis: Edits an Konfig-Dateien müssen im richtigen Pfad landen — das entrypoint.sh im Standard-Image schreibt nach /etc/freeswitch/..., FreeSWITCH selbst liest aber aus /usr/local/freeswitch/conf/.... Und docker rm killt Änderungen, die nur im Container-Writable-Layer leben — deshalb gehört jede Anpassung zusätzlich in den Host-Bind-Mount.

Schritt 3 — Die eigene Voice-Pipeline statt Gemini Live

Google bietet mit Gemini Live eine fertige End-to-End-Pipeline an, die Sprache rein- und Sprache rausbekommt. Die ist beeindruckend, aber sie bringt drei unangenehme Eigenschaften mit: Provider-Lock-in (nur Google), Latenz hängt am Google-Server, und Tool-Use ist eingeschränkterals bei Anthropic Messages oder OpenAI-kompatiblen APIs (Groq, Mistral, DeepSeek, …).

Deshalb läuft Hank auf einer eigenen Pipeline, die drei separate Module hat. STT und TTS kommen aus der Google Cloud (Speech-to-Text per gRPC-Streaming mit Modell latest_long, Text-to-Speech REST mit der Stimme de-DE-Chirp-HD-D). Das LLM dazwischen ist austauschbar: Anthropic mit nativem Tool-Use und SSE-Streaming, oder jede OpenAI-kompatible API mit tool_calls. Pro Telephony-Einstellung wählt man explizit, welcher Provider antwortet.

Die wichtigste Optimierung heißt Sentence-by-Sentence-Streaming. Sobald der LLM-Output einen ersten ganzen Satz fertig hat (erkannt an .!?…-Boundaries), wandert dieser Satz schon an Text-to-Speech, während das LLM den nächsten generiert. Wahrgenommene Antwortzeit halbiert sich, weil der Anrufer den Anfang der Antwort hört, bevor das Ende überhaupt formuliert ist.

Genauso wichtig: Barge-In. Wenn der Anrufer Hank ins Wort fällt, erzeugt STT bereits Interim-Transkripte. Die Pipeline inkrementiert eine turn-id und verwirft alle laufenden LLM- und TTS-Jobs — Hank schweigt sofort, hört zu, antwortet erst danach. Ohne Barge-In wirkt der Bot maschinenhaft, weil er stumpf weiterredet.

Für Voice ist die LLM-Wahl entscheidend. Anthropic Sonnet liefert die beste Qualität, braucht aber ~300 ms bis zum ersten Token. Groq mit Llama-3.3-70Bschafft unter ~50 ms — der Unterschied zwischen Bot-Gefühl und natürlichem Gespräch. Ich nutze Anthropic für komplexe Anfragen, Groq für reine Voice-Sessions.

Schritt 4 — Pre-rendered Greetings: 0 ms statt 1500 ms

Eine echte TTS-Anfrage braucht 800–1500 ms vom Trigger bis zum ersten Audio-Frame. Bei jedem Anruf den ersten Satz live zu synthetisieren wäre also der schlechteste Eindruck überhaupt — der Anrufer hört Stille. Die Lösung: Greetings vorrendern und cachen.

Sobald sich der Begrüßungstext, die Stimme oder ein Kontakt ändert, läuft im Hintergrund ein warmGreetingsAsync(), das die TTS-Datei als PCM-8-kHz-File im Greetings-Cache ablegt. Cache-Key ist Contact + Voice + Hash(Text). Beim Anruf erkennt der Inbound-Handler den Anrufer über die Caller-ID, lädt das passende File und schickt es als ersten Audio-Frame raus — 0 ms Latenz. Unbekannte Anrufer bekommen ein generisches Greeting mit Aufforderung, den Namen zu nennen.

Schritt 5 — Toll-Fraud: Was passiert, wenn SIP offen ist

Hier kommt der unbequeme Teil. Wenn dein FreeSWITCH öffentlich erreichbar ist, wirst du angegriffen. Nicht „vielleicht", sondern garantiert. Innerhalb von Stunden nach der ersten öffentlichen DNS-Auflösung schlagen die ersten SIP-Probes auf. Ich habe das selbst unterschätzt — und musste hinterher 13.283 CDR-Einträge aufräumen.

Was die Bots versuchen: Sie schicken massenhaft REGISTER-Pakete mit gefälschten Caller-IDs (test, trunk1, 1001, sipvicious), probieren INVITE-Pakete an exotische Ziel-Nummern (Irak, Inmarsat, Schweiz) und spielen vor allem auf IPRN-Nummern: International Premium Rate Numbers. Beispiel: 8818899199 ist eine Inmarsat-Premium-Nummer. Würde so ein Anruf durchkommen, kostet jede Minute echtes Geld — und der Anteil fließt an den Angreifer. Klassisches Toll-Fraud.

In meinem Spitzentag habe ich 3.787 Probes an einem einzigen Taggemessen. Top-Angreifer-IPs kamen von Contabo (132k Probes), Telia (122k), OVH und AWS Tokyo. Das sind keine gezielten Angriffe — das ist Hintergrund-Rauschen des Internets. Aber dieses Rauschen genügt, um ein schlecht konfiguriertes System auszunehmen.

Schritt 6 — Hardening Phase A

Vier Verteidigungslinien, in genau dieser Reihenfolge. Jede einzelne reduziert die Angriffsfläche um eine Größenordnung.

1. UFW-Whitelist auf den SIP-Ports. Die Ports 5060/5061/5080/5081 dürfen nur noch Pakete vom Trunk-Provider sehen. Bei mir sind das zwei CIDRs: 95.128.80.0/29 (peoplefone AT) und 185.190.125.0/27 (peoplefone DE). Insgesamt acht Regeln — fertig. Das blockiert allein schon ~99 % des Bot-Traffics auf Netzwerkebene, ohne dass FreeSWITCH überhaupt einen Paket-Header lesen muss.

2. FreeSWITCH-ACL. Defense-in-depth: derselbe IP-Bereich wird auch in autoload_configs/acl.conf.xml hinterlegt, und das externe SIP-Profil referenziert ihn als apply-inbound-acl="trusted-providers". Falls die UFW jemals umgangen wird (Misskonfiguration nach Server-Migration, IPv6-Lücke), filtert FreeSWITCH selbst nochmal.

3. Inbound-Dialplan mit Caller-ID-Whitelist und DID-Match. Anonyme Anrufe (kein From-Header, „anonymous") werden gedroppt. Erlaubte Caller-ID-Präfixe: AT/DE/CH/IT. Die Ziel-Nummer muss exakt der eigenen DID entsprechen (^(\+?43720271025)$). Alles andere → Reject. Damit kann der Trunk gar nicht dazu missbraucht werden, dass jemand über deine echte Nummer zu einer Drittnummer telefoniert.

4. Outbound-Dialplan mit Premium-Block. Hier sterben die meisten Hobby-Setups: ein offener Catch-all ^(\+\d+)$ bedeutet, dass jeder mit Trunk-Zugriff überallhin telefonieren kann. Stattdessen explizite Whitelist AT/DE/CH/IT, davor sechs Premium-Block-Regeln: AT 0900/0820/0939/010/0118, DE 0900/0137/0180/018x, CH 0900/0901/0906, IT 89x/144/155/199, dazu Inmarsat (+882), UIFN (+883/+979). Wer auf einer dieser Nummern landet, fliegt raus, bevor der Anruf überhaupt aufgebaut wird.

Verifikation läuft über fs_cli: ein simples acl 95.128.80.3 trusted-providers muss true liefern, dieselbe Abfrage für eine Angreifer-IP false. sofia status gateway zeigt den Trunk-Registration-Status, ufw status verbose die erlaubten Quellen. Wenn diese drei Checks grün sind, ist Phase A abgeschlossen.

Schritt 7 — Was offen bleibt (Phase B+)

Phase A bringt das System aus der Schusslinie. Für einen produktiven Dauerbetrieb gibt es weitere Schritte, die ich der Reihe nach abarbeite: fail2ban für FreeSWITCH(NO_ROUTE-Einträge und Auth-Fails führen automatisch zu 24h-Bans), Trunk-Password-Rotationmit aktiviertem IP-Lock beim Provider, Audit-Log für jede Outbound-Initiation aus dem Backend (auch lokale Prozesse müssen einen Token vorzeigen), und ein Server-Guardian-Digest, das pro Tag SIP-Probe-Zahlen und Outbound-Counter per E-Mail rausschickt. Erst dann wirkt das Setup operativ stabil.

Was kostet eine Minute?

Die Kosten teilen sich in einen Fixblock und variable Minutengebühren. Fixkosten: die DID-Nummer (0720) kostet bei peoplefone €20,00 netto pro Jahr(€24,00 brutto) — also etwa €1,67/Monat. Pro laufender Gesprächsminute kommen vier variable Posten dazu:

  • peoplefone SIP-Trunk: ~€0,009/min inbound (jemand ruft an), ~€0,076/min outbound auf AT-Mobil, ~€0,022/min auf AT-Festnetz.
  • Google STT (Chirp 3 HD): $0,016/min Audio — da der Caller nur ~35 % der Gesprächszeit spricht, kostet das effektiv ~€0,005/min.
  • Groq LLM (Llama-3.3-70B): $0,59/1M Input + $0,79/1M Output-Token, bei ~2 Turns/min rund ~€0,002/min.
  • Google TTS (de-DE-Chirp-HD-D, Studio-Tier): $160/1M Zeichen — der mit Abstand teuerste Posten, ~€0,027/min. Eine günstigere Alternative (Neural2, $16/1M) kostet zehnmal weniger (~€0,003/min) bei etwas niedrigerer Sprachqualität.

Gesamt pro Gesprächsminute (inbound, mit Chirp-HD TTS): ~€0,04/min. Mit Neural2 TTS sinkt das auf ~€0,02/min.

Hochrechnung auf 500 Inbound-Minuten/Monat:

PostenChirp-HDNeural2
DID-Nummer (Anteil)€1,67€1,67
peoplefone SIP€4,50€4,50
Google STT€2,58€2,58
Groq LLM€1,14€1,14
Google TTS€13,54€1,35
Total (netto)€23,43€11,24

Alle Angaben netto. USD/EUR-Kurs ~1:0,92. Peoplefone-Minutenpreise sind Richtwerte — die exakten Tarife findest du im peoplefone-Portal.

Was du daraus mitnehmen kannst

1. Ein eigener KI-Telefonassistent ist heute mit Open-Source-Bausteinen (FreeSWITCH + STT/TTS + LLM deiner Wahl) machbar. Die Infrastruktur kostet wenig (ein 8-€-VPS reicht), aber die laufenden API-Kosten für STT, TTS und LLM kommen obendrauf — bei Google Cloud + Anthropic/Groq je nach Volumen wenige bis dreistellige Euro pro Monat. Wirklich null Cloud-Kosten gibt es nur mit voll self-hosted Stack (Whisper für STT, Coqui/Piper für TTS, lokales Llama-Modell) — auf Kosten von Qualität und CPU/GPU-Bedarf.

2. Die größte Stellschraube für Latenz ist nicht das Modell, sondern die Architektur: Pre-rendered Greetings (Null-Latenz beim Abheben), Sentence-by-Sentence-TTS (halbierte Antwortzeit) und Barge-In (kein Reden ins Wort).

3. Provider-Agnostik ist kein Luxus, sondern eine Versicherung. Sprache, Geschwindigkeit, Kosten und Compliance ändern sich — die Pipeline sollte den LLM-Provider wechseln können, ohne dass FreeSWITCH oder TTS angefasst werden.

4. Wenn dein SIP-Port offen ist, wirst du angegriffen. Provider-CIDR-Whitelist auf der Firewall ist die wichtigste Einzel-Verteidigung. Ohne sie ist alles andere (ACL, Dialplan, Premium-Block) nur Schadensbegrenzung.

5. Outbound-Dialpläne ohne Catch-all — das ist die Regel, die Premium-Rate-Fraud verhindert. Lieber zu eng anfangen und Nummern manuell freischalten, als zu locker und mit einer Telefon-Rechnung im vierstelligen Bereich aufwachen.

Das ganze Setup steckt bei mir hinter einer einzigen 0720-Nummer. Bei Interesse am Code (Voice-Pipeline, Greeting-Cache, Hardening-Snippets) — der dazugehörige KI-Mitarbeiter HeyHank wird mit dem Public-Launch unter MIT-Lizenz veröffentlicht.

Fragen oder Feedback? office@markusstoeger.com