Fuer mein kleines VPN-Projekt spiele ich aktuell mit allen Moeglichkeiten, Tools und Funktionen umzusetzen. Nachdem ich im VPN einen kleinen Fileserver, ein simples Nachrichtensystem (mit PHP und ohne Datenbank) und einen Ping-Check eingebaut hatte, wollte ich gerne ein bisschen tiefer gehen.
Mir war bekannt, dass es VPN-Anbieter gibt, die die Menge des durch Kunden erzeugten Traffics kennen und irgendwann dicht machen. Und da werden wohl auch welche dabei sein, die OpenVPN verwenden. Im debianforum.de fand ich dann ein Thema[1] bei dem ein langjaehriger Debian-User empfahl einzelne OpenVPN Instanzen zu erstellen um anhand der einzelnen Tunnelschnittstellen (tun0, tun1 usw.) den Traffic messen zu koennen.
Das schien mir allerdings sehr aufwendig, zudem ich ja gerne moechte, dass die Clients sich im gleichen Netz befinden und miteinander kommunizieren koennen.

Mir fiel dann ein, dass OpenVPN ja ein Statuslog anlegt, welches standardmaessig in /etc/openvpn liegt. Dieses Statuslog beinhaltet unter anderem die IP des Clients, die Zeit des Verbindungsaufbaus, die virtuelle IP und auch die eingehenden und ausgehenden Bytes.[2] Daraus kann man doch was machen.
Ich hatte bereits geplant selbst ein Script zu schreiben, welche die in CSV-ähnlichem Format vorliegenden Datensätze in ein PHP-Array umwandelt sodass ich es beliebig ausgeben kann, als ich ein fertiges Script genau für diesen Zweck im Netz fand.[3]

Das Script macht ganz unkompliziert aus der CSV-ähnlichen Datei eine kleine Tabelle und zeigt die Datenmengen usw. menschenlesbar an. Das war für mich schonmal eine prima grundlage um mein Vorhaben umzusetzen. Musste ich doch nur die Ausgabe ein bisschen so trimmen, dass ich HTML-Quelltext herausbekomme, den ich dann in PHP einfach “echoen” konnte.

Das Script sah ursprünglich so aus:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

STATUS = "/etc/openvpn/openvpn.status"

status_file = open(STATUS, 'r')
stats = status_file.readlines()
status_file.close()

hosts = []

headers = {
    'cn':    'Common Name', 
    'virt':  'Virtual Address', 
    'real':  'Real Address', 
    'sent':  'Sent', 
    'recv':  'Received', 
    'since': 'Connected Since'
}

sizes = [
    (1<<50L, 'PB'),
    (1<<40L, 'TB'),
    (1<<30L, 'GB'),
    (1<<20L, 'MB'),
    (1<<10L, 'KB'),
    (1,       'B')
]

def byte2str(size):
    for f, suf in sizes:
        if size >= f:
            break
 
    return "%.2f%s" % (size / float(f), suf)


for line in stats:
    cols = line.split(',')

    if len(cols) == 5 and not line.startswith('Common Name'):
        host  = {}
        host['cn']    = cols[0]
        host['real']  = cols[1].split(':')[0]
        host['recv']  = byte2str(int(cols[2]))
        host['sent']  = byte2str(int(cols[3]))
        host['since'] = cols[4].strip()
        hosts.append(host)

    if len(cols) == 4 and not line.startswith('Virtual Address'):
        for h in hosts:
            if h['cn'] == cols[1]:
                h['virt'] = cols[0]


fmt = "%(cn)-25s%(virt)-18s%(real)-15s%(sent)13s%(recv)13s%(since)25s"
print fmt % headers
print "\n".join([fmt % h for h in hosts])

Meine Version:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

STATUS = "/etc/openvpn/openvpn-status.log"

status_file = open(STATUS, 'r')
stats = status_file.readlines()
status_file.close()

hosts = []

headers = {
    'cn':    'Name', 
    'virt':  'Virtual Address', 
    'real':  'Real Address', 
    'sent':  'Senden', 
    'recv':  'Empfangen', 
    'since': 'Verbunden seit'
}

sizes = [
    (1<<50L, 'PB'),
    (1<<40L, 'TB'),
    (1<<30L, 'GB'),
    (1<<20L, 'MB'),
    (1<<10L, 'KB'),
    (1,       'B')
]

def byte2str(size):
    for f, suf in sizes:
        if size >= f:
            break

    return "%.2f %s" % (size / float(f), suf)


for line in stats:
    cols = line.split(',')

    if len(cols) == 5 and not line.startswith('Common Name'):
        host  = {}
        host['cn']    = cols[0]
        host['real']  = cols[1].split(':')[0]
        host['recv']  = byte2str(int(cols[2]))
        host['sent']  = byte2str(int(cols[3]))
        host['since'] = cols[4].strip()
        hosts.append(host)

    if len(cols) == 4 and not line.startswith('Virtual Address'):
        for h in hosts:
            if h['cn'] == cols[1]:
                h['virt'] = cols[0]


fmt = "<tr><th>%(cn)-25s</th><th>%(sent)13s</th><th>%(recv)13s</th><th>%(since)25s</th></tr>"
print fmt % headers
print "\n".join([fmt % h for h in hosts])

Den Inhalt des Scriptes packt ihr am besten einfach in eine Datei wie zum Beispiel “vpnstats.sh” (in meinem Fall) und macht sie mit “chmod +x vpnstats.sh” ausführbar. Das wars damit – natürlich kann man in dem Script jetzt noch tolle Spielereien machen – dazu schreibe ich vielleicht später mal was.

Kann man es erkennen? Ich habe mir ein ganz einfaches Spiel gemacht und die Ausgabe geändert. Ein paar Spalten weniger (Real IP, Virt. IP) und HTML Tags für eine Tabellenzeile und jeweils die Spalten hinzugefügt. So kann ich die Datei direkt in einer PHP-Datei zwischen ein paar Table-Tags einbinden und spare mir jeglichen Aufwand in PHP Teile zu ersetzen und umzugestalten.

Weiter muss es jetzt in der PHP-Datei gehen. Das ist der letzte Schritt und recht einfach. Hier die Inhalte meiner PHP-Datei:

echo "<table>";
$traffic = file_get_contents('test.txt');
echo $traffic;
echo "</table>";

PHP holt sich also die Datei “test.txt” aus dem gleichen Verzeichnis wo auch die PHP-Datei, welche ausgeführt wird, liegt. Anschließend wird der komplette Inhalt der Datei in die Variable “$traffic” geladen und danach mit “echo” ausgegeben.[4]

Jetzt gilt es nur noch, die Datei test.txt zu generieren. Und da habe ich zwei Ansätze ins Auge gefasst. Eine sichere Methode ist, den Befehl, welcher die Datei generiert, per Cronjob jede Minute ausführen zu lassen. So können jedoch die Trafficangaben unterscheiden, da sie nicht ganz aktuell sind. Jedoch ist das generieren per Cronjob relativ sicher und funktioniert auch, sollte mal PHP-Funktionen wie exec und system nicht nutzen können/wollen (was auch ganz gut ist).

Dennoch schlage ich auch die zweite Methode noch vor, da wir hier immer genaue Angaben haben weil bei jedem Aufruf der PHP-Datei die VPN-Statistik neu erstellt wird. Diese Methode würde ich an eurer Stelle nur in abgeschlossenen Übungssystemen verwenden, da man sich mit exec und Co. ganz schön Sicherheitslücken einbauen kann ohne das gleich zu merken.

1. Methode: Cronjob

* * * * * /pfad/vpnstats.sh > /pfad/test.txt

2. Methode: exec in PHP

In der PHP-Datei folgende Zeile möglichst am Anfang einfügen:

exec("/pfad/vpnstats.sh > /pfad/test.txt");

Wichtig: Der User, mit dem euer PHP ausgeführt wird, muss auf das Script und auf die test.txt zugreifen und bei der TXT schreiben können. Sonst kanns passieren, dass da nur eine leere Ausgabe bei rauskommt. Auch ist wichtig, dass der User die Datei “/etc/openvpn/openvpn-status.log” lesen kann. (Letzteres ist auch wichtig bei der Methode mit den Cronjobs.)

Wenn alles gelungen ist, sehen eure Statistiken auf der Seite dann so aus:

vpnstats

[1] https://debianforum.de/forum/viewtopic.php?f=32&t=144162
[2] http://openvpn.net/archive/openvpn-users/2007-08/msg00197.html
[3] https://sigterm.sh/2009/07/16/simple-openvpn-server-statistics/
[4] http://nl3.php.net/file_get_contents

PS: Mir ist klar, dass das vielleicht nicht die sauberste Methode ist, aber sie funktioniert. Ich werde ein bisschen weiter dran herumspielen um gleichzeitig auch ein bisschen Erfahrung und Wissen zu sammeln. Wenn ihr Vorschläge usw. habt empfehle ich wie immer die Kommentarfunktion. Ich antworte auch definitiv. :)

Hinzugefügt am 22.04.2014:

Auf Xing hat mir jemand kürzlich einen Kommentar hinterlassen, wie man mittels exec das Script weitgehend ohne Sicherheitslücke ausführen kann. “sudo” heißt das Zauberwort. Die Datei mit dem Script wird hierzu einfach außerhalb von /www abgelegt und mittels visudo die sudo-Konfiguration wie folgt editiert:

www-data ALL = NOPASSWD: /pfad/vpnstats.sh

Das sorgt dafür, dass der Nutzer www-data (Standardnutzer für PHP- und Webserverangelegenheiten) Zugriff auf die sh-Datei ohne weiteren Zugriff auf das Dateisystem hat.
Wichtig ist dann noch, den exec-Befehl ein bisschen zu ändern:

exec("sudo /pfad/vpnstats.sh > /pfad/test.txt");

So wird der Befehl mit/über sudo ausgeführt.

2 thoughts on “OpenVPN-Client Traffic-Statistiken auslesen und mit PHP ausgeben

  1. Sauberste Methode? Ich find’s ok und denke dass Ähnliches oft auf die gleiche Art gemacht wird.

    Bietest du jetzt VPN Internetzugänge an?

  2. Sauberste Methode wäre sicher, die Daten mit einer Schnittstelle zu übergeben und dann nicht einen Mischmasch aus Python, Bash und PHP zu verwenden, sondern alles mit einer Sprache zu machen. :D

    Nein, VPN im Sinne von Gateway ins Internet biete ich nicht an. Aber ich habe mit ein paar Freunden ein VPN aufgebaut mit dem wir alte Spiele usw. im LAN-Modus zusammen zocken können.

    Grüße :)

Leave a Reply to Maltris Cancel reply

Your email address will not be published. Required fields are marked *