Skip to content

Commit b57bb91

Browse files
fix: properly parse ip addr show output for network interfaces (#27)
The previous implementation was a stub that always returned eth0 with placeholder IP 192.168.1.100. This fix properly parses: - Interface names from 'X: ifname:' lines - MAC addresses from 'link/ether' lines - Handles Tailscale tunnel interfaces with 'link/none' - IPv4 addresses from 'inet' lines - Correctly classifies interface types (tailscale, wireless, bridge, etc) This enables proper detection of Tailscale VPN interfaces for NetBox network_access_ip field population.
1 parent bb128da commit b57bb91

1 file changed

Lines changed: 86 additions & 16 deletions

File tree

src/domain/parsers/network.rs

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,96 @@ limitations under the License.
1818
1919
use crate::domain::NetworkInterface;
2020

21-
/// Parse network interfaces from ip command output
21+
/// Parse network interfaces from `ip addr show` command output
2222
pub fn parse_ip_output(ip_output: &str) -> Result<Vec<NetworkInterface>, String> {
2323
let mut interfaces = Vec::new();
24+
let mut current_interface: Option<NetworkInterface> = None;
2425

25-
// Simplified parsing - real implementation would be more comprehensive
2626
for line in ip_output.lines() {
27-
if line.contains("eth") || line.contains("ens") {
28-
interfaces.push(NetworkInterface {
29-
name: "eth0".to_string(),
30-
mac: "00:00:00:00:00:00".to_string(),
31-
ip: "192.168.1.100".to_string(),
32-
prefix: "24".to_string(),
33-
speed: Some("1000 Mbps".to_string()),
34-
type_: "Ethernet".to_string(),
35-
vendor: "Unknown".to_string(),
36-
model: "Unknown".to_string(),
37-
pci_id: "Unknown".to_string(),
38-
numa_node: None,
39-
..Default::default()
40-
});
27+
let trimmed = line.trim();
28+
29+
// Interface line starts with a number followed by colon
30+
// Example: "2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ..."
31+
if !line.starts_with(' ') && line.contains(": ") {
32+
// Save previous interface if it exists (and has a name)
33+
if let Some(interface) = current_interface.take() {
34+
// Skip loopback
35+
if interface.name != "lo" {
36+
interfaces.push(interface);
37+
}
38+
}
39+
40+
// Extract interface name: "2: eth0:" -> "eth0"
41+
let parts: Vec<&str> = line.splitn(3, ':').collect();
42+
if parts.len() >= 2 {
43+
let name = parts[1].trim().to_string();
44+
// Skip loopback interface
45+
if name == "lo" {
46+
continue;
47+
}
48+
49+
// Determine interface type based on name
50+
let interface_type = if name.starts_with("tailscale") {
51+
"tailscale".to_string()
52+
} else if name.starts_with("wl") {
53+
"wireless".to_string()
54+
} else if name.starts_with("docker") || name.starts_with("br-") {
55+
"bridge".to_string()
56+
} else if name.starts_with("veth") {
57+
"veth".to_string()
58+
} else {
59+
"ethernet".to_string()
60+
};
61+
62+
current_interface = Some(NetworkInterface {
63+
name,
64+
mac: String::new(),
65+
ip: String::new(),
66+
prefix: String::new(),
67+
speed: None,
68+
type_: interface_type,
69+
vendor: "Unknown".to_string(),
70+
model: "Unknown".to_string(),
71+
pci_id: "Unknown".to_string(),
72+
numa_node: None,
73+
..Default::default()
74+
});
75+
}
76+
} else if let Some(ref mut interface) = current_interface {
77+
// Parse interface details (indented lines)
78+
79+
// MAC address line: " link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff"
80+
if trimmed.starts_with("link/ether ") {
81+
if let Some(mac) = trimmed.split_whitespace().nth(1) {
82+
interface.mac = mac.to_string();
83+
}
84+
}
85+
// Point-to-point interfaces (like Tailscale) have "link/none"
86+
else if trimmed.starts_with("link/none") {
87+
interface.mac = "00:00:00:00:00:00".to_string();
88+
}
89+
// IPv4 address line: " inet 192.168.1.100/24 ..."
90+
else if trimmed.starts_with("inet ") {
91+
if let Some(addr_with_prefix) = trimmed.split_whitespace().nth(1) {
92+
let parts: Vec<&str> = addr_with_prefix.split('/').collect();
93+
if !parts.is_empty() {
94+
// Only set IP if not already set (first IPv4 address wins)
95+
if interface.ip.is_empty() {
96+
interface.ip = parts[0].to_string();
97+
if parts.len() > 1 {
98+
interface.prefix = parts[1].to_string();
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
105+
}
106+
107+
// Don't forget the last interface
108+
if let Some(interface) = current_interface {
109+
if interface.name != "lo" {
110+
interfaces.push(interface);
41111
}
42112
}
43113

0 commit comments

Comments
 (0)