[Openvpn-devel] [L] Change in openvpn[master]: Add python scripts that demonstrate using the mangement interface

"plaisthos \(Code Review\) via Openvpn-devel" <[email protected]>
Newsgroups net.sourceforge.lists.openvpn-devel
Message-ID <[email protected]>
plaisthos has uploaded this change for review. ( http://gerrit.openvpn.net/c/openvpn/+/1859?usp=email )


Change subject: Add python scripts that demonstrate using the mangement interface
......................................................................

Add python scripts that demonstrate using the mangement interface

Change-Id: Ib09bafabf612e9501ea757647c86a450c73228a5
Signed-off-by: Arne Schwabe <[email protected]>
---
A sample/management/acc_sysinfo_client_demo.py
A sample/management/acc_sysinfo_server_demo.py
A sample/management/omi.py
A sample/management/omi_tools.py
4 files changed, 774 insertions(+), 0 deletions(-)



  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/59/1859/1

diff --git a/sample/management/acc_sysinfo_client_demo.py b/sample/management/acc_sysinfo_client_demo.py
new file mode 100755
index 0000000..7a7f015
--- /dev/null
+++ b/sample/management/acc_sysinfo_client_demo.py
@@ -0,0 +1,174 @@
+#!/opt/homebrew/bin/python3
+# Copyright (c) 2026 OpenVPN Inc <[email protected]>
+# Copyright (c) 2026 Arne Schwabe <[email protected]>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# This implement a small demonstration of using the
+# OpenVPN Access Server Management Interface (OMI) and an app Custom Control
+# protocol (info_sample) to retrieve a number of system information from the
+# client and push them to the server. To use this use on the server
+
+# --app-custom-control info_sample
+# --management /path/to/acc_sysinfo_server_demo.py unix-script
+
+# and on the client
+# --app-custom-control info_sample
+# --management /path/to/acc_sysinfo_client_demo.py unix-script
+
+# Note this is just a demonstration and not a production-ready solution.
+# When modifying this code, keep the security in mind. It is easy to
+# introduce a security problem when executing commands on the client.
+
+import asyncio
+import base64
+import json
+import logging
+import os
+import platform
+import socket
+import subprocess
+
+from omi import OmiProtocol
+from omi_tools import run_main, start_management_protocol, ConnectData
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+class OmiSysInfoClientProtocol(OmiProtocol):
+    def __init__(self, finish_future, connect_data: ConnectData):
+        super().__init__(finish_future, connect_data)
+        self._acc_msg = ""
+
+    def recv_notify_ACC(self, args):
+        protocol, fragment, b64msg = args.split(",", 2)
+        if protocol != "sys_info":
+            logging.info(f"Received unknown ACC protocol: {protocol}")
+            return
+
+        msg = base64.decodebytes(b64msg.encode())
+        self._acc_msg += msg.decode()
+
+        # Message is not yet complete. Wait for more fragments.
+        if fragment != "0":
+            return
+
+        try:
+            self.parse_sysinfo_message(self._acc_msg)
+        except Exception as e:
+            logging.error(f"Error parsing ACC message: {e}", exc_info=True)
+        self._acc_msg = ""
+
+    def parse_sysinfo_message(self, accmsg):
+        msg = json.loads(accmsg)
+
+        logging.info(f"Received sys_info message: {accmsg}")
+
+        asyncio.create_task(self.send_info_response(msg))
+
+    async def send_info_response(self, msg):
+        response = {}
+        if msg.get("uname", False):
+            response['uname'] = platform.uname()._asdict()
+
+        if msg.get("username", False):
+            response['username'] = os.getlogin()
+
+        if msg.get("hostname", False):
+            response['hostname'] = platform.node()
+
+        if msg.get("dns-google.com", False):
+            try:
+                addrs = [x[4][0] for x in socket.getaddrinfo('google.com', 443)]
+                response["dns-google.com"] = addrs
+
+            except Exception as e:
+                logging.error(f"Error resolving DNS for google.com: {e}", exc_info=True)
+                response["dns-google.com"] = None
+
+        if msg.get("route", False):
+            response['route'] = await self.get_routes(False)
+
+        if msg.get("route6", False):
+            response['route6'] = await self.get_routes(True)
+
+        # TODO implement fragmentation
+
+        msg = json.dumps(response).encode()
+        # 980 chars after base64 encoding
+        fragment_length = 980*6//8
+
+        for part_start in range(0, len(msg), fragment_length):
+
+            msg_part = msg[part_start:part_start + fragment_length]
+            b64msg = base64.encodebytes(msg_part).decode()
+
+            protocol = "sys_info"
+
+            flags = "6"
+            if part_start + fragment_length < len(msg):
+                flags = "6:F"
+
+            command = f"acc-msg\n{protocol}\n{flags}\n{b64msg}\nEND"
+            self.queue_command(command)
+
+    async def get_routes(self, v6):
+        if platform.system() == 'linux':
+            cmd = '/usr/sbin/ip'
+            if v6:
+                args = ['-6', 'route', 'show']
+            else:
+                args = ['-4', 'route', 'show']
+        elif platform.system() == 'Darwin' or os.platform() == 'FreeBSD':
+            # FreeBSD and macOS
+            if os.path.exists('/usr/sbin/netstat'):
+                cmd = '/usr/sbin/netstat'
+            else:
+                cmd = '/usr/bin/netstat'
+
+            if v6:
+                args = [ '-f', 'inet6', '-rn']
+            else:
+
+                args = [ '-f', 'inet', '-rn']
+        else:
+            return "Unsupported platform"
+
+        proc = await asyncio.subprocess.create_subprocess_exec(cmd, *args, stdout=subprocess.PIPE)
+        output, _ = await proc.communicate()
+        return output.decode()
+
+
+async def main(connect_data: ConnectData):
+    transport, protocol, finish_future = await start_management_protocol(connect_data, OmiSysInfoClientProtocol)
+    while True:
+        await asyncio.sleep(1)
+        print(".", end="", flush=True)
+
+    management_version = await protocol.management_ready()
+    print(f"Management interface version: {management_version}")
+
+    await protocol.set_bytecount_interval(5)
+
+    await finish_future
+
+
+if __name__ == '__main__':
+    run_main(main)
diff --git a/sample/management/acc_sysinfo_server_demo.py b/sample/management/acc_sysinfo_server_demo.py
new file mode 100755
index 0000000..139d2f9
--- /dev/null
+++ b/sample/management/acc_sysinfo_server_demo.py
@@ -0,0 +1,135 @@
+#!/opt/homebrew/bin/python3
+# Copyright (c) 2026 OpenVPN Inc <[email protected]>
+# Copyright (c) 2026 Arne Schwabe <[email protected]>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+import asyncio
+# This implement a small demonstration of using the
+# OpenVPN Access Server Management Interface (OMI) and an app Custom Control
+# protocol (info_sample) to retrieve a number of system information from the
+# client and push them to the server. To use this use on the server
+
+# --app-custom-control info_sample
+# --management /path/to/acc_sysinfo_server_demo.py unix-script
+
+# and on the client
+# --app-custom-control info_sample
+# --management /path/to/acc_sysinfo_client_demo.py unix-script
+
+import base64
+import json
+import logging
+import random
+from pprint import pprint, pformat
+
+from omi import OMIServerProtocol, ConnectedClient
+from omi_tools import run_main, start_management_protocol, ConnectData
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+class SysInoConnectedClient(ConnectedClient):
+    accmsg = ""
+
+class OMISysInfoServerProtocol(OMIServerProtocol):
+    def __init__(self, finish_future, connect_data: ConnectData = None):
+        super().__init__(finish_future, connect_data)
+        self.accmsg = ""
+        self._connected_client_class = SysInoConnectedClient
+
+
+    def client_event_ACC(self,client, kid, env, extra):
+        protocol, fragment, b64msg = extra.split(",", 2)
+        if protocol != "sys_info":
+            logging.info(f"Received unknown ACC protocol: {protocol}")
+            return
+
+        try:
+
+            msg = base64.decodebytes(b64msg.encode())
+            client.accmsg += msg.decode()
+
+            # Message is not yet complete. Wait for more fragments.
+            if fragment != "0":
+                return
+
+            self.parse_sys_info_reply(client.accmsg)
+            client.accmsg = ""
+
+        except Exception as e:
+            logger.error(f"Error parsing sys_info message: {e}", exc_info=True)
+
+    def parse_sys_info_reply(self, accmsg):
+        logger.debug(f"Received sys_info message: {accmsg!r}")
+        client_info = json.loads(accmsg)
+
+        logger.info(f"Received sys_info response: {pformat(client_info)}")
+
+    def client_event_ESTABLISHED(self, client, kid, env, extra):
+        print(f"CLient connected: {client}")
+        pprint(env)
+
+        # TODO: track kid and don't assume we haven't done a renegotiation
+        kid = 1
+        asyncio.create_task(self._send_sys_info_command(client.cid, kid))
+
+    def client_event_CONNECT(self, client, kid, env, extra):
+        print(f"Authenticating client {client.cid}, {kid}")
+        self.queue_command(f"client-auth-nt {client.cid} {kid}")
+
+    async def _send_sys_info_command(self, cid, kid):
+        # Give the client some time to settle its routes
+        await asyncio.sleep(5)
+
+        info = ["route", "route6", "dns-google.com", "username", "hostname", "uname"]
+        # Pick 3 random infos from the client
+
+        infos = random.choices(info, k=3)
+        request = {k: True for k in infos}
+
+        for i in info:
+            request = {i: True}
+            req_str = json.dumps(request).encode()
+
+            logger.info(f"Sending sysinfo request for client {cid} with infos: {infos}")
+
+            req_base64 = base64.encodebytes(req_str).decode()
+            acc_protocol = "sys_info"
+
+            # TODO: Implement fragmentation
+            cmd = f"client-acc-msg {cid} {kid}\n{acc_protocol}\n6\n{req_base64}\nEND"
+
+            await self.queue_command(cmd)
+
+
+async def main(connect_data: ConnectData):
+    transport, protocol, finish_future = await start_management_protocol(connect_data, OMISysInfoServerProtocol)
+
+    management_version = await protocol.management_ready()
+    print(f"Management interface version: {management_version}")
+
+    await protocol.enable_log()
+
+    await protocol.set_bytecount_interval(5)
+
+    await finish_future
+
+
+if __name__ == '__main__':
+    run_main(main)
diff --git a/sample/management/omi.py b/sample/management/omi.py
new file mode 100644
index 0000000..2a7f90e
--- /dev/null
+++ b/sample/management/omi.py
@@ -0,0 +1,372 @@
+#!/opt/homebrew/bin/python3
+# Copyright (c) 2026 OpenVPN Inc <[email protected]>
+# Copyright (c) 2026 Arne Schwabe <[email protected]>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# This is an example implementation in python and asyncio to drive OpenVPN's
+# management interface (OMI). The permissive license allows you to use this
+# code in other projects.
+
+"""
+Implementation of the OpenVPN management interface (OMI) protocol.
+"""
+import asyncio
+import logging
+import re
+from asyncio import Future
+from dataclasses import dataclass
+from datetime import datetime
+from typing import override, List
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+
+@dataclass
+class OmiCommandResult:
+    command: str
+    result: List[str]
+    error: bool
+    status_text: str
+
+
+@dataclass
+class OmiSendCommand:
+    command: str
+    result: Future[OmiCommandResult]
+
+
+@dataclass
+class ConnectionProperties:
+    """
+    Properties for connecting to the management interface.
+    """
+    password: str
+
+
+class OmiProtocol(asyncio.Protocol):
+    def __init__(self, finish_future, connect_data: ConnectionProperties):
+        super().__init__()
+        self.bytes_received = None
+        self.bytes_sent = None
+        self.version = -1
+        self.recvBuffer = ""
+        self.finish_future = finish_future
+        self._can_send = asyncio.Event()
+        self._can_send.clear()
+
+        self._management_ready = asyncio.Event()
+        self._management_ready.clear()
+        self._response = []
+
+        self._response_ready = asyncio.Event()
+        self._response_ready.clear()
+
+        self._send_task = asyncio.create_task(self._send_loop())
+        self._send_queue = asyncio.Queue()
+        self._connect_data: ConnectionProperties = connect_data
+
+    @override
+    def eof_received(self):
+        logger.info('EOF received. Shutting down.')
+        self._stop()
+
+    @override
+    def connection_lost(self, excp):
+        if excp:
+            logger.info(f'Connection lost ({excp}). Shutting down.')
+        else:
+            logger.info(f'Connection lost. Shutting down.')
+        self._stop()
+
+    def _stop(self):
+        if not self.finish_future.done():
+            self._send_task.cancel()
+            self.finish_future.set_result(True)
+
+    async def _send_loop(self):
+        while True:
+            command: OmiSendCommand = await self._send_queue.get()
+            await self._can_send.wait()
+            self.send_line(command.command)
+
+            response = []
+            response_complete = False
+
+            # This is a bit brittle as it will loop forever if not getting a
+            # line that starts with ERROR or SUCCESS. But this is the OMI protocol
+            while not response_complete:
+                await self._response_ready.wait()
+
+                line = self._response.pop(0)
+                response.append(line)
+
+                if not self._response:
+                    self._response_ready.clear()
+
+                lastline = response[-1]
+                if lastline.startswith('ERROR:'):
+                    status = False
+                    response_complete = True
+                    _, status_text = lastline.split(":", 1)
+
+                elif lastline.startswith('SUCCESS:'):
+                    status = True
+                    response_complete = True
+                    _, status_text = lastline.split(":", 1)
+                elif lastline == "END":
+                    status = True
+                    response_complete = True
+                    status_text = None
+                else:
+                    pass
+
+            # logger.debug(f'Result received command: {response}')
+
+            result = OmiCommandResult(command.command, response[:-1], status, status_text)
+
+            command.result.set_result(result)
+            logger.debug(f"Command done.")
+
+    @override
+    def pause_writing(self):
+        self._can_send.clear()
+
+    @override
+    def resume_writing(self):
+        self._can_send.set()
+
+    @override
+    def connection_made(self, transport):
+        peername = transport.get_extra_info('peername')
+        logging.info('Connection from {}'.format(peername))
+        self.transport = transport
+        self._can_send.set()
+
+        # OMI expects us to authenticate with the password just on
+        # its own on a line
+        if self._connect_data.password:
+            self.transport.write(f"{self._connect_data.password}\r\n".encode())
+
+    @override
+    def data_received(self, data):
+        # OMI protocol is pure text so decode everything as UTF-8
+        message = data.decode()
+        self.recvBuffer += message
+
+        parts = self.recvBuffer.split("\r\n")
+
+        # pass complete lines to recvLine
+        for part in parts[:-1]:
+            logger.debug(f'Line received: {part!r}')
+            self.recv_line(part)
+
+        # keep the last incomplete line for the next call
+        self.recvBuffer = parts[-1]
+
+    def recv_line(self, line):
+        if line.startswith(">"):
+            self.recv_notify(line)
+        else:
+            self._response.append(line)
+            self._response_ready.set()
+
+    def recv_notify(self, line):
+        if not ":" in line:
+            logger.warning("Invalid line received: " + line)
+            return
+
+        command, args = line[1:].split(":", 1)
+
+        # do dynamic dispatch based on command to call a handler
+        if hasattr(self, f"recv_notify_{command}"):
+            handler = getattr(self, f"recv_notify_{command}")
+            handler(args)
+            return
+
+        logger.info("Unknown notify line received: " + line)
+
+    def recv_notify_INFO(self, args):
+        m = re.match(r"OpenVPN Management Interface Version (?P<version>\d+)", args)
+        if m:
+            self.version = int(m.group("version"))
+            self._management_ready.set()
+        else:
+            logger.warning(f"Unknown INFO line received: {args}")
+
+    def recv_notify_BYTECOUNT(self, args):
+        bytes_sent, bytes_received = args.split(",", 1)
+        self.bytes_sent = int(bytes_sent)
+        self.bytes_received = int(bytes_received)
+
+    async def _release_hold_task(self, hold_time: int = 0):
+        await asyncio.sleep(hold_time)
+        await self.queue_command("hold release")
+
+    def recv_notify_HOLD(self, args):
+        _text, hold_time = args.split(":", 1)
+        hold_time = int(hold_time)
+
+        asyncio.create_task(self._release_hold_task(hold_time))
+
+    def queue_command(self, command) -> Future[OmiCommandResult]:
+        cmd = OmiSendCommand(command, Future())
+        self._send_queue.put_nowait(cmd)
+        return cmd.result
+
+    async def management_ready(self) -> int:
+        """
+        Waits until the management interface is ready. Returns the management interface version.
+        """
+        await self._management_ready.wait()
+        return self.version
+
+    def send_line(self, line):
+        logger.debug(f"Sending line: {line}")
+        self.transport.write(f"{line}\n".encode())
+
+    """
+    Set the bytecount interval. Use 0 to disable
+    """
+
+    async def set_bytecount_interval(self, interval: int):
+        return await self.queue_command(f"bytecount {interval}")
+
+    async def enable_log(self):
+        return await self.queue_command("log on")
+
+    def recv_notify_LOG(self, args):
+        # >LOG:1786453401,D,MANAGEMENT: CMD 'bytecount 30'
+        timestamp, level, message = args.split(",", 2)
+
+        timestamp = datetime.fromtimestamp(int(timestamp))
+        level = level.strip()
+        message = message.strip()
+        self.log_message(timestamp, level, message)
+
+    def log_message(self, time, level, message):
+        """
+        A new log message has been received. This method is intended
+        to be overridden by a subclass to handle the log messages.
+        :param time:    time in local time format
+        :param level:   level of the log message
+        :param message: the log message itself
+        :return:
+        """
+        logger.debug(f"Received log message: {time}, {level}, {message}")
+
+
+@dataclass
+class ConnectedClient:
+    cid: int
+    bytes_sent: int = 0
+    bytes_received: int = 0
+
+
+class OMIServerProtocol(OmiProtocol):
+    """
+    This class implement common method for the OMI when OpenVPN is running
+    as a server.
+    """
+
+    # Connected client class, can be overridden by subclasses to store
+    # more information about the connected client
+    _connected_client_class = ConnectedClient
+
+    def __init__(self, finish_future, connect_data: ConnectionProperties = None):
+        super().__init__(finish_future, connect_data)
+        self._reset_pending_client_event()
+        self._connected_clients = {}
+
+    def _reset_pending_client_event(self):
+        self._pending_client_event = None
+        self._pending_client_event_cid = -1
+        self._pending_client_event_kid = None
+        self._pending_client_event_extra_args = None
+        self._client_env = {}
+
+    def recv_notify_CLIENT(self, args: str):
+        event, args = args.split(",", 1)
+
+        if event == "ENV":
+            if args == "END":
+                # Client environment is finished, trigger previous client
+                # event that contained an ENV
+                self._trigger_client_event(self._pending_client_event, self._pending_client_event_cid,
+                                           self._pending_client_event_kid, self._client_env,
+                                           self._pending_client_event_extra_args)
+                self._reset_pending_client_event()
+            else:
+                key, value = args.split("=", 1)
+                self._client_env[key] = value
+
+        elif event in ("ESTABLISHED", "DISCONNECT"):
+            # This event is followed by a client environment. Wait for
+            # the client evironment to be completed before triggering the event
+            cid = int(args)
+
+            self._pending_client_event = event
+            self._pending_client_event_cid = int(cid)
+            self._pending_client_event_kid = None
+            if event == "ESTABLISHED":
+                self._add_client(cid)
+
+        elif event in ("CONNECT", "REAUTH", "CR_RESPONSE"):
+            # This event is similar to other event but also carries the key-id
+            cid, kid = args.split(",", 1)
+            cid, kid = int(cid), int(kid)
+            self._add_client(cid)
+
+            self._pending_client_event = event
+            self._pending_client_event_cid = int(cid)
+            self._pending_client_event_kid = int(kid)
+        elif event in ("ACC",):
+            cid, kid, acc_args = args.split(",", 2)
+            self._trigger_client_event(event, int(cid), int(kid), None, acc_args)
+        else:
+            logger.debug(f"Client event {event} not handled.")
+
+    def recv_notify_BYTECOUNT_CLI(self, args):
+        cid, bytes_sent, bytes_received = args.split(",", 2)
+
+        client = self._connected_clients.get(int(cid))
+        if client:
+            client.bytes_sent = int(bytes_sent)
+            client.bytes_received = int(bytes_received)
+
+    def _trigger_client_event(self, event, cid, kid, env, extra):
+        handler_name = f"client_event_{event}"
+        if hasattr(self, handler_name):
+            handler = getattr(self, handler_name)
+            client = self._connected_clients.get(cid, None)
+            handler(client, kid, env, extra)
+        else:
+            logger.debug(f"Client event {event} not handled. Method {handler_name} does not exist.")
+
+    def _add_client(self, cid):
+        if not cid in self._connected_clients:
+            self._connected_clients[cid] = self._connected_client_class(cid = cid)
+
+    def _remove_client(self, cid):
+        del self._connected_clients[cid]
+
+    async def get_client_status(self) -> OmiCommandResult:
+        cmd_result = self.queue_command("status 3")
+        return await cmd_result
diff --git a/sample/management/omi_tools.py b/sample/management/omi_tools.py
new file mode 100644
index 0000000..afa0e46
--- /dev/null
+++ b/sample/management/omi_tools.py
@@ -0,0 +1,93 @@
+#! /usr/bin/python3
+# Copyright (c) 2026 OpenVPN Inc <[email protected]>
+# Copyright (c) 2026 Arne Schwabe <[email protected]>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+import asyncio
+import os
+import sys
+from dataclasses import dataclass
+
+from omi import OmiProtocol, ConnectionProperties
+
+
+@dataclass
+class ConnectData(ConnectionProperties):
+    socket: str
+
+
+def get_unix_script_environment() -> ConnectData | None:
+    password = os.environ.get("MANAGEMENT_PASSWORD", None)
+    socket = os.environ.get("MANAGEMENT_SOCKET", None)
+
+    if password and socket:
+        return ConnectData(password=password, socket=socket)
+    else:
+        return None
+
+
+async def start_management_protocol(connect_data: ConnectData, protocol=OmiProtocol):
+    # Get a reference to the event loop as we plan to use
+    # low-level APIs.
+    loop = asyncio.get_running_loop()
+
+    finish_future = loop.create_future()
+
+    print(f"Connecting to management socket {connect_data.socket} with passwd {connect_data.password[:4]}...")
+    transport, protocol = await loop.create_unix_connection(lambda: protocol(finish_future, connect_data),
+                                                            connect_data.socket)
+
+    return transport, protocol, finish_future
+
+
+async def main():
+    transport, protocol, finish_future = await start_management_protocol()
+
+    management_version = await protocol.management_ready()
+    await protocol.enable_log()
+    print(f"Management interface version: {management_version}")
+
+    await protocol.set_bytecount_interval(5)
+
+    await finish_future
+
+
+def run_main(main_func, connect_parameters: ConnectData = None):
+    """
+    Example function to run a main function to be either using the
+    environment variable for the unix-script or to use. If the unix-script
+    environment are detected, the function will fork and run the main_func
+    with the ConnectData from the environment. If the environment variables
+    are not available, it will not fork will fall back to the connect_parameters
+    if present.
+    :param connect_parameters:  fallback connect parameters if unix-script is not available
+    :param main_func:           The function to call.
+    :return:
+    """
+    unix_script_parameters = get_unix_script_environment()
+    if unix_script_parameters:
+        asyncio.run(main_func(unix_script_parameters))
+    elif connect_parameters:
+        asyncio.run(main_func(connect_parameters))
+    else:
+        print(f"Must be called via unix-script")
+
+
+if __name__ == '__main__':
+    run_main(main)

-- 
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1859?usp=email
To unsubscribe, or for help writing mail filters, visit http://gerrit.openvpn.net/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: Ib09bafabf612e9501ea757647c86a450c73228a5
Gerrit-Change-Number: 1859
Gerrit-PatchSet: 1
Gerrit-Owner: plaisthos <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>

_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.