diff --git a/Cargo.toml b/Cargo.toml index 055743f..ad963bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ windows-sys = { version = "0.61.2", features = [ "Win32_Graphics_Gdi", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", + "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Cryptography", diff --git a/localization/de.toml b/localization/de.toml index 0c47338..fd0b83f 100644 --- a/localization/de.toml +++ b/localization/de.toml @@ -262,6 +262,7 @@ Adapter = "Adapter" NetworkUtilization = "Netzwerkauslastung" LinkSpeed = "Verbindungsgeschwindigkeit" State = "Status" +Ipv4Address = "IPv4-Adresse" BytesSent = "Gesendete Bytes" BytesReceived = "Empfangene Bytes" BytesTotal = "Bytes gesamt" diff --git a/localization/en_us.toml b/localization/en_us.toml index 220bc4a..628be42 100644 --- a/localization/en_us.toml +++ b/localization/en_us.toml @@ -269,6 +269,7 @@ Adapter = "Adapter" NetworkUtilization = "Network Utilization" LinkSpeed = "Link Speed" State = "State" +Ipv4Address = "IPv4 Address" BytesSent = "Bytes Sent" BytesReceived = "Bytes Received" BytesTotal = "Bytes Total" diff --git a/localization/es.toml b/localization/es.toml index 5c418f8..38d74a4 100644 --- a/localization/es.toml +++ b/localization/es.toml @@ -262,6 +262,7 @@ Adapter = "Adaptador" NetworkUtilization = "Utilizacion de red" LinkSpeed = "Velocidad del vinculo" State = "Estado" +Ipv4Address = "Dirección IPv4" BytesSent = "Bytes enviados" BytesReceived = "Bytes recibidos" BytesTotal = "Bytes totales" diff --git a/localization/fr.toml b/localization/fr.toml index b2e3a26..7edfbaf 100644 --- a/localization/fr.toml +++ b/localization/fr.toml @@ -262,6 +262,7 @@ Adapter = "Adaptateur" NetworkUtilization = "Utilisation réseau" LinkSpeed = "Vitesse du lien" State = "État" +Ipv4Address = "Adresse IPv4" BytesSent = "Octets envoyés" BytesReceived = "Octets reçus" BytesTotal = "Octets au total" diff --git a/localization/pt.toml b/localization/pt.toml index 4129216..b2b1fc9 100644 --- a/localization/pt.toml +++ b/localization/pt.toml @@ -262,6 +262,7 @@ Adapter = "Adaptador" NetworkUtilization = "Utilização da rede" LinkSpeed = "Velocidade do link" State = "Status" +Ipv4Address = "Endereço IPv4" BytesSent = "Bytes enviados" BytesReceived = "Bytes recebidos" BytesTotal = "Total de bytes" diff --git a/localization/ru.toml b/localization/ru.toml index 28b0c9e..9dd1aa8 100644 --- a/localization/ru.toml +++ b/localization/ru.toml @@ -262,6 +262,7 @@ Adapter = "Адаптер" NetworkUtilization = "Использование сети" LinkSpeed = "Скорость канала" State = "Состояние" +Ipv4Address = "Адрес IPv4" BytesSent = "Отправлено байт" BytesReceived = "Получено байт" BytesTotal = "Всего байт" diff --git a/localization/zh_cn.toml b/localization/zh_cn.toml index 3af90f1..6fada85 100644 --- a/localization/zh_cn.toml +++ b/localization/zh_cn.toml @@ -262,6 +262,7 @@ Adapter = "适配器" NetworkUtilization = "网络利用率" LinkSpeed = "链路速度" State = "状态" +Ipv4Address = "IPv4 地址" BytesSent = "已发送字节" BytesReceived = "已接收字节" BytesTotal = "总字节数" diff --git a/localization/zh_tw.toml b/localization/zh_tw.toml index 9ca48d8..90dbe2c 100644 --- a/localization/zh_tw.toml +++ b/localization/zh_tw.toml @@ -262,6 +262,7 @@ Adapter = "介面卡" NetworkUtilization = "網路使用率" LinkSpeed = "連線速度" State = "狀態" +Ipv4Address = "IPv4 位址" BytesSent = "已傳送位元組" BytesReceived = "已接收位元組" BytesTotal = "位元組總數" diff --git a/src/pages/network.rs b/src/pages/network.rs index 8642580..cba27c6 100644 --- a/src/pages/network.rs +++ b/src/pages/network.rs @@ -17,7 +17,7 @@ use std::ptr::null_mut; use std::slice; use std::time::Instant; -use windows_sys::Win32::Foundation::{HWND, RECT, WPARAM}; +use windows_sys::Win32::Foundation::{ERROR_BUFFER_OVERFLOW, ERROR_NO_DATA, HWND, RECT, WPARAM}; use windows_sys::Win32::Graphics::Gdi::{ BLACK_BRUSH, BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DC_PEN, DT_CALCRECT, DT_NOPREFIX, DT_RIGHT, DT_SINGLELINE, DT_TOP, DeleteDC, DeleteObject, DrawTextW, FillRect, @@ -26,10 +26,12 @@ use windows_sys::Win32::Graphics::Gdi::{ TRANSPARENT, }; use windows_sys::Win32::NetworkManagement::IpHelper::{ - FreeMibTable, GetIfTable2, IF_TYPE_SOFTWARE_LOOPBACK, IF_TYPE_TUNNEL, MIB_IF_ROW2, - MIB_IF_TABLE2, + FreeMibTable, GAA_FLAG_SKIP_ANYCAST, GAA_FLAG_SKIP_DNS_SERVER, GAA_FLAG_SKIP_MULTICAST, + GetAdaptersAddresses, GetIfTable2, IF_TYPE_SOFTWARE_LOOPBACK, IF_TYPE_TUNNEL, + IP_ADAPTER_ADDRESSES_LH, IP_ADAPTER_UNICAST_ADDRESS_LH, MIB_IF_ROW2, MIB_IF_TABLE2, }; use windows_sys::Win32::NetworkManagement::Ndis::IfOperStatusUp; +use windows_sys::Win32::Networking::WinSock::{AF_INET, SOCKET_ADDRESS}; use windows_sys::Win32::UI::Controls::{ LVCF_FMT, LVCF_SUBITEM, LVCF_TEXT, LVCF_WIDTH, LVCFMT_LEFT, LVCFMT_RIGHT, LVCOLUMNW, LVIF_PARAM, LVIF_TEXT, LVITEMW, LVM_DELETECOLUMN, LVM_DELETEITEM, LVM_GETITEMCOUNT, @@ -83,6 +85,7 @@ struct RawAdapterEntry { receive_link_speed_bps: u64, bytes_sent: u64, bytes_received: u64, + ipv4: String, } struct OwnedIfTable { @@ -120,7 +123,12 @@ impl Drop for OwnedIfTable { } } -type NetworkWorkerResult = Result, u32>; +struct AdapterSample { + adapters: Vec, + ipv4_error: Option, +} + +type NetworkWorkerResult = Result; struct NetworkWorkerCompletion { sampled_at: Instant, @@ -201,6 +209,7 @@ struct NetworkAdapterEntry { bytes_sent: String, bytes_received: String, bytes_total: String, + ipv4: String, current_sent: u64, current_received: u64, sent_history: HistoryBuffer, @@ -218,6 +227,7 @@ struct PreviousAdapterState { bytes_sent: String, bytes_received: String, bytes_total: String, + ipv4: String, current_sent: u64, current_received: u64, } @@ -249,6 +259,7 @@ pub struct NetworkPageState { cached_graph_height: i32, worker: Option>, last_refresh_error: Option, + last_ipv4_error: Option, } impl NetworkPageState { @@ -761,9 +772,10 @@ impl NetworkPageState { for completion in drained.completions { crate::infrastructure::diagnostics::with_operation_id(completion.operation_id, || { match completion.value.result { - Ok(adapters) => { + Ok(sample) => { self.last_refresh_error = None; - self.apply_adapter_snapshot(adapters, completion.value.sampled_at); + self.note_ipv4_error(sample.ipv4_error); + self.apply_adapter_snapshot(sample.adapters, completion.value.sampled_at); } Err(error) => self.set_refresh_error(error), } @@ -781,6 +793,15 @@ impl NetworkPageState { self.last_refresh_error = Some(error); } + fn note_ipv4_error(&mut self, error: Option) { + if self.last_ipv4_error != error { + if let Some(error) = error { + record_win32_error("network ipv4", error); + } + self.last_ipv4_error = error; + } + } + pub fn handle_worker_completion(&mut self) { self.drain_worker_results(); } @@ -820,6 +841,7 @@ impl NetworkPageState { bytes_sent, bytes_received, bytes_total, + ipv4, current_sent, current_received, sent_history, @@ -840,6 +862,7 @@ impl NetworkPageState { bytes_sent, bytes_received, bytes_total, + ipv4, current_sent, current_received, }), @@ -905,6 +928,7 @@ impl NetworkPageState { bytes_total: bytes_total .map(format_counter) .unwrap_or_else(|| "-".to_string()), + ipv4: raw_adapter.ipv4, current_sent: raw_adapter.bytes_sent, current_received: raw_adapter.bytes_received, sent_history, @@ -919,7 +943,8 @@ impl NetworkPageState { || previous_state.utilization != adapter.utilization || previous_state.bytes_sent != adapter.bytes_sent || previous_state.bytes_received != adapter.bytes_received - || previous_state.bytes_total != adapter.bytes_total; + || previous_state.bytes_total != adapter.bytes_total + || previous_state.ipv4 != adapter.ipv4; } adapters.push(adapter); } @@ -940,7 +965,7 @@ impl NetworkPageState { self.update_graphs(); } - fn collect_adapters() -> Result, u32> { + fn collect_adapters() -> Result { let mut raw_table = null_mut::(); let status = unsafe { GetIfTable2(&mut raw_table) }; if status != 0 { @@ -951,6 +976,10 @@ impl NetworkPageState { let table = unsafe { OwnedIfTable::from_raw(raw_table) } .ok_or(windows_sys::Win32::Foundation::ERROR_INVALID_DATA)?; + let ipv4_query = query_unicast_ipv4_by_index(); + let ipv4_error = ipv4_query.as_ref().err().copied(); + let ipv4_map = ipv4_query.ok(); + let mut adapters = Vec::with_capacity(table.rows().len()); for row in table.rows() { if !include_adapter(row) { @@ -978,9 +1007,13 @@ impl NetworkPageState { receive_link_speed_bps: row.ReceiveLinkSpeed, bytes_sent: row.OutOctets, bytes_received: row.InOctets, + ipv4: ipv4_label(row.InterfaceIndex, ipv4_map.as_ref()), }); } - Ok(adapters) + Ok(AdapterSample { + adapters, + ipv4_error, + }) } fn configure_columns(&self) { @@ -1005,6 +1038,7 @@ impl NetworkPageState { (titles[1], 96, LVCFMT_RIGHT), (titles[2], 90, LVCFMT_RIGHT), (titles[3], 120, LVCFMT_LEFT), + (titles[4], 140, LVCFMT_LEFT), ]; for (index, (title, width, fmt)) in columns.iter().enumerate() { @@ -1356,12 +1390,13 @@ fn push_history(history: &mut HistoryBuffer, value: u8) { history.push(value); } -fn adapter_row_texts(adapter: &NetworkAdapterEntry) -> [&str; 4] { +fn adapter_row_texts(adapter: &NetworkAdapterEntry) -> [&str; 5] { [ &adapter.name, &adapter.utilization, &adapter.link_speed, &adapter.state, + &adapter.ipv4, ] } @@ -1370,6 +1405,123 @@ fn include_adapter(row: &MIB_IF_ROW2) -> bool { row.Type != IF_TYPE_SOFTWARE_LOOPBACK && row.Type != IF_TYPE_TUNNEL } +const MAX_ADAPTER_ADDRESS_BYTES: u32 = 1024 * 1024; + +fn ipv4_label(interface_index: u32, addresses: Option<&HashMap>) -> String { + match addresses { + Some(addresses) => addresses + .get(&interface_index) + .cloned() + .unwrap_or_else(|| "-".to_string()), + None => adapter_state("Unknown").to_string(), + } +} + +fn query_unicast_ipv4_by_index() -> Result, u32> { + // 跳过 anycast / multicast / DNS,只留下单播地址供 FirstUnicastAddress 遍历。 + let flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + let mut size = 16 * 1024u32; + for _attempt in 0..4 { + if size == 0 || size > MAX_ADAPTER_ADDRESS_BYTES { + return Err(ERROR_BUFFER_OVERFLOW); + } + let align = std::mem::align_of::(); + let mut storage = vec![0u8; size as usize + align]; + let base = storage.as_mut_ptr() as usize; + let aligned = (base + align - 1) & !(align - 1); + let head = aligned as *mut IP_ADAPTER_ADDRESSES_LH; + // SAFETY: `head` points at `align_of::()`-aligned storage that + // stays alive for this call. A null reserved pointer is the documented unused argument. + // On success the API writes a linked list into `storage`; the walk below copies owned + // strings out before `storage` drops. + let status = unsafe { + GetAdaptersAddresses(u32::from(AF_INET), flags, std::ptr::null(), head, &mut size) + }; + if status == ERROR_BUFFER_OVERFLOW { + continue; + } + if status == ERROR_NO_DATA { + return Ok(HashMap::new()); + } + if status != 0 { + return Err(status); + } + return Ok(ipv4_map_from_adapters(head)); + } + Err(ERROR_BUFFER_OVERFLOW) +} + +fn ipv4_map_from_adapters(mut head: *const IP_ADAPTER_ADDRESSES_LH) -> HashMap { + let mut addresses = HashMap::new(); + let mut adapters_seen = 0u32; + while !head.is_null() && adapters_seen < 4096 { + adapters_seen += 1; + // SAFETY: `head` is a node written by the successful `GetAdaptersAddresses` call whose + // buffer is still alive. `Next` is either null or another node in that buffer. + let adapter = unsafe { &*head }; + let text = ipv4_text_from_unicast(adapter.FirstUnicastAddress); + if text != "-" { + // SAFETY: `Anonymous1.Anonymous` is the Length/IfIndex view that + // `GetAdaptersAddresses` initializes for every returned adapter. + let interface_index = unsafe { adapter.Anonymous1.Anonymous.IfIndex }; + addresses.insert(interface_index, text); + } + head = adapter.Next; + } + addresses +} + +fn ipv4_text_from_unicast(mut node: *const IP_ADAPTER_UNICAST_ADDRESS_LH) -> String { + let mut parts = Vec::new(); + let mut seen = 0u32; + while !node.is_null() && seen < 64 { + seen += 1; + // SAFETY: `node` is a `FirstUnicastAddress` link from the live adapter buffer. `Next` + // stays inside that buffer or is null. + let current = unsafe { &*node }; + if let Some(text) = ipv4_text_from_socket_address(¤t.Address) + && !parts.contains(&text) + { + parts.push(text); + } + node = current.Next; + } + if parts.is_empty() { + "-".to_string() + } else { + parts.join(", ") + } +} + +fn ipv4_text_from_socket_address(address: &SOCKET_ADDRESS) -> Option { + let length = address.iSockaddrLength; + let sockaddr = address.lpSockaddr; + if sockaddr.is_null() || length < 8 { + return None; + } + // SAFETY: `lpSockaddr` and `iSockaddrLength` describe one sockaddr in the adapter buffer. + // Only `sa_family` is read here; address bytes are accepted later, and only for AF_INET. + let family = unsafe { (*sockaddr).sa_family }; + let bytes = unsafe { slice::from_raw_parts(sockaddr.cast::(), length as usize) }; + ipv4_octets_from_sockaddr(family, length, bytes).map(format_ipv4_octets) +} + +fn ipv4_octets_from_sockaddr(family: u16, length: i32, bytes: &[u8]) -> Option<[u8; 4]> { + // SOCKADDR_IN is family:u16, port:u16, then four address octets in network order. + if family != AF_INET || length < 8 || bytes.len() < 8 { + return None; + } + let octets = [bytes[4], bytes[5], bytes[6], bytes[7]]; + if octets == [0, 0, 0, 0] { + return None; + } + Some(octets) +} + +fn format_ipv4_octets(octets: [u8; 4]) -> String { + format!("{}.{}.{}.{}", octets[0], octets[1], octets[2], octets[3]) +} + fn collapse_raw_adapters(adapters: Vec) -> Vec { let mut interface_to_index = HashMap::::with_capacity(adapters.len()); let mut collapsed = Vec::::with_capacity(adapters.len()); @@ -1854,6 +2006,7 @@ mod tests { bytes_sent: "10".to_string(), bytes_received: "20".to_string(), bytes_total: "30".to_string(), + ipv4: "192.168.0.2".to_string(), current_sent: 10, current_received: 20, sent_history: HistoryBuffer::zeroed(3), @@ -1879,7 +2032,51 @@ mod tests { #[test] fn renamed_adapter_updates_the_primary_list_column() { let adapter = test_adapter("Renamed Ethernet"); - assert_eq!(adapter_row_texts(&adapter)[0], "Renamed Ethernet"); + let columns = adapter_row_texts(&adapter); + assert_eq!(columns[0], "Renamed Ethernet"); + assert_eq!(columns[4], "192.168.0.2"); + } + + fn ipv4_text_from_sockaddr_list(nodes: &[(u16, &[u8])]) -> String { + let mut parts = Vec::new(); + for &(family, bytes) in nodes { + if let Some(octets) = ipv4_octets_from_sockaddr(family, bytes.len() as i32, bytes) { + let text = format_ipv4_octets(octets); + if !parts.contains(&text) { + parts.push(text); + } + } + } + if parts.is_empty() { + "-".to_string() + } else { + parts.join(", ") + } + } + + #[test] + fn unicast_address_text_keeps_only_af_inet() { + let ipv4 = [2, 0, 0, 0, 192, 168, 1, 10]; + let other_ipv4 = [2, 0, 0, 0, 10, 1, 2, 3]; + let ipv6 = [23, 0, 0, 0, 0x20, 0x01, 0x0d, 0xb8]; + let unspecified = [2, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!( + ipv4_text_from_sockaddr_list(&[ + (23, &ipv6), + (2, &unspecified), + (2, &ipv4), + (2, &ipv4), + (2, &other_ipv4), + ]), + "192.168.1.10, 10.1.2.3" + ); + assert_eq!(ipv4_text_from_sockaddr_list(&[(23, &ipv6)]), "-"); + assert_eq!( + ipv4_octets_from_sockaddr(2, 16, &ipv4), + Some([192, 168, 1, 10]) + ); + assert_eq!(ipv4_octets_from_sockaddr(23, 28, &ipv6), None); + assert_eq!(ipv4_octets_from_sockaddr(2, 4, &ipv4), None); } #[test] diff --git a/src/ui/localization/terms.rs b/src/ui/localization/terms.rs index deac608..d630c21 100644 --- a/src/ui/localization/terms.rs +++ b/src/ui/localization/terms.rs @@ -36,13 +36,15 @@ pub fn service_column_titles() -> [&'static str; 5] { ] } -pub fn network_column_titles() -> [&'static str; 7] { +pub fn network_column_titles() -> [&'static str; 8] { // 网络页列标题会被列表头和相关对话框共同复用。 + // 前五项是列表列:适配器、利用率、链路速度、状态、IPv4。字节计数接在后面。 [ text(TextKey::Adapter), text(TextKey::NetworkUtilization), text(TextKey::LinkSpeed), text(TextKey::State), + text(TextKey::Ipv4Address), text(TextKey::BytesSent), text(TextKey::BytesReceived), text(TextKey::BytesTotal), diff --git a/src/ui/localization/text_key.rs b/src/ui/localization/text_key.rs index 620f5fa..43fadbf 100644 --- a/src/ui/localization/text_key.rs +++ b/src/ui/localization/text_key.rs @@ -277,6 +277,7 @@ pub enum TextKey { NetworkUtilization, LinkSpeed, State, + Ipv4Address, BytesSent, BytesReceived, BytesTotal,