use crate::notifications;
use crate::AppState;
use anyhow::{anyhow, Result};
use std::sync::Arc;
use std::time::Duration;
use sysinfo::System;
use tokio::sync::watch;
use tokio::sync::RwLock;
use tracing::{info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SocketEntry {
pub local_addr: u32,
pub local_port: u16,
pub pid: u32,
}
pub fn enumerate_listening_sockets_raw() -> Result<Vec<SocketEntry>> {
use windows_sys::Win32::NetworkManagement::IpHelper::{
GetExtendedTcpTable, TCP_TABLE_OWNER_PID_LISTENER,
};
use windows_sys::Win32::Networking::WinSock::AF_INET;
let mut pdw_size: u32 = 0;
let mut sockets = Vec::new();
let result = unsafe {
GetExtendedTcpTable(
std::ptr::null_mut(),
&mut pdw_size,
1, AF_INET as u32,
TCP_TABLE_OWNER_PID_LISTENER,
0,
)
};
const ERROR_INSUFFICIENT_BUFFER: u32 = 122;
if result != ERROR_INSUFFICIENT_BUFFER && result != 0 {
return Err(anyhow!(
"GetExtendedTcpTable size query failed: code {}",
result
));
}
for attempt in 0..3 {
let mut buffer = vec![0u8; pdw_size as usize];
let result = unsafe {
GetExtendedTcpTable(
buffer.as_mut_ptr() as *mut std::ffi::c_void,
&mut pdw_size,
1,
AF_INET as u32,
TCP_TABLE_OWNER_PID_LISTENER,
0,
)
};
if result == ERROR_INSUFFICIENT_BUFFER {
if attempt < 2 {
continue; }
return Err(anyhow!(
"GetExtendedTcpTable: buffer too small after {} retries",
attempt + 1
));
}
if result != 0 {
return Err(anyhow!("GetExtendedTcpTable failed: code {}", result));
}
if buffer.len() < 4 {
return Err(anyhow!("Buffer too small to read dwNumEntries"));
}
let dw_num_entries = u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
const ROW_SIZE: usize = 24;
let expected_size = 4 + dw_num_entries as usize * ROW_SIZE;
if buffer.len() < expected_size {
return Err(anyhow!(
"Buffer too small: expected {}, got {}",
expected_size,
buffer.len()
));
}
for i in 0..dw_num_entries as usize {
let offset = 4 + i * ROW_SIZE;
let row_data = &buffer[offset..offset + ROW_SIZE];
let dw_local_addr = u32::from_le_bytes([row_data[4], row_data[5], row_data[6], row_data[7]]);
let dw_local_port = u32::from_le_bytes([row_data[8], row_data[9], row_data[10], row_data[11]]);
let pid = u32::from_le_bytes([row_data[20], row_data[21], row_data[22], row_data[23]]);
let local_port = ((dw_local_port as u16).swap_bytes()) as u16;
sockets.push(SocketEntry {
local_addr: dw_local_addr,
local_port,
pid,
});
}
return Ok(sockets);
}
Err(anyhow!(
"GetExtendedTcpTable: failed after maximum retries"
))
}
pub fn find_exposed_ollama(sockets: &[SocketEntry], port: u16) -> Option<u32> {
sockets
.iter()
.find(|entry| entry.local_addr == 0 && entry.local_port == port)
.map(|entry| entry.pid)
}
fn kill_exposed_process(pid: u32, state: &mut AppState) {
let mut sys = System::new();
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, false);
if let Some(proc) = sys.process(sysinfo::Pid::from_u32(pid)) {
proc.kill();
state.ollama_running = false;
warn!("Killed exposed Ollama process (PID {})", pid);
}
}
fn notify_exposure_kill(state: &AppState) {
match state.notification_hwnd {
Some(hwnd) => {
notifications::show_balloon(
hwnd as *mut std::ffi::c_void,
"FreeCycle: Security Alert",
"Ollama was exposed and has been restarted securely",
notifications::BalloonKind::Warning,
);
}
None => {
warn!("Exposure monitor: notification_hwnd not available for balloon notification");
}
}
}
pub async fn run_exposure_monitor(
state: Arc<RwLock<AppState>>,
mut shutdown_rx: watch::Receiver<bool>,
) {
info!("Exposure monitor starting");
loop {
{
let app_state = state.read().await;
if app_state.config.agent_server.compatibility_mode {
info!("Exposure monitor: compatibility_mode is true, exiting");
return;
}
}
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(60)) => {
}
_ = shutdown_rx.changed() => {
info!("Exposure monitor: shutdown signal received, exiting");
break;
}
}
let ollama_port = {
let app_state = state.read().await;
app_state.config.ollama.port
};
match enumerate_listening_sockets_raw() {
Ok(sockets) => {
if let Some(exposed_pid) = find_exposed_ollama(&sockets, ollama_port) {
let mut app_state = state.write().await;
kill_exposed_process(exposed_pid, &mut app_state);
notify_exposure_kill(&app_state);
}
}
Err(e) => {
warn!("Exposure monitor: failed to enumerate sockets: {}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_exposed_ollama_exposed_on_all_interfaces() {
let sockets = vec![SocketEntry {
local_addr: 0, local_port: 11434,
pid: 1234,
}];
assert_eq!(find_exposed_ollama(&sockets, 11434), Some(1234));
}
#[test]
fn test_find_exposed_ollama_not_exposed() {
let sockets = vec![SocketEntry {
local_addr: 0x7f000001, local_port: 11434,
pid: 1234,
}];
assert_eq!(find_exposed_ollama(&sockets, 11434), None);
}
#[test]
fn test_find_exposed_ollama_different_port() {
let sockets = vec![SocketEntry {
local_addr: 0,
local_port: 8080,
pid: 1234,
}];
assert_eq!(find_exposed_ollama(&sockets, 11434), None);
}
#[test]
fn test_find_exposed_ollama_multiple_sockets_first_match() {
let sockets = vec![
SocketEntry {
local_addr: 0x7f000001, local_port: 11434,
pid: 1000,
},
SocketEntry {
local_addr: 0, local_port: 11434,
pid: 2000,
},
];
assert_eq!(find_exposed_ollama(&sockets, 11434), Some(2000));
}
#[test]
fn test_port_byte_order_conversion() {
let dw_port = 0x0000AA2Cu32;
let host_port = ((dw_port as u16).swap_bytes()) as u16;
assert_eq!(host_port, 11434);
}
#[test]
fn test_empty_socket_list() {
let sockets: Vec<SocketEntry> = vec![];
assert_eq!(find_exposed_ollama(&sockets, 11434), None);
}
}