1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::collections::HashSet;
10use std::net::{Ipv4Addr, Ipv6Addr};
11
12pub fn inodes_using_port(port: u16) -> HashMap<u64, (String, String, String)> {
14 let mut map = HashMap::new();
15 for &is_v6 in &[false, true] {
16 for &udp in &[false, true] {
17 let proto = match (is_v6, udp) {
18 (false, false) => "TCP",
19 (false, true) => "UDP",
20 (true, false) => "TCP6",
21 (true, true) => "UDP6",
22 };
23 let path = match (is_v6, udp) {
24 (false, false) => "/proc/net/tcp",
25 (false, true) => "/proc/net/udp",
26 (true, false) => "/proc/net/tcp6",
27 (true, true) => "/proc/net/udp6",
28 };
29 if let Ok(raw) = std::fs::read_to_string(path) {
30 for line in raw.lines().skip(1) {
31 let fields: Vec<&str> = line.split_whitespace().collect();
32 if fields.len() < 10 {
33 continue;
34 }
35 let local = parse_addr(fields[1], is_v6);
36 let remote = parse_addr(fields[2], is_v6);
37 if local.1 != port && remote.1 != port {
38 continue;
39 }
40 let inode: u64 = fields[9].parse().unwrap_or(0);
41 let state_hex = u8::from_str_radix(fields[3], 16).unwrap_or(0);
42 let state = tcp_state(state_hex);
43 let kind = if state == "LISTEN" {
44 format!("LISTEN/{}", proto)
45 } else {
46 format!("CONN/{}", proto)
47 };
48 let local_s = format!("{}:{}", local.0, local.1);
49 let remote_s = if state == "LISTEN" {
50 "-".to_string()
51 } else {
52 format!("{}:{}", remote.0, remote.1)
53 };
54 map.insert(inode, (kind, local_s, remote_s));
55 }
56 }
57 }
58 }
59 map
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct SocketEntry {
64 pub protocol: String,
65 pub local_addr: String,
66 pub local_port: u16,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ConnectionEntry {
71 pub protocol: String,
72 pub local_addr: String,
73 pub local_port: u16,
74 pub remote_addr: String,
75 pub remote_port: u16,
76 pub state: String,
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct NetworkInfo {
81 pub listening_tcp: Vec<SocketEntry>,
82 pub listening_udp: Vec<SocketEntry>,
83 pub connections: Vec<ConnectionEntry>,
84}
85
86pub fn collect_network(pid: i32) -> anyhow::Result<NetworkInfo> {
89 let socket_inodes = process_socket_inodes(pid);
91
92 let mut listening_tcp = Vec::new();
94 let mut listening_udp = Vec::new();
95 let mut connections = Vec::new();
96
97 for &is_v6 in &[false, true] {
98 for &udp in &[false, true] {
99 let proto = match (is_v6, udp) {
100 (false, false) => "TCP",
101 (false, true) => "UDP",
102 (true, false) => "TCP6",
103 (true, true) => "UDP6",
104 };
105 let path = match (is_v6, udp) {
106 (false, false) => "/proc/net/tcp",
107 (false, true) => "/proc/net/udp",
108 (true, false) => "/proc/net/tcp6",
109 (true, true) => "/proc/net/udp6",
110 };
111
112 if let Ok(raw) = std::fs::read_to_string(path) {
113 for line in raw.lines().skip(1) {
114 let fields: Vec<&str> = line.split_whitespace().collect();
115 if fields.len() < 10 {
116 continue;
117 }
118 let inode: u64 = fields[9].parse().unwrap_or(0);
119 if !socket_inodes.contains(&inode) {
120 continue;
121 }
122
123 let local = parse_addr(fields[1], is_v6);
124 let remote = parse_addr(fields[2], is_v6);
125 let state_hex = u8::from_str_radix(fields[3], 16).unwrap_or(0);
126 let state = tcp_state(state_hex);
127
128 if state == "LISTEN" {
129 let entry = SocketEntry {
130 protocol: proto.to_string(),
131 local_addr: local.0,
132 local_port: local.1,
133 };
134 if udp {
135 listening_udp.push(entry);
136 } else {
137 listening_tcp.push(entry);
138 }
139 } else if !udp || remote.1 != 0 {
140 connections.push(ConnectionEntry {
141 protocol: proto.to_string(),
142 local_addr: local.0,
143 local_port: local.1,
144 remote_addr: remote.0,
145 remote_port: remote.1,
146 state: state.to_string(),
147 });
148 }
149 }
150 }
151 }
152 }
153
154 Ok(NetworkInfo {
155 listening_tcp,
156 listening_udp,
157 connections,
158 })
159}
160
161pub fn process_socket_inodes(pid: i32) -> HashSet<u64> {
163 let mut inodes = HashSet::new();
164 let fd_dir = format!("/proc/{}/fd", pid);
165 if let Ok(entries) = std::fs::read_dir(&fd_dir) {
166 for entry in entries.flatten() {
167 if let Ok(target) = std::fs::read_link(entry.path()) {
168 let s = target.to_string_lossy();
169 if let Some(inode_str) =
170 s.strip_prefix("socket:[").and_then(|s| s.strip_suffix(']'))
171 {
172 if let Ok(inode) = inode_str.parse::<u64>() {
173 inodes.insert(inode);
174 }
175 }
176 }
177 }
178 }
179 inodes
180}
181
182fn parse_addr(field: &str, is_v6: bool) -> (String, u16) {
184 let parts: Vec<&str> = field.splitn(2, ':').collect();
185 if parts.len() != 2 {
186 return ("?".to_string(), 0);
187 }
188 let port = u16::from_str_radix(parts[1], 16).unwrap_or(0);
189 let addr_hex = parts[0];
190
191 let addr = if is_v6 {
192 let bytes: Vec<u32> = addr_hex
194 .as_bytes()
195 .chunks(8)
196 .filter_map(|c| {
197 let s = std::str::from_utf8(c).ok()?;
198 u32::from_str_radix(s, 16).ok()
199 })
200 .collect();
201 if bytes.len() == 4 {
202 let b: Vec<u8> = bytes.iter().flat_map(|w| w.to_le_bytes()).collect();
203 let arr: [u8; 16] = b.try_into().unwrap_or([0; 16]);
204 Ipv6Addr::from(arr).to_string()
205 } else {
206 addr_hex.to_string()
207 }
208 } else if let Ok(n) = u32::from_str_radix(addr_hex, 16) {
209 let ip = Ipv4Addr::from(n.to_le_bytes());
210 ip.to_string()
211 } else {
212 addr_hex.to_string()
213 };
214
215 (addr, port)
216}
217
218fn tcp_state(state: u8) -> &'static str {
219 match state {
220 0x01 => "ESTABLISHED",
221 0x02 => "SYN_SENT",
222 0x03 => "SYN_RECV",
223 0x04 => "FIN_WAIT1",
224 0x05 => "FIN_WAIT2",
225 0x06 => "TIME_WAIT",
226 0x07 => "CLOSE",
227 0x08 => "CLOSE_WAIT",
228 0x09 => "LAST_ACK",
229 0x0A => "LISTEN",
230 0x0B => "CLOSING",
231 _ => "UNKNOWN",
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::{parse_addr, tcp_state};
238
239 #[test]
240 fn parses_ipv4_addr_and_port() {
241 let (addr, port) = parse_addr("0100007F:1F40", false);
243 assert_eq!(addr, "127.0.0.1");
244 assert_eq!(port, 8000);
245 }
246
247 #[test]
248 fn parses_tcp_states() {
249 assert_eq!(tcp_state(0x01), "ESTABLISHED");
250 assert_eq!(tcp_state(0x0A), "LISTEN");
251 assert_eq!(tcp_state(0xFF), "UNKNOWN");
252 }
253}