portls 0.3.0

Modern cross-platform port inspector - ls for network ports
Documentation
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use anyhow::{bail, Context, Result};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawSocket {
    pub local_addr: IpAddr,
    pub local_port: u16,
    pub remote_addr: IpAddr,
    pub remote_port: u16,
    pub state: SocketState,
    pub inode: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocketState {
    Established,
    SynSent,
    SynRecv,
    FinWait1,
    FinWait2,
    TimeWait,
    Close,
    CloseWait,
    LastAck,
    Listen,
    Closing,
    Unknown(u8),
}

impl SocketState {
    fn from_hex(hex: &str) -> Result<Self> {
        let num = u8::from_str_radix(hex, 16).context("Invalid state hex")?;
        Ok(match num {
            1 => SocketState::Established,
            2 => SocketState::SynSent,
            3 => SocketState::SynRecv,
            4 => SocketState::FinWait1,
            5 => SocketState::FinWait2,
            6 => SocketState::TimeWait,
            7 => SocketState::Close,
            8 => SocketState::CloseWait,
            9 => SocketState::LastAck,
            10 => SocketState::Listen,
            11 => SocketState::Closing,
            n => SocketState::Unknown(n),
        })
    }
}

pub fn parse_hex_addr(hex: &str) -> Result<Ipv4Addr> {
    let num = u32::from_str_radix(hex, 16).context("Invalid address hex")?;
    Ok(Ipv4Addr::new(
        (num & 0xFF) as u8,
        ((num >> 8) & 0xFF) as u8,
        ((num >> 16) & 0xFF) as u8,
        ((num >> 24) & 0xFF) as u8,
    ))
}

pub fn parse_hex_port(hex: &str) -> Result<u16> {
    u16::from_str_radix(hex, 16).context("Invalid port hex")
}

pub fn parse_hex_addr_v6(hex: &str) -> Result<Ipv6Addr> {
    if hex.len() != 32 {
        bail!("IPv6 address must be 32 hex chars, got {}", hex.len());
    }

    let mut octets = [0u8; 16];

    for i in 0..4 {
        let word_hex = &hex[i * 8..(i + 1) * 8];
        let word = u32::from_str_radix(word_hex, 16).context("Invalid IPv6 hex")?;

        let base = i * 4;
        octets[base] = (word & 0xFF) as u8;
        octets[base + 1] = ((word >> 8) & 0xFF) as u8;
        octets[base + 2] = ((word >> 16) & 0xFF) as u8;
        octets[base + 3] = ((word >> 24) & 0xFF) as u8;
    }

    Ok(Ipv6Addr::from(octets))
}

pub fn parse_hex_addr_any(hex: &str) -> Result<IpAddr> {
    match hex.len() {
        8 => Ok(IpAddr::V4(parse_hex_addr(hex)?)),
        32 => Ok(IpAddr::V6(parse_hex_addr_v6(hex)?)),
        n => bail!("Invalid address length: {} (expected 8 or 32)", n),
    }
}

pub fn parse_socket_line(line: &str) -> Result<RawSocket> {
    let parts: Vec<&str> = line.split_whitespace().collect();

    if parts.len() < 10 {
        bail!("Invalid socket line: not enough fields");
    }

    let local = parts[1];
    let remote = parts[2];
    let state_hex = parts[3];
    let inode_str = parts[9];

    let (local_addr_hex, local_port_hex) = local
        .split_once(':')
        .context("Invalid local address format")?;
    let (remote_addr_hex, remote_port_hex) = remote
        .split_once(':')
        .context("Invalid remote address format")?;

    Ok(RawSocket {
        local_addr: parse_hex_addr_any(local_addr_hex)?,
        local_port: parse_hex_port(local_port_hex)?,
        remote_addr: parse_hex_addr_any(remote_addr_hex)?,
        remote_port: parse_hex_port(remote_port_hex)?,
        state: SocketState::from_hex(state_hex)?,
        inode: inode_str.parse().context("Invalid inode")?,
    })
}

pub fn parse_proc_net_file(content: &str) -> Vec<RawSocket> {
    content
        .lines()
        .skip(1)
        .filter_map(|line| parse_socket_line(line).ok())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_hex_addr_localhost() {
        let result = parse_hex_addr("0100007F").unwrap();
        assert_eq!(result, Ipv4Addr::new(127, 0, 0, 1));
    }

    #[test]
    fn test_parse_hex_addr_any() {
        let result = parse_hex_addr("00000000").unwrap();
        assert_eq!(result, Ipv4Addr::new(0, 0, 0, 0));
    }

    #[test]
    fn test_parse_hex_addr_192_168_1_5() {
        let result = parse_hex_addr("0501A8C0").unwrap();
        assert_eq!(result, Ipv4Addr::new(192, 168, 1, 5));
    }

    #[test]
    fn test_parse_hex_port_8080() {
        let result = parse_hex_port("1F90").unwrap();
        assert_eq!(result, 8080);
    }

    #[test]
    fn test_parse_hex_port_443() {
        let result = parse_hex_port("01BB").unwrap();
        assert_eq!(result, 443);
    }

    #[test]
    fn test_socket_state_listen() {
        let result = SocketState::from_hex("0A").unwrap();
        assert_eq!(result, SocketState::Listen);
    }

    #[test]
    fn test_socket_state_established() {
        let result = SocketState::from_hex("01").unwrap();
        assert_eq!(result, SocketState::Established);
    }

    #[test]
    fn test_parse_socket_line_listening_ipv4() {
        let line = "   0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000   500        0 12345 1 0000000000000000 100 0 0 10 0";

        let result = parse_socket_line(line).unwrap();

        assert_eq!(result.local_addr, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
        assert_eq!(result.local_port, 8080);
        assert_eq!(result.remote_addr, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
        assert_eq!(result.remote_port, 0);
        assert_eq!(result.state, SocketState::Listen);
        assert_eq!(result.inode, 12345);
    }

    #[test]
    fn test_parse_socket_line_established_ipv4() {
        let line = "   1: 0100007F:1F90 0501A8C0:D431 01 00000000:00000000 00:00000000 00000000   500        0 12346 1 0000000000000000 100 0 0 10 0";

        let result = parse_socket_line(line).unwrap();

        assert_eq!(result.local_addr, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
        assert_eq!(result.local_port, 8080);
        assert_eq!(
            result.remote_addr,
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 5))
        );
        assert_eq!(result.remote_port, 54321);
        assert_eq!(result.state, SocketState::Established);
        assert_eq!(result.inode, 12346);
    }

    #[test]
    fn test_parse_socket_line_ipv6_listening() {
        let line = "   0: 00000000000000000000000001000000:1F90 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000   500        0 12347 1 0000000000000000 100 0 0 10 0";

        let result = parse_socket_line(line).unwrap();

        assert_eq!(
            result.local_addr,
            IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
        );
        assert_eq!(result.local_port, 8080);
        assert_eq!(result.state, SocketState::Listen);
    }

    #[test]
    fn test_parse_proc_net_file_skips_header() {
        let content = "  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000   500        0 12345 1 0000000000000000 100 0 0 10 0";

        let result = parse_proc_net_file(content);

        assert_eq!(result.len(), 1);
        assert_eq!(result[0].local_port, 8080);
    }

    #[test]
    fn test_parse_proc_net_file_multiple_lines() {
        let content = "  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000   500        0 12345 1 0000000000000000 100 0 0 10 0
   1: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 12346 1 0000000000000000 100 0 0 10 0";

        let result = parse_proc_net_file(content);

        assert_eq!(result.len(), 2);
        assert_eq!(result[0].local_port, 8080);
        assert_eq!(result[1].local_port, 80);
    }

    #[test]
    fn test_parse_hex_addr_v6_loopback() {
        let result = parse_hex_addr_v6("00000000000000000000000001000000").unwrap();
        assert_eq!(result, Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
    }

    #[test]
    fn test_parse_hex_addr_v6_any() {
        let result = parse_hex_addr_v6("00000000000000000000000000000000").unwrap();
        assert_eq!(result, Ipv6Addr::UNSPECIFIED);
    }

    #[test]
    fn test_parse_hex_addr_v6_ipv4_mapped() {
        let result = parse_hex_addr_v6("0000000000000000FFFF00000100007F").unwrap();
        assert_eq!(result, "::ffff:127.0.0.1".parse::<Ipv6Addr>().unwrap());
    }

    #[test]
    fn test_parse_hex_addr_v6_invalid_length() {
        let result = parse_hex_addr_v6("0100007F");
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_hex_addr_any_ipv4() {
        let result = parse_hex_addr_any("0100007F").unwrap();
        assert_eq!(result, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
    }

    #[test]
    fn test_parse_hex_addr_any_ipv6() {
        let result = parse_hex_addr_any("00000000000000000000000001000000").unwrap();
        assert_eq!(result, IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)));
    }
}