soth-mitm 0.3.3

Rust intercepting proxy crate with deterministic handler/event contracts for SOTH.
Documentation
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use super::{ip_matches, push_unique_pid, select_unique_pid, SocketQuery};

#[derive(Debug, Clone, Copy)]
struct LinuxSocketEntry {
    local_ip: IpAddr,
    local_port: u16,
    remote_ip: IpAddr,
    remote_port: u16,
    established: bool,
    inode: u64,
}

pub(super) fn lookup_established_tcp_pid_linux(query: SocketQuery) -> Option<u32> {
    let mut entries = linux_read_tcp_entries("/proc/net/tcp", false);
    entries.extend(linux_read_tcp_entries("/proc/net/tcp6", true));

    let mut fallback_inodes = Vec::new();

    for entry in entries {
        if !entry.established || entry.local_port != query.local_port {
            continue;
        }
        if !ip_matches(entry.local_ip, query.local_ip) {
            continue;
        }

        if entry.remote_port == query.remote_port && ip_matches(entry.remote_ip, query.remote_ip) {
            if let Some(pid) = linux_find_pid_by_inode(entry.inode) {
                return Some(pid);
            }
            continue;
        }

        if !fallback_inodes.contains(&entry.inode) {
            fallback_inodes.push(entry.inode);
        }
    }

    resolve_unique_linux_fallback_pid(fallback_inodes)
}

fn linux_read_tcp_entries(path: &str, is_v6: bool) -> Vec<LinuxSocketEntry> {
    let Ok(contents) = std::fs::read_to_string(path) else {
        return Vec::new();
    };

    contents
        .lines()
        .skip(1)
        .filter_map(|line| linux_parse_tcp_entry(line, is_v6))
        .collect()
}

fn linux_parse_tcp_entry(line: &str, is_v6: bool) -> Option<LinuxSocketEntry> {
    let fields: Vec<&str> = line.split_whitespace().collect();
    if fields.len() < 10 {
        return None;
    }

    let (local_ip, local_port) = linux_parse_socket_endpoint(fields[1], is_v6)?;
    let (remote_ip, remote_port) = linux_parse_socket_endpoint(fields[2], is_v6)?;
    let established = fields[3] == "01";
    let inode = fields[9].parse::<u64>().ok()?;

    Some(LinuxSocketEntry {
        local_ip,
        local_port,
        remote_ip,
        remote_port,
        established,
        inode,
    })
}

fn linux_parse_socket_endpoint(value: &str, is_v6: bool) -> Option<(IpAddr, u16)> {
    let (addr_hex, port_hex) = value.split_once(':')?;
    let port = u16::from_str_radix(port_hex, 16).ok()?;
    let ip = if is_v6 {
        linux_parse_ipv6(addr_hex)?
    } else {
        linux_parse_ipv4(addr_hex)?
    };
    Some((ip, port))
}

fn linux_parse_ipv4(hex: &str) -> Option<IpAddr> {
    if hex.len() != 8 {
        return None;
    }
    let value = u32::from_str_radix(hex, 16).ok()?;
    Some(IpAddr::V4(Ipv4Addr::from(value.to_le_bytes())))
}

fn linux_parse_ipv6(hex: &str) -> Option<IpAddr> {
    if hex.len() != 32 {
        return None;
    }

    let mut octets = [0u8; 16];
    for index in 0..4 {
        let start = index * 8;
        let end = start + 8;
        let chunk = &hex[start..end];
        let word = u32::from_str_radix(chunk, 16).ok()?;
        octets[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
    }

    Some(IpAddr::V6(Ipv6Addr::from(octets)))
}

fn linux_find_pid_by_inode(target_inode: u64) -> Option<u32> {
    for proc_entry in std::fs::read_dir("/proc").ok()? {
        let proc_entry = proc_entry.ok()?;
        let pid = proc_entry.file_name().to_str()?.parse::<u32>().ok()?;

        let fd_dir = proc_entry.path().join("fd");
        let Ok(fd_entries) = std::fs::read_dir(fd_dir) else {
            continue;
        };

        for fd_entry in fd_entries {
            let Ok(fd_entry) = fd_entry else {
                continue;
            };
            let Ok(link_target) = std::fs::read_link(fd_entry.path()) else {
                continue;
            };
            let link_text = link_target.to_string_lossy();
            if linux_parse_socket_inode(link_text.as_ref()) == Some(target_inode) {
                return Some(pid);
            }
        }
    }

    None
}

fn linux_parse_socket_inode(link_text: &str) -> Option<u64> {
    let prefix = "socket:[";
    let suffix = "]";
    if !link_text.starts_with(prefix) || !link_text.ends_with(suffix) {
        return None;
    }

    link_text[prefix.len()..link_text.len() - suffix.len()]
        .parse::<u64>()
        .ok()
}

fn resolve_unique_linux_fallback_pid(inodes: Vec<u64>) -> Option<u32> {
    let mut fallback_pids = Vec::new();
    for inode in inodes {
        let pid = linux_find_pid_by_inode(inode)?;
        push_unique_pid(&mut fallback_pids, pid);
        if fallback_pids.len() > 1 {
            return None;
        }
    }
    select_unique_pid(fallback_pids)
}