Monitor OpenVPN Client Activity using Python


09 Aug 2020 linux

Installing OpenVPN in your own server gives you two major benefits. First, you will have better privacy control. Second, no need to spend on pay-per-user thing. As long as the CPU can handle encryption/decryption, and the ethernet controller can handle the traffic, OpenVPN is a pretty good choice for providing VPN service to a small-sized team.

OpenVPN Config

Anyway, a typical OpenVPN Server configuration looks like the following.

dev tun-udp-1194
server 10.10.10.0 255.255.255.0
proto udp
port 1194

ca /etc/openvpn/pki/ca.crt
cert /etc/openvpn/pki/issued/server@mdminhazulhaque.io.crt
key /etc/openvpn/pki/private/server@mdminhazulhaque.io.key
dh /etc/openvpn/pki/dh.pem
crl-verify /etc/openvpn/pki/crl.pem

remote-cert-tls client
keepalive 10 120
tls-auth /etc/openvpn/pki/ta.key 0
cipher AES-256-CBC
tls-cipher TLS-DHE-DSS-WITH-AES-256-CBC-SHA256
auth SHA256
tls-version-min 1.2
comp-lzo
persist-key
persist-tun
verb 0

OpenVPN Management over Telnet

In order to access the telnet management console, the following line has to be added in /etc/openvpn/server.conf. Also the openvpn.service must be reloaded for the change to take effect.

management 127.0.0.1 7000

Alright. Next thing is, try connecting to port 7000 via telnet command. If it was a success, then we can use a small Python3 script to talk with the OpenVPN server.

A Bit of Knowledge about Common Name

Before we run any script, we need to find out the CN or Common Name used in the RSA Root Certificate. For example, if you generate the CA file with CN=mdminhazulhaque.io, both EasyRSA and OpenVPN will append this CN after the client’s name. A sample server and client names could be as follows.

Server: server@mdminhazulhaque.io
Client Mark: mark@mdminhazulhaque.io
Client Bob: bob@mdminhazulhaque.io
Client Alice: alice@mdminhazulhaque.io

Which means, the client names must be unique because OpenVPN uses the client name to map DHCP IP Addresses for remote users.


Run the Script!

Well, now we know the common name. It could be mdminhazulhaque.io, or example.com or server or anything. We need to amend the RE_COMMON_NAME regex and put the correct value there. Once done, upload the script to the OpenVPN server and run it using python3.

#!/usr/bin/env python3

import re
import telnetlib
import time

# TODO change here
RE_COMMON_NAME = "^*@mdminhazulhaque.io.*$"

HOST = "127.0.0.1"
PORT = "7000"
INTERVAL = 5
PRE_USERS = {}

def alert(user, ip, state):
    print(F"*{user}* {state} from {ip}")
    # TODO You can call Slack or API Gateway here
    
while True:
    telnet = telnetlib.Telnet(HOST,PORT, 5)
    telnet.write("status\n")
    output = telnet.read_until("END\n", 1)
    telnet.close()
    
    CURR_USERS = {}
    
    for line in output.split("\n"):
        if re.search(RE_COMMON_NAME, line):
            user, realaddress, _, _, _ = line.split(",")
            ip = realaddress.split(":")[0]
            CURR_USERS[user] = ip
    
    connected = set(CURR_USERS.keys()) - set(PRE_USERS.keys())
    disconnected = set(PRE_USERS.keys()) - set(CURR_USERS.keys())
    
    for user in list(connected):
        alert(user, CURR_USERS[user], "connected")
    for user in list(disconnected):
        alert(user, PRE_USERS[user], "disconnected")
    
    PRE_USERS = CURR_USERS    
    
    time.sleep(INTERVAL)

The Result

If you keep running the script in background (screen or systemd whichever you prefer), you will see activity alerts like the following.

mark@mdminhazulhaque.io connected from 190.106.117.114
bob@mdminhazulhaque.io connected from 186.110.104.190
alice@mdminhazulhaque.io connected from 144.143.163.176
...
alice@mdminhazulhaque.io disconnected from 144.143.163.176
mark@mdminhazulhaque.io disconnected from 190.106.117.114
bob@mdminhazulhaque.io disconnected from 186.110.104.190

More Tweaks

You can simply override the alert function and call custom Slack or API Gateway URL using requests. Tada!