openvpn-mgmt-codec 0.8.0

OpenVPN management protocol codecs (tokio-util en/decoder implementations).
Documentation
//! Server-mode example: OpenVPN connects to *us* (`--management-client`).
//!
//! When OpenVPN is started with `--management-client`, it dials out to
//! a listening management program rather than the other way around. This
//! is the standard pattern for auth plugins and process managers.
//!
//! Usage:
//!   cargo run --example server_mode -- [bind_addr]
//!
//! Then start OpenVPN with:
//!   openvpn --config your.ovpn \
//!           --management 127.0.0.1 7505 \
//!           --management-client \
//!           --management-hold
//!
//! The codec is transport-agnostic — it works the same whether *you*
//! connect to OpenVPN or OpenVPN connects to *you*. The only difference
//! is who calls `bind`/`listen` vs `connect`.

use std::net::SocketAddr;

use clap::Parser;
use futures::{SinkExt, StreamExt};
use openvpn_mgmt_codec::{
    OvpnCodec, OvpnCommand,
    command::server_connection_sequence,
    stream::{ClassifyExt, ManagementEvent},
};
use tokio::net::{TcpListener, TcpStream};
use tokio_util::codec::Framed;
use tracing::{debug, error, info};

/// Server-mode example: listen for OpenVPN to connect to us.
#[derive(Parser)]
struct Args {
    /// Address to bind to.
    #[arg(default_value = "127.0.0.1:7505")]
    address: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "server_mode=debug,openvpn_mgmt_codec=info".parse().unwrap()),
        )
        .init();

    let addr = Args::parse().address;

    let listener = TcpListener::bind(&addr).await?;
    info!(%addr, "listening, waiting for OpenVPN to connect");

    loop {
        let (stream, peer) = listener.accept().await?;
        info!(%peer, "accepted connection");

        tokio::spawn(async move {
            if let Err(error) = handle_connection(stream, peer).await {
                error!(%peer, %error, "session error");
            }
            info!(%peer, "connection closed");
        });
    }
}

async fn handle_connection(stream: TcpStream, peer: SocketAddr) -> anyhow::Result<()> {
    let framed = Framed::new(stream, OvpnCodec::new());
    let (mut sink, raw_stream) = framed.split();
    let mut mgmt = raw_stream.classify();

    // Run the server-mode startup sequence (includes env-filter setup).
    for cmd in server_connection_sequence(5, 0) {
        sink.send(cmd).await?;
    }

    // Process events until the connection closes.
    while let Some(event) = mgmt.next().await {
        match event? {
            ManagementEvent::Notification(notification) => {
                info!(%peer, ?notification, "notification");

                // Auto-approve all client connections (demo only!).
                if let openvpn_mgmt_codec::Notification::Client {
                    event: openvpn_mgmt_codec::ClientEvent::Connect,
                    cid,
                    kid: Some(kid),
                    ..
                } = &notification
                {
                    info!(%peer, %cid, "auto-approving client");
                    sink.send(OvpnCommand::ClientAuthNt {
                        cid: *cid,
                        kid: *kid,
                    })
                    .await?;
                }
            }
            ManagementEvent::Response(response) => {
                debug!(%peer, ?response, "response");
            }
        }
    }

    Ok(())
}