use clap::Parser;
use futures::{SinkExt, StreamExt};
use openvpn_mgmt_codec::{
Notification, OvpnCodec, OvpnCommand, StatusFormat,
command::connection_sequence,
stream::{ClassifyExt, ManagementEvent},
};
use tokio::net::TcpStream;
use tokio_util::codec::Framed;
use tracing::{debug, error, info, warn};
#[derive(Parser)]
struct Args {
#[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(|_| "client_mode=debug,openvpn_mgmt_codec=info".parse().unwrap()),
)
.init();
let addr = Args::parse().address;
let mut backoff = std::time::Duration::from_secs(1);
loop {
info!(%addr, "connecting");
match TcpStream::connect(&addr).await {
Ok(stream) => {
backoff = std::time::Duration::from_secs(1);
info!(%addr, "connected");
if let Err(error) = handle_connection(stream).await {
error!(%error, "session error");
}
warn!("connection lost, reconnecting");
}
Err(error) => {
warn!(%error, ?backoff, "connect failed, retrying");
}
}
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(std::time::Duration::from_secs(30));
}
}
async fn handle_connection(stream: TcpStream) -> anyhow::Result<()> {
let framed = Framed::new(stream, OvpnCodec::new());
let (mut sink, raw_stream) = framed.split();
let mut mgmt = raw_stream.classify();
for cmd in connection_sequence(5) {
sink.send(cmd).await?;
}
sink.send(OvpnCommand::Status(StatusFormat::V3)).await?;
while let Some(event) = mgmt.next().await {
match event? {
ManagementEvent::Notification(notification) => {
info!(?notification, "notification");
if let Notification::Fatal { message } = ¬ification {
error!(%message, "OpenVPN fatal");
break;
}
}
ManagementEvent::Response(response) => {
debug!(?response, "response");
}
}
}
Ok(())
}