fritzcap: Tool für Etherreal Trace und Audiodaten-Extraktion v2.0

Yellow99

Neuer User
Mitglied seit
8 Feb 2020
Beiträge
11
Punkte für Reaktionen
0
Punkte
1
Werden nur 2 .wav-Files erzeugt oder mehrere?
Also hab heut mal nachgesehen. Ich hab einige 'Schrott'-Aufnahmen, da sind 3 Dateien im Verzeichnis (also 0 - 2), erste ist relativ klein, so ca. 4000 - 5000 Byte und scheint keine Audio zu enthalten. Das sind aber alles so Sachen, wo jemand nur den Hörer abgenommen hat, oder belegt war o.Ä.

Bei RICHTIGEN Telefongesprächen hab ich (bisher) immer exakt 2 Dateien gehabt, wie es sein soll. Waren aber auch keine riesenlangen Dauergespräche bisher.

Wer sich mit Python auskennt, woraus wird die MIXED Datei denn nun generiert? Aus den fertigen .WAV (dann sollte man den Fehler doch leicht finden können) oder ebenfalls direkt aus dem .CAP ?
 

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Die mixed-Datei wird in der g711_decoder.py erzeugt:
Es sollen nur 2 wav.files dekodiert werden, egal wie lang das Gespräch dauerte(Anrufer/Angerufener).

Anschließend werden diese syncronisiert zu einem zusammengefügt:

Code:
def finalize(self):
        sdfound = []
        # Finalize wave file, patch header
        for sd in self.sda:
            # Finalize RIFF header
            self.write_RIFF_header(sd)

            if self.mix:
                # Find associated streams to mix
                for sdother in self.sda:
                    if sd['source'] == sdother['destination'] and sdother['source'] == sd['destination']:
                        if sd['index'] not in sdfound and sdother['index'] not in sdfound:
und genau hier
Code:
if sd['source'] == sdother['destination'] and sdother['source'] == sd['destination']:
ist das Problem.

Der Startwert des zweiten wav.files entspricht nicht dem Endwert des ersten wav-files. und somit wird es nicht erzeugt. Hier bricht die Schleife ab.
Somit komme ich zu dem Schluß, dass die Dekodierung aus dem Cap-file nicht mehr paßt, eventuell ist eben der header bei IPv6 anders.


Hier werden die wav-files erzeugt:

Code:
def decode(self, time_sec, time_usec, packet, len):
        # This is a simple parser. The payload is not fully decoded. Instead some assumptions based upon the
        # length of the captured package are checked
        for candidate in self.lenmap:
            if len == candidate['len']:
                # Read 12 byte RTP header data in front of the audio payload and analyze it
                # Important: SSI (Synchronization Source Identifier), sequence number, payload type and timestamp
                (streamsetup, payloadtype, seqnr, timestamp, ssi) = struct.unpack('>BBHLL', packet[candidate['offs']-12 : candidate['offs']])
Und hier stimmt schon vermutlich etwas nicht mit unserer Paketlänge (# Read 12 byte RTP header), weil mehr als 2 wav-files erzeugt werden.
Code:
 struct.unpack('>BBHLL', packet[candidate['offs']-12 : candidate['offs']]
Dazu fehlt mir aber die genaue Kenntnis der RTP-Pakete, warum ich jetzt (bevor ich lange suche) ein anderes Script (welches ein anderes Verfahren zum dekodieren benutzt) zum dekodieren verwende.
(Siehe hier: https://www.ip-phone-forum.de/threads/fritzcap-tool-für-etherreal-trace-und-audiodaten-extraktion-v2-0.232682/post-2357440)

Wenn du mehr als 2 wav.files in deinem Verzeichnis hast, ist genau das bei dir auch das Problem.

-- Zusammenführung Doppelpost by stoney

Wenn ich das sagen wir mal auf meinem vernetzten Linux Sat-Receiver nutzen will...
Richte dir doch einfach einen Raspi 3 ein und lasse den für FritzCap laufen.
Kann dir auch gern ein Image mit meiner jetztigen Konfiguration machen, welches du einfach auf eine SD-Karte kopierst und fertig. Kannst auch selber machen - Ist einfach und nicht teuer.
 
Zuletzt bearbeitet:

Yellow99

Neuer User
Mitglied seit
8 Feb 2020
Beiträge
11
Punkte für Reaktionen
0
Punkte
1
Also das ist doch eigentlich wurscht, wenn ich die zwei WAV Files mit FFMPEG mixe, kommt doch auch ein gemischtes File bei raus, egal ob die 1:1 zusammenpassen. Könnte man das nicht irgendwie im Python-Script anpassen, d.h. ein 'misch einfach zusammen ohne weitere Prüfung' Befehl?

BTW, gibt's eigentlich ne Möglichkeit, dass FritzCap erst bei zustandegekommenen Gesprächen aufnimmt statt gleich bei jedem Wählversuch o.Ä. ? Oder kann man den Call-Monitor so einstellen, dass er erst dann alspringt?
 

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
1.) Also das ist doch eigentlich wurscht, wenn ich die zwei WAV Files mit FFMPEG mixe, kommt doch auch ein gemischtes File bei raus, egal ob die 1:1 zusammenpassen.

2.)Könnte man das nicht irgendwie im Python-Script anpassen, d.h. ein 'misch einfach zusammen ohne weitere Prüfung' Befehl?

3.) BTW, gibt's eigentlich ne Möglichkeit, dass FritzCap erst bei zustandegekommenen Gesprächen aufnimmt statt gleich bei jedem Wählversuch o.Ä. ? Oder kann man den Call-Monitor so einstellen, dass er erst dann alspringt?
Zu 1.)
Das Verfahren ist einfach anders, du wählst die 2 Files mit deinem Hirn aus und er mischt nach Zeit. Macht Audacity auch. Das zu automatisieren macht halt g711_decoder, aber nicht mehr richtig, da mehr als 2 Files erzeugt werden.

Zu 2.)
Mit Aufwand sicherlich ja. Hört sich derzeit im gesamten dann an, als ob Mickey-Maus spricht und ist damit unverständlich. Hab ich auf die Schnelle auch versucht.

Zu 3.)
Nein. Weil fritzcap, sobald ein Gespräch anlandet, einen Auszug des gesamten Netzverkehrs macht. Fritzcap zeichnet nicht nur das Telefonat auf, sondern in dem Moment sämlichen Datenverkehr deines Netzwerkes, so als ob du an der Fritzbox "aufzeichnen" manuell anklickst.

Fakt ist:
Entweder hat einer Zeit, die Pakete vernünftig zu analysieren und zu dekodieren und FritzCap zu updaten
siehe auch:
Code:
# This is a simple parser. The payload is not fully decoded. Instead some assumptions based upon the
# length of the captured package are checked
oder der Entwickler macht ein Update,
oder es gibt einen, der meinen Denkfehler was RTP IPv4/IPv6 angeht revidiert,
oder man fügt die wav-files manuell bei Bedarf zusammen,
oder man nimmt ein aktuelleres Script zum dekodieren der .cap-Datei unter Linux / Raspian.
 
Zuletzt bearbeitet:

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Aber bei mir kommen doch bei Gesprächen exakt zwei Files raus, woran scheitert das denn dann?
Wenn ich das wüßte.
Bei der Zeile:
Code:
if sd['source'] == sdother['destination'] and sdother['source'] == sd['destination']:
füge doch mal eine Zeile darüber mal folgenden Ausdruck ein:
self.logger.debug(sd['source'], sdother['destination'], sdother['source'], sd['destination'])
Also so:
Code:
# Find associated streams to mix
                for sdother in self.sda:
                    self.logger.debug(sd['source'], sdother['destination'], sdother['source'], sd['destination'])
                    if sd['source'] == sdother['destination'] and sdother['source'] == sd['destination']:
dann bekommst du in deiner log_debug_fritzcap.txt Werte:

<Startwert 1. Datei><Zielwert 2. Datei><Startwert 2. Datei><Zielwert 1. Datei>
die Formel
Code:
if sd['source'] == sdother['destination'] and sdother['source'] == sd['destination']
sagt soviel wie: ist "Startwert 1. Datei = Zielwert 2. Datei UND Startwert 2. Datei = Zielwert 1. Datei , dann füge beide Dateien zusammen.
Und das scheint nicht mehr zu klappen, weil die Daten vermutlich verschoben sind, weil falsch dekodiert!
 

Yellow99

Neuer User
Mitglied seit
8 Feb 2020
Beiträge
11
Punkte für Reaktionen
0
Punkte
1
Das mit der zusätzlichen Zeile für Extra-Info klappt bei mir irgendwie nicht... na ja.

Die Größe der zwei WAV Dateien ist (geringfügig) unterschiedlich, aber wenn ich sie zusammenmische, kommt ein brauchbares WAV raus (d.h. die Stimmen von beiden Seiten überlappen sich NICHT)...
 
Zuletzt bearbeitet:

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Die Größe der zwei WAV Dateien ist (geringfügig) unterschiedlich, aber wenn ich sie zusammenmische, kommt ein brauchbares WAV raus (d.h. die Stimmen von beiden Seiten überlappen sich NICHT)...
Ja, ist ja auch richtig. Das eine ist der Anrufer, das andere der Angerufene. Darf auch unterschiedlich, muss sogar unterschiedlich sein.
Nimm vorläufig einen raspi unter Linux mit meinen Veränderungen und alles ist gut.
Sobald ich mal die Zeit habe, kümmere ich mich gerne um ein update der g711_decoder.py.
Oder der Entwickler meldet sich doch noch mal.... *motz
 

Yellow99

Neuer User
Mitglied seit
8 Feb 2020
Beiträge
11
Punkte für Reaktionen
0
Punkte
1
... weiß eigentlich jemand, wie man statt Datum/Zeit evtl. die Rufnummer des Anrufers in den Filenamen / Pfad kriegt? In der fritzcap.py stehen neben der tcaps Variable (die in der fritzcap.conf verwendet wird) ja auch noch andere, z.B. 'caller' oder 'acalls', nur weiß ich nicht, wie man davon Parameter übernimmt...

Jemand ne Idee?
 

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Hm, täte sagen:
Code:
    # tstart: time of program start
    # tcall: time of ring/call (_Y: year)
    # tconn: time of connection
    # tdisc: time of disconnection
    # tcaps: time of starting capture
    # tcape: time of ending capture
    # caller: the caller numer/name (id,nr,name,)
    # dialed: the dialed number/name
    # acalls: active calls number
    # lineport: the used line port name (SIP0,SIP1,etc.)
in der fritzcap.conf festlegen mit
Code:
cap_folder             = captures/%(caller.number)/
cap_file               = capture_%(tcaps.YmdHMS).cap
Edit:
Überarbeitet, Lösung HIER!
 
Zuletzt bearbeitet:

Yellow99

Neuer User
Mitglied seit
8 Feb 2020
Beiträge
11
Punkte für Reaktionen
0
Punkte
1
Vollzitat von darüber entfernt by stoney
Nein, raten kann ich auch, so geht es leider eben NICHT... hab auch schon mit 'Variablen' versucht, z.B. %(caller.id) u.Ä.

Übrigens, hab vorhin die after_capture_time Variable in der fritzcap.conf aktiviert (# am Anfang entfernt), und die Zahl scheint als _String_ übernommen zu werden statt als Integer, was zu nem Fehler führt.

Kann man sicher leicht beheben. Irgendwer...?

Ist erstaunlich, wie wenig das alles dokumentiert zu sein scheint. Besteht da so geringes Interesse dran?
 

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Nein, raten kann ich auch...
Nagut, dann raten wir mal nicht, sehen uns den Code an und stellen in der log_debug_fritzcap.txt den Eintrag fest:
Code:
data_map:{'dialed.numbername': '08xxxxxxxxxxx', 'pbook_number.0048177111222': 'MeineFrau-Handy', 'callpartner.name': '', 'tcape': datetime.datetime(1900, 1, 1, 0, 0), 'me.name': '', 'pbook_name.ZuHause': '0448556694', 'me.numbername': '08xxxxxxxxxxx', 'tcaps': datetime.datetime(2020, 2, 15, 11, 27, 42, 486239), 'pbook_number.08003301000': 'Telekom', 'dialed.number': '08xxxxxxxxxxx', 'dialed.name': '', 'callpartner.numbername': '017xxxxxxxxxxx', 'pbook_name.MeineFrau-Handy': '0048177111222', 'tdisc': datetime.datetime(1900, 1, 1, 0, 0), 'callpartner.number': '017xxxxxxxxx', 'tcall': datetime.datetime(2020, 2, 15, 11, 27, 41, 899977), 'caller.number': '017xxxxxxxxx', 'tstart': datetime.datetime(2020, 2, 15, 11, 27, 37, 387344), 'pbook_number.0177111222': 'MeineFrau-Handy', 'caller.numbername': '017xxxxxxxx', 'caller.name': '', 'acalls.number': 1, 'pbook_number.0448556694': 'ZuHause', 'pbook_name.Telekom': '08003301000', 'todisc': datetime.datetime(1900, 1, 1, 0, 0), 'tconn': datetime.datetime(1900, 1, 1, 0, 0), 'tocall': time.struct_time(tm_year=2020, tm_mon=2, tm_mday=15, tm_hour=11, tm_min=27, tm_sec=41, tm_wday=5, tm_yday=46, tm_isdst=-1), 'callevent.name': 'RING', 'toconn': datetime.datetime(1900, 1, 1, 0, 0), 'lineport.name': 'SIP0', 'me.number': '08xxxxxxxxxx'}
Also lautet der richtige Aufruf in der fritzcap.conf:
Code:
cap_folder             = captures/%(caller.number)/
...und wir stellen fest: Funktioniert nicht. Warum nicht? Aha. Ein RegEx-Fehler in der /core/string_helper.py.

Also ändern wir in der /core/string_helper.py den Ausdruck
Code:
compile_str = r"(%\(((\w+).(\w+)\)))"
im Abschnitt:

Code:
        compile_str = r"(%\(((\w+).(\w+)\)))"
        pattern = re.compile(compile_str)
        matchObj = pattern.search(data_str)
        while (matchObj):
            data_key = matchObj.group(2)
            if (data_map.has_key(data_key)):
                value = data_map.get(data_key)
            else:
                value = ""
            data_str = data_str[:matchObj.start(1)]+value+data_str[matchObj.end(1):]
            matchObj = pattern.search(data_str)

        return data_str
zu:

Code:
        #compile_str = r"(%\(((\w+).(\w+)\)))"
        compile_str = r"(%\(((\w+).(\w+))\))"
        pattern = re.compile(compile_str)
        matchObj = pattern.search(data_str)
        while (matchObj):
            data_key = matchObj.group(2)
            if (data_map.has_key(data_key)):
                value = data_map.get(data_key)
            else:
                value = ""
            data_str = data_str[:matchObj.start(1)]+value+data_str[matchObj.end(1):]
            matchObj = pattern.search(data_str)

        return data_str
da in dem RegEx-String ein "\" um eine Stelle falsch gesetzt ist.

Vergleich:
compile_str = r"(%\(((\w+).(\w+)\)))" => falsch
compile_str = r"(%\(((\w+).(\w+))\))" => richtig

Alles abspeichern, neu starten und siehe da - erhalten wir beim nächsten Telefonat in der log_info_fritzcap.txt den Eintrag:
Code:
Start capture (capture_file:'captures/017xxxxxxxxx/capture_20200215092920.cap').
Funktioniert auch mit der cap_file Variable -> Aber VORSICHT! Wenn der Filename immer zb. die dialed.number ist, wird das cap-file immer überschrieben.

cap_folder = captures/%(caller.number)/%(tcaps.Y-m-d/H:M:S)/
cap_file = capture_%(tcaps.YmdHMS).cap
ergibt:
/captures/017xxxxx/2020-02-15/14:41:07/capture_20200215144107.cap

cap_folder = captures/%(caller.number)/%(tcaps.Y-m-d/H:M:S)/
cap_file = capture_%(dialed.number).cap
ergibt:
captures/017xxxxxxxxx/2020-02-15/14:44:26/capture_08xxxx.cap


Schönes Wochenende.
 
Zuletzt bearbeitet:
  • Like
Reaktionen: Yellow99

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Übrigens, hab vorhin die after_capture_time Variable in der fritzcap.conf aktiviert (# am Anfang entfernt), und die Zahl scheint als _String_ übernommen zu werden statt als Integer, was zu nem Fehler führt.
setze in der /core/capture_monitor.py den Wert
Code:
self.after_capture_time = after_capture_time
auf
Code:
self.after_capture_time = int(after_capture_time)
Wohl auch ein BUG im fritzcap.
 
  • Like
Reaktionen: Yellow99

Yellow99

Neuer User
Mitglied seit
8 Feb 2020
Beiträge
11
Punkte für Reaktionen
0
Punkte
1
Nagut, dann raten wir mal nicht, [...]
Mein... HELD! ;) Man kann es nicht besser umschreiben.

Eigentlich recht simpel, ein fehlerhaftes RegEX, ein fehlendes Cast to Int... ich hätt's aber ganz bestimmt nie selbst gefunden.

Gibt es denn auch eine Möglichkeit, bei rausgehenden Anrufen automatisch die Nummer des Angerufenen einzufügen, und bei eingehenden Anrufen automatisch die Nummer des Anrufers? Was wäre denn der Parameter für 'Nummer des Angerufenen'?

Und dann hätte ich gleich noch eine Frage: wenn ich im Capture-Path z.B. das Datum mit Unterstrichen unterteilt haben möchte, also

cap_folder = captures/%(tcaps.Y_m_d/H-M-S)/

dann klappt das nicht. Muss man die _ irgendwie escapen o.Ä.?

Und, irgendwann mal, schau Dir doch bitte mal die g711_decoder.py genauer an... die Nachwelt wird es Dir danken. ;)
 

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Mein... HELD! ;) Man kann es nicht besser umschreiben.
Danke, dafür gibt es auch ein "LIKE" unten rechts ;-)

Und dann hätte ich gleich noch eine Frage: wenn ich im Capture-Path z.B. das Datum mit Unterstrichen unterteilt haben möchte, also
cap_folder = captures/%(tcaps.Y_m_d/H-M-S)/
Der Unterstrich "_" ist kein gültiges time-format, das py-script dahingehend umzuschreiben ist aufwändiger als mit dem Kompromiss mit einem Bindestrich "-" zu arbeiten.
Code:
cap_folder = captures/%(tcaps.Y-m-d/H-M-S)/
ergibt doch auch ein gut zu lesendes Format?
Gibt es denn auch eine Möglichkeit, bei rausgehenden Anrufen automatisch die Nummer des Angerufenen einzufügen, und bei eingehenden Anrufen automatisch die Nummer des Anrufers? Was wäre denn der Parameter für 'Nummer des Angerufenen'?
Es gibt zwei unterschiedliche Nummern:
dialed.number = die gewählte Nummer => bei eingehenden Gesprächen ist es die eigene Ruf-Nummer, bei ausgehenden Gesprächen die vom Anschluß gewählte Nummer.
caller.number = ist die Nummer der Gegenseite bei eingehenden -, die eigene Nummer bei ausgehenden Gesprächen.

Schreib:
Code:
cap_folder             = captures/%(callevent.name)_%(tcaps.Y-m-d/H-M-S)/
damit gibt es ein
Code:
captures/CALL_2020-02-15/15-47-33/capture_0176xxxx.cap
oder
captures/RING_2020-02-15/15-47-33/capture_0176xxxxxx.cap
CALL = ausgehendes Gespräch
RING = eingehendes Gespräch

Code:
cap_folder             = captures/%(callevent.name)_%(dialed.number)_%(tcaps.Y-m-d/H-M-S)/
ergibt captures/CALL_0176xxxxx_2020-02-15/15-52-27/capture_017.....cap

Die Kombinationsmöglichkeiten sind fast unbegrenzt.
Code:
cap_folder             = captures/%(dialed.number)/%(callevent.name)/%(tcaps.Y-m-d/H-M-S)/
ergibt den Pfad (ausgehendes Gespräch):
captures/0176xxxxx/CALL/2020-02-15/16-10-35/capture_0176xxxx.cap

ergibt den Pfad (eingehendes Gespräch):
captures/08xxxx/RING/2020-02-15/16-10-35/capture_08xxxx.cap

EDIT: Bitte Post#317 beachten, nochmalige Verbesserung/Fehlerbeseitigung!
 
Zuletzt bearbeitet:
  • Like
Reaktionen: Yellow99

jochen01

Neuer User
Mitglied seit
16 Feb 2020
Beiträge
2
Punkte für Reaktionen
0
Punkte
1
cap_folder = captures/%(caller.number)/%(tcaps.Y-m-d/H:M:S)/
cap_file = capture_%(dialed.number).cap
ergibt:
captures/017xxxxxxxxx/2020-02-15/14:44:26/capture_08xxxx.cap
Prima, dass das jetzt ohne Fehler geht.
Ich möchte gern cap_file auf das hier stellen:
capture_%(tcaps.Y-m-d-H-M-S)_%(callevent.name)_%(dialed.number)_%(caller.number).cap

Das funktioniert leider nicht, da kommt sowas wie:
Code:
'capture_2020-02-16-09-33-38)%(Sun Feb 16 09:33:38 2020Sun 9 916%v16capture_.Sun0216)%(Sun Feb 16 09:33:38 2020Sun 9 91609:33:38 AM.702Feb1609:33:38 AM)%(16%iSun 91616.702Feb1609:33:38 AM.cap'
raus. Meine Versuche haben ergeben, dass es funktioniert, wenn ich den Datum+Zeit-String ans Ende setze:
capture_%(callevent.name)_%(dialed.number)_%(caller.number)_%(tcaps.Y-m-d-H-M-S).cap

Da muss es also im Parser des Format-Strings noch irgendwo einen Fehler geben. Aber als Python-Analphabet sind mir hier enge Grenzen gesetzt...
Kann mal wer Kundiges schauen?
 

ctiemann

Neuer User
Mitglied seit
19 Sep 2011
Beiträge
19
Punkte für Reaktionen
4
Punkte
3
Ich möchte gern cap_file auf das hier stellen:
capture_%(tcaps.Y-m-d-H-M-S)_%(callevent.name)_%(dialed.number)_%(caller.number).cap
Okay, hatte ich gestern schon selbst gemerkt, dachte mir, solange es keiner braucht :cool:

Ändere in der /core/capture_monitor.py den Abschnitt:
Code:
    def init_capture_file(self):
        # Create capfile folder
        folder = StringHelper.parse_string(self.cap_folder, self.data_map)
        folder = folder.replace("\\","/")
        if (not folder.endswith("/")):
            folder = folder+"/"

        file = StringHelper.parse_string(self.cap_file, self.data_map)

        self.cap_file_path = folder+file
        self.logger.debug("Initialize capture file (folder:%s, file:%s)." % (folder,file))
        if not os.path.exists(folder):
            self.logger.debug("Destination folder:'%s' not exists. Create." % folder)
            os.makedirs(folder)
zu
Code:
    def init_capture_file(self):
        # Create capfile folder
        folder = ""
        sections = self.cap_folder.split("%(")
        for sec in sections:
                folder += StringHelper.parse_string("%("+sec, self.data_map)
                folder = folder.replace("%(","")
        folder = folder.replace("\\","/")
        if (not folder.endswith("/")):
                folder = folder+"/"

        file = ""
        sections = self.cap_file.split("%(")
        for sec in sections:
                file += StringHelper.parse_string("%("+sec, self.data_map)
                file = file.replace("%(","")

        self.cap_file_path = folder+file
        self.logger.debug("Initialize capture file (folder:%s, file:%s)." % (folder,file))
        if not os.path.exists(folder):
            self.logger.debug("Destination folder:'%s' not exists. Create." % folder)
            os.makedirs(folder)
Damit ist das Problem beseitigt.

Viele Grüße und LIKE nicht vergessen ;)
 
  • Like
Reaktionen: jochen01

3CX PBX - GRATIS
Linux / Win / Cloud

Statistik des Forums

Themen
233,964
Beiträge
2,040,740
Mitglieder
353,176
Neuestes Mitglied
Mogli70