diff --git a/config/rootfiles/common/aarch64/stage2 b/config/rootfiles/common/aarch64/stage2
index f4169a44e..f52768726 100644
--- a/config/rootfiles/common/aarch64/stage2
+++ b/config/rootfiles/common/aarch64/stage2
@@ -131,6 +131,7 @@ usr/local/bin/xt_geoip_update
 #usr/local/share/zoneinfo
 #usr/local/src
 #usr/sbin
+usr/sbin/openvpn-metrics
 usr/sbin/ovpn-ccd-convert
 usr/sbin/ovpn-collectd-convert
 #usr/share
diff --git a/config/rootfiles/common/stage2 b/config/rootfiles/common/stage2
index fca540431..63aeeffe1 100644
--- a/config/rootfiles/common/stage2
+++ b/config/rootfiles/common/stage2
@@ -131,6 +131,7 @@ usr/local/bin/xt_geoip_update
 #usr/local/share/zoneinfo
 #usr/local/src
 #usr/sbin
+usr/sbin/openvpn-metrics
 usr/sbin/ovpn-ccd-convert
 usr/sbin/ovpn-collectd-convert
 #usr/share
diff --git a/config/rootfiles/common/x86_64/stage2 b/config/rootfiles/common/x86_64/stage2
index cc67837e5..6a6fe7b88 100644
--- a/config/rootfiles/common/x86_64/stage2
+++ b/config/rootfiles/common/x86_64/stage2
@@ -133,6 +133,7 @@ usr/local/bin/xt_geoip_update
 #usr/local/share/zoneinfo
 #usr/local/src
 #usr/sbin
+usr/sbin/openvpn-metrics
 usr/sbin/ovpn-ccd-convert
 usr/sbin/ovpn-collectd-convert
 #usr/share
diff --git a/html/cgi-bin/ovpnmain.cgi b/html/cgi-bin/ovpnmain.cgi
index 00ecd77a0..734cc0bfa 100644
--- a/html/cgi-bin/ovpnmain.cgi
+++ b/html/cgi-bin/ovpnmain.cgi
@@ -372,6 +372,11 @@ sub writeserverconf {
 	} else {
 		print CONF "verb 3\n";
 	}
+
+    print CONF "# Log clients connecting/disconnecting\n";
+    print CONF "client-connect \"/usr/sbin/openvpn-metrics client-connect\"\n";
+    print CONF "client-disconnect \"/usr/sbin/openvpn-metrics client-disconnect\"\n";
+
     # Print server.conf.local if entries exist to server.conf
     if ( !-z $local_serverconf  && $sovpnsettings{'ADDITIONAL_CONFIGS'} eq 'on') {
        open (LSC, "$local_serverconf");
diff --git a/lfs/stage2 b/lfs/stage2
index 355aeef54..ad6d64838 100644
--- a/lfs/stage2
+++ b/lfs/stage2
@@ -105,6 +105,7 @@ endif
 	done
 
 	# Move script to correct place.
+	mv -vf /usr/local/bin/openvpn-metrics /usr/sbin/
 	mv -vf /usr/local/bin/ovpn-ccd-convert /usr/sbin/
 	mv -vf /usr/local/bin/ovpn-collectd-convert /usr/sbin/
 	mv -vf /usr/local/bin/captive-cleanup /usr/bin/
diff --git a/src/scripts/openvpn-metrics b/src/scripts/openvpn-metrics
new file mode 100755
index 000000000..30b3932c5
--- /dev/null
+++ b/src/scripts/openvpn-metrics
@@ -0,0 +1,171 @@
+#!/usr/bin/python3
+############################################################################
+#                                                                          #
+# This file is part of the IPFire Firewall.                                #
+#                                                                          #
+# IPFire is free software; you can redistribute it and/or modify           #
+# it under the terms of the GNU General Public License as published by     #
+# the Free Software Foundation; either version 2 of the License, or        #
+# (at your option) any later version.                                      #
+#                                                                          #
+# IPFire is distributed in the hope that it will be useful,                #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of           #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            #
+# GNU General Public License for more details.                             #
+#                                                                          #
+# You should have received a copy of the GNU General Public License        #
+# along with IPFire; if not, write to the Free Software                    #
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA #
+#                                                                          #
+# Copyright (C) 2007-2020 IPFire Team <info@ipfire.org>.                   #
+#                                                                          #
+############################################################################
+
+import argparse
+import logging
+import logging.handlers
+import os
+import sqlite3
+import sys
+
+_ = lambda x: x
+
+DEFAULT_DATABASE_PATH = "/var/ipfire/ovpn/clients.db"
+
+def setup_logging(level=logging.INFO):
+	l = logging.getLogger("openvpn-metrics")
+	l.setLevel(level)
+
+	# Log to console
+	h = logging.StreamHandler()
+	h.setLevel(logging.DEBUG)
+	l.addHandler(h)
+
+	# Log to syslog
+	h = logging.handlers.SysLogHandler(address="/dev/log",
+		facility=logging.handlers.SysLogHandler.LOG_DAEMON)
+	h.setLevel(logging.INFO)
+	l.addHandler(h)
+
+	# Format syslog messages
+	formatter = logging.Formatter("openvpn-metrics[%(process)d]: %(message)s")
+	h.setFormatter(formatter)
+
+	return l
+
+# Initialise logging
+log = setup_logging()
+
+class OpenVPNMetrics(object):
+	def __init__(self):
+		self.db = self._open_database()
+
+	def parse_cli(self):
+		parser = argparse.ArgumentParser(
+			description=_("Tool that collects metrics of OpenVPN Clients"),
+		)
+		subparsers = parser.add_subparsers()
+
+		# client-connect
+		client_connect = subparsers.add_parser("client-connect",
+			help=_("Called when a client connects"),
+		)
+		client_connect.add_argument("file", nargs="?",
+			help=_("Configuration file")
+		)
+		client_connect.set_defaults(func=self.client_connect)
+
+		# client-disconnect
+		client_disconnect = subparsers.add_parser("client-disconnect",
+			help=_("Called when a client disconnects"),
+		)
+		client_disconnect.add_argument("file", nargs="?",
+			help=_("Configuration file")
+		)
+		client_disconnect.set_defaults(func=self.client_disconnect)
+
+		# Parse CLI
+		args = parser.parse_args()
+
+		# Print usage if no action was given
+		if not "func" in args:
+			parser.print_usage()
+			sys.exit(2)
+
+		return args
+
+	def __call__(self):
+		# Parse command line arguments
+		args = self.parse_cli()
+
+		# Call function
+		try:
+			ret = args.func(args)
+		except Exception as e:
+			log.critical(e)
+
+		# Return with exit code
+		sys.exit(ret or 0)
+
+	def _open_database(self, path=DEFAULT_DATABASE_PATH):
+		db = sqlite3.connect(path)
+
+		# Create schema if it doesn't exist already
+		db.executescript("""
+			CREATE TABLE IF NOT EXISTS sessions(
+				common_name TEXT NOT NULL,
+				connected_at INTEGER NOT NULL,
+				duration INTEGER,
+				bytes_received INTEGER,
+				bytes_sent INTEGER
+			);
+
+			-- Create index for speeding up searches
+			CREATE INDEX IF NOT EXISTS sessions_common_name ON sessions(common_name);
+		""")
+
+		return db
+
+	def _get_environ(self, key):
+		if not key in os.environ:
+			sys.stderr.write("%s missing from environment\n" % key)
+			raise SystemExit(1)
+
+		return os.environ.get(key)
+
+	def client_connect(self, args):
+		common_name = self._get_environ("common_name")
+
+		# Time
+		time_ascii = self._get_environ("time_ascii")
+		time_unix  = self._get_environ("time_unix")
+
+		log.info("Opening session for %s at %s" % (common_name, time_ascii))
+
+		c = self.db.cursor()
+		c.execute("INSERT INTO sessions(common_name, connected_at) \
+			VALUES(?, ?)", (common_name, time_unix))
+		self.db.commit()
+
+	def client_disconnect(self, args):
+		common_name = self._get_environ("common_name")
+		duration    = self._get_environ("time_duration")
+
+		# Collect some usage statistics
+		bytes_received = self._get_environ("bytes_received")
+		bytes_sent     = self._get_environ("bytes_sent")
+
+		log.info("Closing session for %s after %ss and receiving/sending %s/%s bytes" \
+			% (common_name, duration, bytes_received, bytes_sent))
+
+		c = self.db.cursor()
+		c.execute("UPDATE sessions SET duration = ?, bytes_received = ?, \
+			bytes_sent = ? WHERE common_name = ? AND duration IS NULL",
+			(duration, bytes_received, bytes_sent, common_name))
+		self.db.commit()
+
+def main():
+	m = OpenVPNMetrics()
+	m()
+
+main()
