OpenVPN status

Hi all,

I am working on this for days now and I a bit lost right now.
I would like to show some OpenVPN status in HomeAssistant, I didn’t found anything out of the box so I came up with this plan:

  1. parse the openvpn-status.log to json with local script

  2. load json into HomeAssistant with the “file sensor”

  3. display in a table which “open vpn profile” is used and from which “ip” it is connecting.

  4. is fine.

  5. is kinda fine, the json is loaded but the complete state is the json string so no attributes.

  6. has my most worries: I cannot filter the json string in the html file. (I can print in completely in there and also attributes)

For the table I took this as example: https://community.home-assistant.io/t/table-on-front-end/19763/19

Does anybody knows how this should work of has a better idea to get this done? :slight_smile:

I see I did something wrong with the formatting but 4,5,6 are the outcome of step 1,2,3

I see to recall openvpn has a socket file you can get status from.

I think there is a log reader built and one using the socket.

1 Like

I managed to get the html table working but I have next thingie:
My Json:
{ “size”: 2, “details”: [ {“profile”: “profile_x”, “ip”: 12.34.56:9876} , {“profile”: “profile_y”, “ip”: 98.76.54:4321}

In sensor template following doesn’t work:
‘{{ states.sensor.my_sensor.state.size }}’
‘{{ states.sensor.my_sensor.state.details }}’

But following in file/command_line sensor does work:
‘{{ value_json.size }}’
‘{{ value_json.details }}’

I would like to have one sensor instead of two so that both states will be updated by one sensor.

Maybe someone sees what I am missing :slight_smile:
Thx

It’s been a while since this question was posted but I’ve faced similar problem. The solution I’ve implemented is to use OpenVPN’s management interface (add management ip_address port line to openvpn server config file) and it exposes the management interface that can be accessed via telnet. Now you can query it and with a bit of grep and sed you can filter out just the lines that are interesting, here with a bash script (it runs a status command over telnet and ends telnet session):

#!/usr/local/bin/bash
{ echo "status"; sleep 1; echo "exit"; sleep 1;} | telnet 10.144.1.20 7656 | grep , | grep -v Updated | grep -v bcast | sed '/Virtual/,+100d'

You can trigger above bash script from HA using shell_command entity:

shell_command:
    vpnstatus_csv: 'ssh -i private_key_file user_id@remote_host_ip "/root/vpn.bash" > /config/custom_files/vpnstatus.csv'

I’m running it on a different host as it requires telnet which is not available on the HA container.
Now you need to import the file to HA. I’m using a python program (taken and adjusted program found from here):

import csv
import requests

csv_file = '/config/custom_files/vpnstatus.csv'
url = 'http://localhost:8123/api/states/sensor.'
days = 7

headers = {
    'Authorization': 'Bearer TOKEN',
    'content-type': 'application/json'
}

# Read the csv_file
csv_reader = csv.DictReader(open(csv_file), delimiter=',')

the_list = [dict(d) for d in csv_reader]


errors = ''

variable = 'vpn_clients'
state = len(the_list)

attributes = '{"clients": ' + str(the_list) + ', "state_class": "measurement", "icon": "mdi:vpn", "friendly_name": "VPN Clients"}'

data = '{"state": "' + str(state) + '", "unique_id": "truenas_vpn_clients", "attributes": ' + attributes + '}'
data = data.replace("'",'"')
r = requests.post(url+variable, data=data, headers=headers)
if r.status_code != 200 and r.status_code != 201:
    errors = errors + 'ERROR:' + variable + ' - ' + str(r.status_code)

if errors != '':
    print(errors)

Again you can create a shell_command to run this python program from HA.
Now you just need to both services on a regular basis (I use HA automation for that) and you’ll get nice sensor displaying number of connected clients and details of those clients in the attributes:

I just stumbled across this last night and had a few issues implementing as-is, so figured I’d add a few notes to maybe save someone in the future a couple of hours.

  1. This solution won’t work with the Home Assistant containers, because these don’t have a ~/.ssh folder, and if you create it, it isn’t persistent (it’ll disappear on update). Minor tweak, but the SSH command needs to specify a known_hosts file in a persistent location (IE: /config)
  2. The telnet command (more accurately, the greps/sed that follow it) flat out didn’t work for me on the specific version of OpenVPN I’m running (OpenVPN 2.5.5 x86_64-pc-linux-gnu) I can’t really determine why the difference without seeing the differences in raw output, so YMMV for anyone deciding to implement this, and it may require some tweaking/experimentation, especially depending on requirements.
  3. I personally didn’t love how dispersed the scripts were, I would prefer have everything in one place, on one system with one update command. So that’s what I did. This way the only change needed on the OpenVPN server is configuring management. Everything else is handled in Home Assistant.
  4. I loathe the fact you need to restart Home Assistant to make changes to shell_command, and the easiest way to get around that is to have the command a shell script, so you can tweak it without restarting.

So, here’s my modified solution:

Since you installed an OpenVPN server, I’m assuming you’re capable of enabling management on openvpn, running ssh-key-gen and ssh-copy-id, etc. or at least Googling it. Once you have password-free SSH working, copy ~/.ssh/known_hosts to wherever this lives in your setup. Also make sure your pub/private keys are there as well. I placed everything in /config/openvpn, but the scripts should be agnostic to their location, as long as everything is placed together.

shell_command in configuration.yaml:

shell_command:
    vpnstatus_csv: 'bash /config/openvpn/update.sh'

Update your install location (/config/openvpn/) as appropriate.

update.sh:

#!/bin/bash

CURRENT_DIR=`dirname "$(realpath $0)"`
OPENVPN=<USER@HOST>

TELNET_CMD='{ echo "status"; sleep 1; echo "exit"; sleep 1;} | telnet localhost 7505 | grep CLIENT_LIST | sed "s/.*CLIENT_LIST,//g"'
ssh -o UserKnownHostsFile=$CURRENT_DIR/known_hosts -i $CURRENT_DIR/open_vpn_rsa $OPENVPN "( $TELNET_CMD )" > $CURRENT_DIR/vpn_status.csv
ssh -o UserKnownHostsFile=$CURRENT_DIR/known_hosts -i $CURRENT_DIR/open_vpn_rsa $OPENVPN "cat /etc/openvpn/server/easy-rsa/pki/index.txt | grep ^V | awk '{print $5}' | sed 's/.*=//'" > $CURRENT_DIR/vpn_clients.txt

python $CURRENT_DIR/ovpn_status.py

Change open_vpn_rsa to the name of your ssh-key and <USER@HOST> as appropriate (EG: [email protected]).

ovpn_status.py:

"""Add OpenVPN Status info to Home Assistant."""
import csv
import os
import requests


CSV_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vpn_status.csv")
CLIENT_LIST = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vpn_clients.txt")
URL = "http://localhost:8123/api/states/sensor.{entity_id}"
HEADERS = {
    "Authorization": "Bearer <LONG_LIVED_TOKEN>",
    "content-type": "application/json",
}


def post_entity(entity_id, state, attributes):
    """Submits a status to Home Assistant."""

    data = f'{{"state": "{state}", "unique_id": "openvpn_{entity_id}", "attributes": {attributes}}}'
    result = requests.post(URL.format(entity_id=entity_id), data=data, headers=HEADERS, timeout=10)

    if result.status_code not in (200, 201):
        print("ERROR: vpn_clients - " + str(result.status_code))


def update_client_usages(results):
    """Submits individual client's usage (sent/received)."""

    # Create the list of sensors, zero them out
    client_list = {}
    with open(CLIENT_LIST, encoding="utf8") as client_fh:
        for line in client_fh:
            client_list[line.rstrip()] = {"sent": 0, "received": 0}

    # Update the usage statistics for any active sensors
    for client in results:
        client_list[client["Common Name"]] = {"sent": client["Bytes Sent"], "received": client["Bytes Received"]}

    # Submit all the clients
    for client, values in client_list.items():
        unique_id = client.replace("-", "_")
        attributes = (
          '{{"state_class": "measurement", "icon": "mdi:vpn", "friendly_name": "'
          + client
          + ' Bytes {operation}", "unit_of_measurement": "B", "device_class": "data_size", "state_class": "total_increasing"}}'
        )

        post_entity(f"{unique_id}_bytes_sent", values["sent"], attributes.format(operation="Sent"))
        post_entity(f"{unique_id}_bytes_received", values["received"], attributes.format(operation="Received"))


def main():
    """Main body."""

    # Read the csv_file
    with open(CSV_FILE, encoding="utf8") as csv_fh:
        csv_reader = csv.DictReader(csv_fh, delimiter=",")

        results = []
        for line in csv_reader:
            results.append(line)
    update_client_usages(results)

    # Submit primary entity status
    attributes = (
        '{"clients": '
        + str(results).replace("'", '"')
        + ', "state_class": "measurement", "icon": "mdi:vpn", "friendly_name": "VPN Clients", "unit_of_measurement": "client(s)"}'
    )
    post_entity("vpn_clients", len(results), attributes)


if __name__ == "__main__":
    main()

Change <LONG_LIVED_TOKEN> to your token.

You should be able to run bash /config/openvpn/update.sh from a Terminal on the Home Assistant now and it’ll create the sensor etc. If you see any errors, it’s time to debug, and I would specifically look at the telnet status command output format. You may need to tweak the greps/seds. Additionally you will probably need to make changes to the CSV parsing in the openvpn.py

When this succeeds I see:

bash-5.1# bash /config/openvpn/update.sh
Connection closed by foreign host.
bash-5.1#

Restart Home Assistant to get the shell_command changes loaded.

Once all that works, create the updater automation:

alias: "Timer: Update VPN Clients"
description: ""
trigger:
  - platform: time_pattern
    seconds: "30"
condition: []
action:
  - service: shell_command.vpnstatus_csv
    data: {}
mode: single

This will update every 60 seconds, reasonable for me, but, I’m assuming if you made it this far, you can tweak all this as appropriate.

As a final note, I would recommend running chmod 600 -R <install_dir> on this once done. You have tokens, and private keys living here, it doesn’t hurt to be extra secure.

Edit April 11, 2023: I added sensors to track usage per client, this added the second ssh line to update.sh and the call to update_client_usages() in ovpn_status.py::main(). Additionally I did some heavy cleanup to the script to improve statistics and readability. This particular implementation will have persistent sensors for all configured clients, but if they aren’t in use, their usage is “0”.