Mein Internetprovider (GGA Maur) bietet neu auch Telefonie (GGAdigiPhone) über das Kabelnetz an. Wir haben das Angebot bestellt, unter anderem aus dem Grunde, dass ein Telefon geschenkt wird.
Das Paket ist mit einem Mediatrix 1102 IP-Telefonie-Gerät gekommen (im folgenden als “Box” bezeichnet), den man zwischen Kabelmodem und PC anschliesst. An diese Box schliesst man dann das eigentliche Telefon per RJ11 an.
Mir war natürlich zu einfach, die Box einfach anzuschliessen und laufen zu lassen - ich wollte wissen, wie das Gerät funktioniert und habe so diesen Text geschrieben.
Die Box ist transparent, der PC merkt also nichts davon und holt sich normal per DHCP seine IP. Auch die Serveranwendungen sind weiterhin von aussen normal verfügbar.
Ich wunderte mich, wie dies möglich ist, und habe die Box an meinen Router angeschlossen. Die Box hat eine IP-Adresse von meinem DHCP-Server bezogen; das an der Box angeschlossene Telefon funktionierte aber nicht (kein Wählton war hörbar).
Aus den Logfiles des DHCP-Servers habe ich die MAC-Adresse der Box herausgefunden:
dhcpd: DHCPOFFER on 192.168.0.13 to 00:90:f8:xx:xx:xx via eth1 dhcpd: DHCPREQUEST for 192.168.0.13 from 00:90:f8:xx:xx:xx via eth1 dhcpd: DHCPACK on 192.168.0.13 to 00:90:f8:xx:xx:xx via eth1
Das Ändern der MAC-Adresse auf dem Netzwerkinterface meines Linux-Servers hat ergeben, dass der DHCP-Server dem Modem eine andere IP-Adresse zuweist, und zwar eine aus dem privaten 172.16.0.0/12-Netz:
# ifconfig eth0 down # ifconfig eth0 hw ether 00:90:f8:xx:xx:xx # dhclient eth0 DHCPOFFER from 172.20.xx.xx DHCPREQUEST on eth0 to 255.255.255.255 port 67 DHCPACK from 172.20.xx.xx bound to 172.23.xx.xx -- renewal in 7200 seconds.
Da mein lokales Netzwerk hinter NAT ist, kann die Box direkt vom Provider keine IP-Adresse beziehen, weil alle ausgehenden Pakete mit der MAC-Adresse des Servers ausgesendet werden und der DHCP-Server des Providers mir die falsche IP liefert.
Da ich die Box trotzdem hinter dem Server haben will, habe ich ein Python-Script geschrieben, das alle eingehenden Pakete auf dem LAN-Interface eth1 mit der MAC-Adresse 00:90:f8:xx:xx:xx direkt zu eth0 schiebt und umgekehrt:
#!/usr/bin/python # Interface to the internet EXTERNAL = "eth0" # Interface to our local network INTERNAL = "eth1" # MAC address of our telephone adapter MAC = "00:90:f8:xx:xx:xx" if __name__ == '__main__': import voip_bridge, os print "Starting VoIP bridge..." pid = os.fork() if pid == 0: voip_bridge.main(True, EXTERNAL, INTERNAL, MAC) os._exit(0) pid = os.fork() if pid == 0: voip_bridge.main(False, EXTERNAL, INTERNAL, MAC) os._exit(0) print "Done." os._exit(0)
import binascii, socket, pcap, dpkt # mode = True: Redirect packets from internal interface to the external interface # mode = False: Redirect packets from external interface to the internal interface def main(mode, external, internal, mac): binmac = binascii.a2b_hex(''.join(mac.split(':'))) s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW) if mode: pc = pcap.pcap(internal) pc.setfilter('ether src %s' % mac) s.bind((external, dpkt.ethernet.ETH_TYPE_ARP)) else: pc = pcap.pcap(external) pc.setfilter('ether dst %s or ether dst ff:ff:ff:ff:ff:ff' % mac) s.bind((internal, dpkt.ethernet.ETH_TYPE_ARP)) decode = { pcap.DLT_LOOP:dpkt.loopback.Loopback, pcap.DLT_NULL:dpkt.loopback.Loopback, pcap.DLT_EN10MB:dpkt.ethernet.Ethernet }[pc.datalink()] if mode: for ts, pkt in pc: # DEBUG: print 'INTERNAL', ts, `decode(pkt)` s.send(str(pkt)) else: for ts, pkt in pc: # DEBUG: print 'EXTERNAL', ts, `decode(pkt)` d = decode(pkt) if d.dst == "\xff\xff\xff\xff\xff\xff": # NOTE: This is a hack! if d.src != binmac and binmac in str(d.data): d.dst = binmac s.send(str(pkt)) else: s.send(str(pkt))
Das Script setzt dpkt und pypcap voraus.
Das eingehende Paket auf die Antwort der DHCP-Response war komischerweise an die MAC-Adresse ff:ff:ff:ff:ff:ff adressiert. Um das Paket trotzdem eindeutig zuordnen zu können und richtig weiterleiten zu können, habe ich if-Abfrage in der zweiten for-Schlaufe eingebaut. Diese ist aber ein Hack, da sie einfach prüft, ob die MAC-Adresse der Box im Paketinhalt vorhanden ist. Ausserdem wird die Ziel-MAC-Adresse dann korrigiert, da die Box die IP sonst nicht fressen wollte und weitere DHCP-Requests aussendete.
Beim Importieren des pypcap-Moduls hatte ich am Anfang übrigens Probleme. Es ist nämlich mit folgender Fehlermeldung fehlschlagen:
ImportError: /usr/lib/python2.3/site-packages/pcap.so: undefined symbol: pcap_close
Abhilfe schuf das Setzen von #define HAVE_PCAP_COMPILE_NOPCAP 0 in der config.h von pypcap.
Das Script wird am besten beim Aufsetzen des Interfaces ausgeführt. Ich habe deshalb in die /etc/network/interfaces folgende Zeilen hinzugefügt (es wird angenommen, dass es sich um ein Debian-System handelt und die Python-Scripts sich in /etc/network befinden):
up killall -9 voip.py up cd /etc/network && ./voip.py
In meinem iptables-Script fügte ich noch folgende Zeile ein, um die Pakete der Box zu blocken, weil jetzt ja mein Script mit diesen Paketen hantiert:
$IPT -t nat -A PREROUTING -i $LIF -m mac --mac-source $VOIP -j DROP
Vielleicht sollte man dies auch in die andere Richtung tun (vielleicht ist es auch komplett unnötig), jedoch konnte ich bei iptables keine –mac-destination-Option finden. Hat da jemand genauere Informationen?
Als weitere Hürde stellte sich noch mein DHCP-Server heraus, der die DHCP-Requests des Modems trotz der iptables-Regel mit einem DHCPNAK quittierte, sodass das arme Modem keine IP-Adresse bekam. Mit folgendem Eintrag brachte ich den DHCP-Server dann doch dazu, die Requests zu ignorieren:
host mediatrix { hardware ethernet 00:90:f8:xx:xx:xx; ignore booting; }
Leider kam dabei immer folgende Nachricht in /var/log/daemon.log:
dhcpd: /etc/dhcpd.conf line 66: expecting a parameter or declaration. dhcpd: ignore dhcpd: ^ dhcpd: Configuration file errors encountered -- exiting dhcpd: exiting.
Wie ich durch Probieren herausgefunden habe, löste ein apt-get install dhcp3-server das Problem (vorher war das Paket dhcp installiert).
Jetzt endlich bekam die Box eine IP-Adresse und sogar der Wählton war zu hören!
Jetzt, da die Box hinter dem Server angeschlossen ist, lässt sich der Verkehr mit einem Sniffer auf dem Server abhören. Bei einem eingehenden Anruf sieht das beispielsweise so aus:
U 81.7.xxx.xxx:2727 -> 172.23.xxx.xxx:2427 RQNT xxxxxxxx aaln/xx@mxxxxxxxxxxxx MGCP 1.0.Q:loop,process.R:G/ft(N),G/mt(N).S:L/rg,G/ rt@xxxxxxxxx,L/ci(xx/xx/xx/xx,"TELEFONNUMMER",).X:xxxxxxxxxxxxxxxx.
Erfolgt der Anruf anonym, erscheint statt der Telefonnummer ein P.
Ich habe ein Script geschrieben, welches folgendes tut:
Hier sind nun die benötigten Dateien:
#!/usr/bin/python import pcap, dpkt, re import syslog import voip_speak def speak(string, lookup=True): voip_speak.speak('/path/to/your/speak.sh', str(string), '/path/to/your/phone.db', lookup) def main(external, mac, port): pc = pcap.pcap(external) pc.setfilter('ether dst %s and dst port %s' % (mac, port)) decode = { pcap.DLT_LOOP:dpkt.loopback.Loopback, pcap.DLT_NULL:dpkt.loopback.Loopback, pcap.DLT_EN10MB:dpkt.ethernet.Ethernet }[pc.datalink()] p = re.compile('S:.*ci\((.*)\)') for ts, pkt in pc: data = decode(pkt).data if data and isinstance(data, dpkt.ip.IP): ipdata = data.data if isinstance(ipdata, dpkt.udp.UDP): try: lines = ipdata.data.split('\n') cmd = lines[0].split(' ')[0] if cmd == 'RQNT': # parse the stuff for line in lines: match = p.match(line) if match: try: number = match.groups()[0].split(',')[1].split('"')[1] speak(str(number)) print number except: pass except: pass if __name__ == '__main__': import os pid = os.fork() if pid == 0: syslog.openlog('voip_calldetect') main('eth0', '00:90:f8:xx:xx:xx', '2427') os._exit(0) os._exit(0)
import syslog, re import subprocess import voip_lookup def go_lookup(string): return voip_lookup.lookup(string) def shell_escape(s): s = s.replace('\\', '\\\\') s = s.replace("'", "'\\''") return "'"+s+"'" def speak(executable, number, dbfn, lookup=True): dbfd = open(dbfn) dbstr = dbfd.read() dbfd.close() db = {} for line in dbstr.split('\n'): l = line.split('\t', 1) if len(l) == 2: db[l[0].strip()] = l[1].strip() try: say = db[number] except: if lookup: try: say = go_lookup(number) except: say = str(number) else: say = str(number) syslog.syslog(syslog.LOG_NOTICE, say) # We need to shell_escape() it subprocess.call(['su', 'tom', executable, shell_escape(say)])
#!/usr/bin/python # -*- coding: latin-1 -*- import urllib, re def lookup(number): url = 'http://tel.search.ch/result.html?name=&misc=&strasse=&ort=&kanton=&tel='+number cantons = {'ZH':'Zürich', 'BE':'Bern', 'VD':'Waadt', 'AG':'Aargau', 'SG':'Sankt Gallen', 'GE':'Genf', 'LU':'Luzern', 'TI':'Tessin', 'VS':'Wallis', 'BL':'Basel Land', 'SO':'Solothurn', 'FR':'Fribourg', 'TG':'Thurgau', 'BS':'Basel Stadt', 'GR':'Graubünden', 'NE':'Neuenburg', 'SZ':'Schwyz', 'ZG':'Zug', 'SH':'Schaffhausen', 'JU':'Jura', 'AR':'Appenzell', 'GL':'Glarus', 'NW':'Nidwalden', 'UR':'Uri', 'OW':'Obwalden', 'AI':'Appenzell'} try: f = urllib.urlopen(url) lines = f.read().split('\n') f.close() except: return 'Verbindung mit Telefonbuch fehlgeschlagen. %s' % number else: rname = re.compile('.*<div class="rname"><a href.*>(.*)<\/a><\/div>') raddr = re.compile('.*<div class="raddr"><span class="tel_addrpart">(.*)<\/span>.* ' '<span class="tel_addrpart">(.*)</span>') roccu = re.compile('.*<div class="roccu">(.*)<\/div>') name = '' street = '' city = '' occu = '' for line in lines: if not occu: match = roccu.match(line) if match: try: occu = match.groups()[0].strip() # occu can be very long, so just use the first four words occu = ' '.join(occu.split(' ')[0:4]) except: pass if not name: match = rname.match(line) if match: try: name = match.groups()[0].strip() except: pass if not street or not city: match = raddr.match(line) if match: try: street = match.groups()[0].strip() city = match.groups()[1].strip() except: pass if street and city and name: break if name and city: arr = city.split(' ')[1].split('/') #return 'Anruf von %s. %s. %s. %s' % (name, street, city, number) try: arr[1] = cantons[arr[1]] except: arr[1] = arr[1].lower() city = '%s. Kanton %s' % (arr[0], arr[1]) if arr[0] == arr[1]: city = 'Stadt %s' % arr[0] if occu: return 'Anruf von %s. %s. %s' % (name, occu, city) else: return 'Anruf von %s. %s' % (name, city) else: if name: if occu: #return 'Anruf von %s. %s. %s' % (name, occu, number) return 'Anruf von %s. %s' % (name, occu) else: return 'Anruf von %s' % name #return 'Anruf von %s. %s' % (name, number) else: return 'Nicht im Telefonbuch. %s' % number
Im ersten Script müssen die Pfade in der speak()-Funktion und die MAC-Adresse (ganz unten) angepasst weden. /path/to/your/speak.sh wird durch ein Shellscript ersetzt. Diesem Shellscript wird der zu sprechende Text übergeben. Es ist somit zuständig, dass der Text vorgelesen wird. Bei mir sieht es folgendermassen aus:
#!/bin/sh cd /home/tom/mbrola && echo $@ . $@ | txt2pho/pipefilt/pipefilt | txt2pho/preproc/preproc txt2pho/preproc/Hadifix.abk | txt2pho -f | mbrola -t 1.0 -f 1.0 de3/de3 - -.wav | ssh tom@ibook bplay
Wie im Script zu sehen ist, wird der Text doppelt abgespielt ($@ . $@). Ausserdem benutze ich MBROLA (leider Closed Source) und txt2pho (leider auch Closed Source und auf wenigen Plattformen lauffähig). Die Einrichtung von MBROLA mit txt2pho ist relativ komplex, jedoch werde ich darauf in diesem Text nicht eingehen. Da ich in meinem Server keine Soundkarte habe, wird die erzeugte Sounddatei am Schluss noch per SSH an iBook gesendet und mit bplay abgespielt.
Der zweite Pfad (/path/to/your/phone.db) wird durch den Pfad zur Textdatenbankdatei ersetzt. Diese Datei sieht zum Beispiel so aus:
P Anonymer Anruf 0123456789 Anruf von Hans Muster 0987654321 Anruf von Peter Mustermann
Im zweiten Script sollte in der letzten Zeile tom durch den jeweiligen Benutzer ersetzt werden, da man vermutlich die Soundateien nicht als root generieren will.
Auch diese Script setzen dpkt und pypcap voraus.
Ist alles eingerichtet, kann man den Daemon als root folgendermassen starten: python voip_calldetect.py
Bei einem eingehenden Anruf müsste nun der Namen des Anrufers oder wenigstens seine Telefonnummer vorgelesen werden.
Bei Fragen, Kommentaren oder Verbesserungsvorschlägen, zögere nicht, mich per E-Mail zu kontaktieren unter:
tom.eggdrop.ch (ersetze den ersten Punkt mit einem @).
Letzte Änderung: 20.09.20
Dieser Text gehört zu http://www.eggdrop.ch/ und ist unter einer Creative Commons Lizenz lizensiert.