pub mod actions;
pub mod keymethod;
pub mod packet;
pub mod peer;
pub mod reliable;
pub mod session;
pub mod tls_channel;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
use crate::utils::clock::Instant;
use crate::utils::wire_failure::WireFailure;
use actions::{OpenvpnProtocol, OPENVPN_KEY_EXCHANGE_EVENT, OPENVPN_PEER_RESET_EVENT};
use anyhow::{Context, Result};
use keymethod::ClientKeyMethod2;
use packet::{ControlFrame, DataFrame};
use peer::{Peer, PeerAdmission, PeerManager};
use session::{ControlSession, SessionEvent, SessionManager};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use tracing::{debug, error, trace, warn};
const MAX_PEERS: usize = 100;
const PEER_IDLE_TIMEOUT_SECS: u64 = 120;
const SWEEP_INTERVAL_SECS: u64 = 30;
const RETRANSMIT_TICK_MS: u64 = 250;
pub struct OpenvpnServer {
socket: Arc<UdpSocket>,
local_addr: SocketAddr,
peer_manager: Arc<PeerManager>,
sessions: Arc<SessionManager>,
server_session_id: u64,
tls_config: Arc<rustls::ServerConfig>,
llm_client: Arc<OllamaClient>,
}
impl OpenvpnServer {
pub async fn spawn_with_llm_actions(
bind_addr: SocketAddr,
llm_client: Arc<OllamaClient>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<SocketAddr> {
let tls = tokio::task::spawn_blocking(tls_channel::build_control_channel_tls)
.await
.context("TLS setup task for the OpenVPN control channel panicked")??;
let socket = UdpSocket::bind(bind_addr)
.await
.with_context(|| format!("Failed to bind OpenVPN UDP socket on {}", bind_addr))?;
let local_addr = socket.local_addr()?;
let server_session_id = rand::random::<u64>();
let log = Log::new(Some(&status_tx));
log.info(format!(
"OpenVPN control-channel server listening on {} (session id {:016x})",
local_addr, server_session_id
));
log.info(format!(
"OpenVPN control channel peer fingerprint SHA256={}",
tls.fingerprint
));
log.warn(
"OpenVPN carries no traffic: the control channel completes a TLS handshake and the \
key method 2 exchange, but PUSH_REQUEST is not answered, no data channel keys are \
derived and there is no TUN device, so no tunnel is ever established. Use WireGuard \
for a real VPN.",
);
let server = Arc::new(OpenvpnServer {
socket: Arc::new(socket),
local_addr,
peer_manager: Arc::new(PeerManager::new()),
sessions: Arc::new(SessionManager::new()),
server_session_id,
tls_config: tls.config,
llm_client,
});
let loop_server = server.clone();
let loop_state = app_state.clone();
let loop_status = status_tx.clone();
let accept_handle = tokio::spawn(async move {
tokio::select! {
res = loop_server.clone().recv_loop(loop_state.clone(), server_id, loop_status.clone()) => {
if let Err(e) = res {
error!("OpenVPN receive loop stopped: {}", e);
}
}
_ = loop_server.clone().sweep_loop(loop_state, server_id) => {}
_ = loop_server.retransmit_loop() => {}
}
});
app_state
.register_server_task(server_id, accept_handle)
.await;
log.info(format!("OpenVPN server ready on {}", local_addr));
Ok(local_addr)
}
async fn recv_loop(
self: Arc<Self>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<()> {
let mut buf = vec![0u8; packet::MAX_PACKET_SIZE];
loop {
let (len, peer_addr) = self.socket.recv_from(&mut buf).await?;
let datagram = &buf[..len];
trace!("OpenVPN: {} bytes from {}", len, peer_addr);
let (opcode, key_id) = match packet::parse_opcode_byte(datagram) {
Ok(v) => v,
Err(e) => {
debug!("OpenVPN: undecodable datagram from {}: {}", peer_addr, e);
continue;
}
};
if opcode.is_tls_crypt_v2() {
Log::new(Some(&status_tx)).warn(format!(
"OpenVPN: {} uses tls-crypt-v2 ({:?}), which this server cannot decode; ignoring",
peer_addr, opcode
));
continue;
}
if opcode.is_data() {
self.handle_data(datagram, peer_addr, &status_tx).await;
continue;
}
let frame = match ControlFrame::parse(datagram) {
Ok(f) => f,
Err(e) => {
debug!("OpenVPN: malformed {:?} from {}: {}", opcode, peer_addr, e);
continue;
}
};
if opcode.is_client_reset() {
self.clone()
.handle_client_reset(
frame,
peer_addr,
key_id,
app_state.clone(),
server_id,
status_tx.clone(),
)
.await;
} else if opcode.is_ack() {
self.handle_ack(frame, peer_addr).await;
} else {
self.clone()
.handle_control(
frame,
peer_addr,
app_state.clone(),
server_id,
status_tx.clone(),
)
.await;
}
}
}
async fn sweep_loop(
self: Arc<Self>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
) {
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(SWEEP_INTERVAL_SECS));
ticker.tick().await;
loop {
ticker.tick().await;
let expired = self
.peer_manager
.remove_idle_peers(std::time::Duration::from_secs(PEER_IDLE_TIMEOUT_SECS))
.await;
for peer in expired {
debug!("OpenVPN: forgetting idle peer {}", peer.addr);
self.sessions.remove(&peer.addr).await;
if peer.admission == PeerAdmission::Accepted {
app_state
.close_connection_on_server(server_id, peer.connection_id)
.await;
}
}
}
}
async fn retransmit_loop(self: Arc<Self>) {
let mut ticker =
tokio::time::interval(std::time::Duration::from_millis(RETRANSMIT_TICK_MS));
loop {
ticker.tick().await;
let (datagrams, dead) = self.sessions.drain_all(Instant::now()).await;
for (addr, datagram) in datagrams {
if let Err(e) = self.socket.send_to(&datagram, addr).await {
warn!("OpenVPN: retransmission to {} failed: {}", addr, e);
}
}
for addr in dead {
debug!(
"OpenVPN: {} stopped acknowledging control packets; dropping its session",
addr
);
self.sessions.remove(&addr).await;
}
}
}
async fn handle_client_reset(
self: Arc<Self>,
frame: ControlFrame,
peer_addr: SocketAddr,
key_id: u8,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) {
if !frame.is_plain_reset() {
Log::new(Some(&status_tx)).warn(format!(
"OpenVPN: reset from {} is not a plain reset ({} payload bytes, {} ACKs); \
the client is probably using --tls-auth or --tls-crypt, which is not \
supported. Ignoring rather than mis-parsing it.",
peer_addr,
frame.payload.len(),
frame.ack_packet_ids.len()
));
return;
}
let client_packet_id = frame.packet_id.unwrap_or(0);
if let Some(existing) = self.peer_manager.get_peer(&peer_addr).await {
if existing.session_id == frame.session_id {
match existing.admission {
PeerAdmission::Deciding => {
trace!(
"OpenVPN: reset retransmit from {} while a decision is in flight",
peer_addr
);
}
PeerAdmission::Accepted => {
self.sessions
.with(&peer_addr, |s| s.on_client_reset_retransmit())
.await;
self.flush_peer(peer_addr).await;
}
PeerAdmission::Rejected => {
trace!(
"OpenVPN: ignoring reset retransmit from rejected {}",
peer_addr
);
}
}
self.peer_manager.touch(&peer_addr).await;
return;
}
debug!("OpenVPN: {} restarted with a new session id", peer_addr);
self.peer_manager.remove_peer(&peer_addr).await;
self.sessions.remove(&peer_addr).await;
}
if self.peer_manager.count().await >= MAX_PEERS {
Log::new(Some(&status_tx)).warn(format!(
"OpenVPN: peer table full ({}), ignoring reset from {}",
MAX_PEERS, peer_addr
));
return;
}
let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
let peer = Peer::new(connection_id, peer_addr, frame.session_id, key_id);
self.peer_manager.add_peer(peer).await;
Log::new(Some(&status_tx)).info(format!(
"OpenVPN reset from {} (session {:016x}, {:?})",
peer_addr, frame.session_id, frame.opcode
));
let this = self.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
this.decide_and_answer(
peer_addr,
connection_id,
frame,
key_id,
client_packet_id,
app_state,
server_id,
status_tx,
)
.await;
})
.await;
}
#[allow(clippy::too_many_arguments)]
async fn decide_and_answer(
&self,
peer_addr: SocketAddr,
connection_id: ConnectionId,
frame: ControlFrame,
key_id: u8,
client_packet_id: u32,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) {
let event = Event::new(
&OPENVPN_PEER_RESET_EVENT,
serde_json::json!({
"peer_addr": peer_addr.to_string(),
"client_session_id": format!("{:016x}", frame.session_id),
"key_id": key_id,
"reset_type": format!("{:?}", frame.opcode),
"packet_id": client_packet_id,
"peer_count": self.peer_manager.count().await,
}),
);
let protocol = OpenvpnProtocol::new();
let outcome = call_llm(
&self.llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await;
let log = Log::new(Some(&status_tx));
let decision = match outcome {
Ok(result) => {
for message in &result.messages {
log.info(message);
}
extract_decision(&result.protocol_results, actions::PEER_DECISION_RESULT)
}
Err(e) => {
let class = match WireFailure::classify(&e) {
WireFailure::Overloaded => "overloaded",
WireFailure::Unavailable => "unavailable",
};
log.error(format!(
"OpenVPN {} decision=fail_closed_llm_error class={} - leaving it \
unanswered (the protocol has no reply that is not an admission): {}",
peer_addr, class, e
));
None
}
};
match decision {
Some(Decision::Accept { reason }) => {
self.accept_peer(
peer_addr,
connection_id,
frame.session_id,
key_id,
client_packet_id,
reason,
&app_state,
server_id,
&status_tx,
)
.await;
}
Some(Decision::Reject { reason }) => {
self.peer_manager
.set_admission(&peer_addr, PeerAdmission::Rejected)
.await;
log.info(format!(
"OpenVPN {} decision=model_reject - refused, no reply sent ({})",
peer_addr,
reason.as_deref().unwrap_or("no reason given")
));
}
None => {
self.peer_manager
.set_admission(&peer_addr, PeerAdmission::Rejected)
.await;
log.warn(format!(
"OpenVPN {} decision=fail_closed_no_action - the model produced neither \
accept_peer nor reject_peer; leaving it unanswered",
peer_addr
));
}
}
}
#[allow(clippy::too_many_arguments)]
async fn accept_peer(
&self,
peer_addr: SocketAddr,
connection_id: ConnectionId,
client_session_id: u64,
key_id: u8,
client_packet_id: u32,
reason: Option<String>,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: &mpsc::UnboundedSender<String>,
) {
let control = match ControlSession::new(
connection_id,
client_session_id,
self.server_session_id,
key_id,
client_packet_id,
self.tls_config.clone(),
) {
Ok(s) => s,
Err(e) => {
self.peer_manager
.set_admission(&peer_addr, PeerAdmission::Rejected)
.await;
Log::new(Some(status_tx)).error(format!(
"OpenVPN {} decision=fail_closed_no_control_channel - could not start a \
control session, so nothing was sent: {}",
peer_addr, e
));
return;
}
};
self.sessions.insert(peer_addr, control).await;
self.peer_manager
.update_peer(&peer_addr, |p| {
p.admission = PeerAdmission::Accepted;
})
.await;
let sent = self.flush_peer(peer_addr).await;
if sent == 0 {
Log::new(Some(status_tx)).error(format!(
"OpenVPN: reply to {} could not be sent; the peer is left unanswered",
peer_addr
));
self.sessions.remove(&peer_addr).await;
self.peer_manager
.set_admission(&peer_addr, PeerAdmission::Rejected)
.await;
return;
}
let now = crate::utils::clock::Instant::now();
app_state
.add_connection_to_server(
server_id,
ConnectionState {
id: connection_id,
remote_addr: peer_addr,
local_addr: self.local_addr,
bytes_sent: sent,
bytes_received: 0,
packets_sent: 1,
packets_received: 1,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
},
)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
Log::new(Some(status_tx)).info(format!(
"OpenVPN {} decision=model_accept - answered with HARD_RESET_SERVER_V2 ({} bytes){}",
peer_addr,
sent,
reason.map(|r| format!(" - {}", r)).unwrap_or_default()
));
}
async fn flush_peer(&self, peer_addr: SocketAddr) -> u64 {
let datagrams = self
.sessions
.with(&peer_addr, |s| s.drain_datagrams(Instant::now()))
.await
.unwrap_or_default();
let mut sent = 0u64;
for datagram in &datagrams {
match self.socket.send_to(datagram, peer_addr).await {
Ok(n) => sent = sent.saturating_add(n as u64),
Err(e) => {
warn!("OpenVPN: send to {} failed: {}", peer_addr, e);
break;
}
}
}
if sent > 0 {
self.peer_manager
.update_peer(&peer_addr, |p| p.record_sent(sent))
.await;
}
sent
}
async fn handle_control(
self: Arc<Self>,
frame: ControlFrame,
peer_addr: SocketAddr,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) {
match self.peer_manager.get_peer(&peer_addr).await {
Some(p) if p.admission == PeerAdmission::Accepted => {}
Some(_) => {
trace!("OpenVPN: control packet from unanswered peer {}", peer_addr);
return;
}
None => {
trace!("OpenVPN: control packet from unknown peer {}", peer_addr);
return;
}
}
let payload_len = frame.payload.len();
let hint = describe_tls_payload(&frame.payload);
trace!(
"OpenVPN: control payload from {}: {}",
peer_addr,
hex_prefix(&frame.payload, 64)
);
let events = match self
.sessions
.with(&peer_addr, |s| s.on_control_frame(frame))
.await
{
Some(events) => events,
None => {
trace!("OpenVPN: control packet from {} has no session", peer_addr);
return;
}
};
let (first_control, connection_id) = self
.peer_manager
.update_peer_returning(&peer_addr, |p| {
p.record_received(payload_len as u64);
let first = !p.saw_control_payload;
p.saw_control_payload = true;
(first, Some(p.connection_id))
})
.await
.unwrap_or((false, None));
if let Some(connection_id) = connection_id {
app_state
.update_connection_stats(
server_id,
connection_id,
Some(payload_len as u64),
None,
Some(1),
None,
)
.await;
}
if first_control && payload_len > 0 {
Log::new(Some(&status_tx)).info(format!(
"OpenVPN: {} sent {} ({} bytes); feeding it to the control-channel TLS session",
peer_addr, hint, payload_len
));
}
self.flush_peer(peer_addr).await;
for event in events {
self.clone()
.handle_session_event(event, peer_addr, &app_state, server_id, &status_tx)
.await;
}
}
async fn handle_session_event(
self: Arc<Self>,
event: SessionEvent,
peer_addr: SocketAddr,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: &mpsc::UnboundedSender<String>,
) {
let log = Log::new(Some(status_tx));
match event {
SessionEvent::TlsEstablished { version, cipher } => {
log.info(format!(
"OpenVPN {} control channel TLS handshake completed ({}, {})",
peer_addr, version, cipher
));
}
SessionEvent::ClientKeyExchange(km2) => {
let connection_id = self
.sessions
.with(&peer_addr, |s| s.connection_id)
.await
.unwrap_or_else(|| ConnectionId::new(0));
log.info(format!(
"OpenVPN {} sent key method 2: username {:?}, {} bytes of peer info",
peer_addr,
km2.username,
km2.peer_info.len()
));
debug!(
"OpenVPN {} credentials: username={:?} password={:?} options={:?}",
peer_addr, km2.username, km2.password, km2.options
);
let this = self.clone();
let state = app_state.clone();
let status = status_tx.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
this.decide_key_exchange(
peer_addr,
connection_id,
km2,
state,
server_id,
status,
)
.await;
})
.await;
}
SessionEvent::ControlMessage(message) => {
if message.starts_with("PUSH_REQUEST") {
log.warn(format!(
"OpenVPN {} sent PUSH_REQUEST, which this server does not answer: it \
derives no data channel keys and has no TUN device, so the client will \
time out here rather than build a tunnel",
peer_addr
));
} else {
log.info(format!(
"OpenVPN {} control message: {}",
peer_addr,
crate::utils::truncate::truncate_for_log(&message, 200)
));
}
}
SessionEvent::TlsFailed(reason) => {
log.warn(format!(
"OpenVPN {} control-channel TLS session failed: {}",
peer_addr, reason
));
self.flush_peer(peer_addr).await;
}
}
}
async fn decide_key_exchange(
self: Arc<Self>,
peer_addr: SocketAddr,
connection_id: ConnectionId,
km2: Box<ClientKeyMethod2>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
status_tx: mpsc::UnboundedSender<String>,
) {
let event = Event::new(
&OPENVPN_KEY_EXCHANGE_EVENT,
serde_json::json!({
"peer_addr": peer_addr.to_string(),
"username": km2.username,
"password": km2.password,
"has_credentials": !km2.username.is_empty(),
"options": km2.options,
"peer_info": keymethod::peer_info_map(&km2.peer_info),
}),
);
let protocol = OpenvpnProtocol::new();
let outcome = call_llm(
&self.llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
&protocol,
)
.await;
let log = Log::new(Some(&status_tx));
let decision = match outcome {
Ok(result) => {
for message in &result.messages {
log.info(message);
}
extract_decision(
&result.protocol_results,
actions::KEY_EXCHANGE_DECISION_RESULT,
)
}
Err(e) => {
let class = match WireFailure::classify(&e) {
WireFailure::Overloaded => "overloaded",
WireFailure::Unavailable => "unavailable",
};
log.error(format!(
"OpenVPN {} decision=fail_closed_llm_error class={} - key exchange left \
unanswered (no message at this point in the handshake means anything but \
'admitted'): {}",
peer_addr, class, e
));
None
}
};
match decision {
Some(Decision::Accept { reason }) => {
let written = self
.sessions
.with(&peer_addr, |s| s.write_key_method_2_answer(&km2.options))
.await;
match written {
Some(Ok(options)) => {
self.flush_peer(peer_addr).await;
log.info(format!(
"OpenVPN {} decision=model_accept - answered key method 2 with \
options {:?}{}",
peer_addr,
crate::utils::truncate::truncate_for_log(&options, 160),
reason.map(|r| format!(" - {}", r)).unwrap_or_default()
));
}
Some(Err(e)) => log.error(format!(
"OpenVPN {} decision=fail_closed_write_error - the key method 2 answer \
could not be written: {}",
peer_addr, e
)),
None => log.warn(format!(
"OpenVPN {} decision=fail_closed_no_session - the control session was \
gone before the key method 2 answer could be written",
peer_addr
)),
}
}
Some(Decision::Reject { reason }) => {
log.info(format!(
"OpenVPN {} decision=model_reject - key exchange refused, nothing sent ({})",
peer_addr,
reason.as_deref().unwrap_or("no reason given")
));
self.sessions.remove(&peer_addr).await;
}
None => {
log.warn(format!(
"OpenVPN {} decision=fail_closed_no_action - the model produced neither \
accept_key_exchange nor reject_key_exchange; the key exchange is left \
unanswered",
peer_addr
));
self.sessions.remove(&peer_addr).await;
}
}
}
async fn handle_ack(&self, frame: ControlFrame, peer_addr: SocketAddr) {
trace!(
"OpenVPN: ACK from {} for {:?}",
peer_addr,
frame.ack_packet_ids
);
self.sessions
.with(&peer_addr, |s| s.on_ack_frame(&frame))
.await;
self.peer_manager.touch(&peer_addr).await;
}
async fn handle_data(
&self,
datagram: &[u8],
peer_addr: SocketAddr,
status_tx: &mpsc::UnboundedSender<String>,
) {
let frame = match DataFrame::parse(datagram) {
Ok(f) => f,
Err(e) => {
debug!("OpenVPN: malformed data packet from {}: {}", peer_addr, e);
return;
}
};
let first = self
.peer_manager
.update_peer_returning(&peer_addr, |p| {
p.record_received(frame.payload.len() as u64);
let first = !p.saw_data_packet;
p.saw_data_packet = true;
first
})
.await
.unwrap_or(false);
debug!(
"OpenVPN: dropping {:?} from {} ({} ciphertext bytes, peer id {:?}): no data \
channel keys exist",
frame.opcode,
peer_addr,
frame.payload.len(),
frame.peer_id
);
if first {
Log::new(Some(status_tx)).warn(format!(
"OpenVPN: {} sent a data packet, but this server derives no data channel keys, \
so it cannot be decrypted and is dropped",
peer_addr
));
}
}
pub async fn accepted_peers(&self) -> Vec<SocketAddr> {
self.peer_manager
.get_all_peers()
.await
.into_iter()
.filter(|p| p.admission == PeerAdmission::Accepted)
.map(|p| p.addr)
.collect()
}
}
enum Decision {
Accept { reason: Option<String> },
Reject { reason: Option<String> },
}
fn extract_decision(results: &[ActionResult], wanted: &str) -> Option<Decision> {
fn walk(results: &[ActionResult], wanted: &str, out: &mut Option<Decision>) {
for result in results {
if out.is_some() {
return;
}
match result {
ActionResult::Custom { name, data } if name == wanted => {
let reason = data
.get("reason")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
match data.get("accept").and_then(|v| v.as_bool()) {
Some(true) => *out = Some(Decision::Accept { reason }),
Some(false) => *out = Some(Decision::Reject { reason }),
None => {}
}
}
ActionResult::Multiple(inner) => walk(inner, wanted, out),
_ => {}
}
}
}
let mut out = None;
walk(results, wanted, &mut out);
out
}
fn describe_tls_payload(payload: &[u8]) -> &'static str {
match payload.first() {
Some(0x16) => "a TLS handshake record (ClientHello)",
Some(0x14) => "a TLS change-cipher-spec record",
Some(0x15) => "a TLS alert record",
Some(0x17) => "a TLS application-data record",
Some(_) => "a non-TLS control payload",
None => "an empty control payload",
}
}
fn hex_prefix(data: &[u8], limit: usize) -> String {
let shown = &data[..data.len().min(limit)];
let mut s = String::with_capacity(shown.len() * 2 + 16);
for b in shown {
s.push_str(&format!("{:02x}", b));
}
if data.len() > shown.len() {
s.push_str(&format!("... ({} bytes total)", data.len()));
}
s
}