Digitale Telefonie mit einem Mediatrix 1102 IP-Telefonie-Gerät

Einleitung

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.

Einrichten der Box hinter einem Linux-Server mit NAT

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:

voip.py

#!/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)

voip_bridge.py

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!

Automatische Ansage des Anrufenden

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:

  1. Alle Pakete des Telefonadapters werden ausgelesen
  2. Handelt es sich um ein RQNT-Paket (NotificationRequest), wird die Telefonnummer aus dem Paket ausgelesen
  3. Es wird geschaut, ob die Telefonnummer in einer internen Datenbank bzw. Textdatei vorhanden ist
    • Wenn ja, wird der entsprechende Eintrag ausgelesen und vorgelesen
    • Wenn nein, startet das Script eine Anfrage an das Telefonbuch (tel.search.ch) und liest die entsprechenden Daten vor
  4. Der Anruf wird durch syslog in einem Logfile registriert.

Hier sind nun die benötigten Dateien:

voip_calldetect.py

#!/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)

voip_speak.py

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)])

voip_lookup.py

#!/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.

Kontakt

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 @).


Zurück zu www.eggdrop.ch

Letzte Änderung: 02.03.13

Creative Commons License

Dieser Text gehört zu http://www.eggdrop.ch/ und ist unter einer Creative Commons Lizenz lizensiert.