Skip to content

Commit 0bcd35e

Browse files
authored
Merge pull request #2 from shellrow/dev
Release v0.3.0
2 parents 9c10f77 + c2833fa commit 0bcd35e

7 files changed

Lines changed: 358 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "nifa"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2024"
55
authors = ["shellrow <shellrow@foctal.com>"]
66
description = "Cross-platform CLI tool for network information"
@@ -33,6 +33,12 @@ url = "2.5"
3333
#tracing-subscriber = { version = "0.3", features = ["time", "chrono"] }
3434
#home = { version = "0.5" }
3535

36+
[target.'cfg(unix)'.dependencies]
37+
libc = "0.2"
38+
39+
[target.'cfg(target_os = "windows")'.dependencies]
40+
windows-sys = { version = "0.59", features = ["Win32_System_SystemInformation", "Wdk_System_SystemServices"] }
41+
3642
# The profile that 'dist' will build with
3743
[profile.dist]
3844
inherits = "release"

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[crates-badge]: https://img.shields.io/crates/v/nifa.svg
22
[crates-url]: https://crates.io/crates/nifa
3+
[license-badge]: https://img.shields.io/crates/l/nifa.svg
34

4-
# nifa [![Crates.io][crates-badge]][crates-url]
5+
# nifa [![Crates.io][crates-badge]][crates-url] ![License][license-badge]
56
Cross-platform CLI tool for network information
67

78
## Features
@@ -17,6 +18,29 @@ Cross-platform CLI tool for network information
1718
- **macOS**
1819
- **Windows**
1920

21+
## Installation
22+
23+
### Install prebuilt binaries via shell script
24+
25+
```sh
26+
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/shellrow/nifa/releases/latest/download/nifa-installer.sh | sh
27+
```
28+
29+
### Install prebuilt binaries via powershell script
30+
31+
```sh
32+
powershell -ExecutionPolicy Bypass -c "irm https://github.com/shellrow/nifa/releases/latest/download/nifa-installer.ps1 | iex"
33+
```
34+
35+
### From Releases
36+
You can download archives of precompiled binaries from the [releases](https://github.com/shellrow/nifa/releases)
37+
38+
### Using Cargo
39+
40+
```sh
41+
cargo install nifa
42+
```
43+
2044
## Usage
2145
```
2246
Usage: nifa [OPTIONS] [COMMAND]
@@ -38,7 +62,7 @@ Options:
3862
-V, --version Print version
3963
```
4064

41-
See nifa <sub-command> -h for more detail.
65+
See `nifa <sub-command> -h` for more detail.
4266

4367
## Note for Developers
4468
If you are looking for a Rust library for network interface,

src/cmd/public.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ pub async fn show_public_ip_info(cli: &Cli, args: &PublicArgs) -> Result<()> {
4848

4949
let out = build_public_out(v4, v6);
5050

51+
let default_iface_opt = crate::collector::iface::get_default_interface();
52+
5153
match cli.format {
5254
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&out)?),
5355
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&out)?),
54-
_ => print_public_ip_tree(&out),
56+
_ => print_public_ip_tree(&out, default_iface_opt),
5557
}
5658
Ok(())
5759
}

src/collector/iface.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,24 @@
11
use netdev::Interface;
2+
use netdev::interface::InterfaceType;
3+
4+
/// Common patterns that indicate a VPN/tunnel adapter
5+
const VPN_NAME_PATTERNS: &[&str] = &[
6+
"tun",
7+
"tap",
8+
"wg",
9+
"tailscale",
10+
"zerotier",
11+
"zt",
12+
"openvpn",
13+
"ovpn",
14+
"ipsec",
15+
"utun",
16+
"vpn",
17+
"adapter",
18+
"wan miniport",
19+
"nord",
20+
"expressvpn",
21+
];
222

323
pub fn collect_all_interfaces() -> Vec<Interface> {
424
netdev::get_interfaces()
@@ -20,3 +40,97 @@ pub fn get_interface_by_name(name: &str) -> Option<Interface> {
2040
}
2141
None
2242
}
43+
44+
#[derive(Debug)]
45+
pub struct VpnHeuristic {
46+
pub is_vpn_like: bool,
47+
#[allow(dead_code)]
48+
pub score: i32,
49+
#[allow(dead_code)]
50+
pub signals: Vec<String>,
51+
}
52+
53+
/// Check if the given interface looks like a VPN interface using simple heuristics.
54+
pub fn detect_vpn_like(default_if: &Interface) -> VpnHeuristic {
55+
let mut score = 0;
56+
let mut sig = Vec::new();
57+
58+
// Check InterfaceType
59+
match default_if.if_type {
60+
InterfaceType::Tunnel | InterfaceType::Ppp | InterfaceType::ProprietaryVirtual => {
61+
score += 4;
62+
sig.push(format!("type={:?}", default_if.if_type));
63+
}
64+
_ => {}
65+
}
66+
67+
// Check name patterns
68+
let name = default_if.name.to_lowercase();
69+
if VPN_NAME_PATTERNS.iter().any(|p| name.contains(p)) {
70+
score += 3;
71+
sig.push(format!("name={}", default_if.name));
72+
}
73+
74+
// Check friendly_name patterns
75+
if let Some(fname) = &default_if.friendly_name {
76+
let fname_lower = fname.to_lowercase();
77+
if VPN_NAME_PATTERNS.iter().any(|p| fname_lower.contains(p)) {
78+
score += 3;
79+
sig.push(format!("friendly_name={}", fname));
80+
}
81+
}
82+
83+
// Check MTU
84+
if let Some(mtu) = default_if.mtu {
85+
if mtu < 1500 {
86+
// Likely VPN MTU
87+
score += if (1410..=1460).contains(&mtu) { 2 } else { 1 };
88+
sig.push(format!("mtu={}", mtu));
89+
}
90+
}
91+
92+
// Check if IPv4 is 10/8 or 100.64/10
93+
let v4_inner_like = default_if.ipv4.iter().any(|n| {
94+
let ip = n.addr();
95+
let oct = ip.octets();
96+
oct[0] == 10 || (oct[0] == 100 && (oct[1] & 0b1100_0000) == 0b0100_0000) // 100.64.0.0/10
97+
});
98+
if v4_inner_like {
99+
score += 2;
100+
sig.push("ipv4=private(10/8 or 100.64/10)".into());
101+
}
102+
103+
// Check if DNS is 100.64/10
104+
let dns_any_100_64 = default_if.dns_servers.iter().any(|ip| {
105+
if let std::net::IpAddr::V4(v4) = ip {
106+
let o = v4.octets();
107+
o[0] == 100 && (o[1] & 0b1100_0000) == 0b0100_0000
108+
} else {
109+
false
110+
}
111+
});
112+
if dns_any_100_64 {
113+
score += 1;
114+
sig.push("dns=100.64.0.0/10".into());
115+
}
116+
117+
// Check if the type is clearly not physical
118+
match default_if.if_type {
119+
InterfaceType::Ethernet
120+
| InterfaceType::Wireless80211
121+
| InterfaceType::GigabitEthernet
122+
| InterfaceType::FastEthernetT
123+
| InterfaceType::FastEthernetFx => {}
124+
_ => {
125+
score += 1;
126+
sig.push(format!("type-other={:?}", default_if.if_type));
127+
}
128+
}
129+
130+
let is_vpn_like = score >= 5;
131+
VpnHeuristic {
132+
is_vpn_like,
133+
score,
134+
signals: sig,
135+
}
136+
}

src/collector/sys.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub struct SysInfo {
55
pub hostname: String,
66
pub os_type: String,
77
pub os_version: String,
8+
pub kernel_version: Option<String>,
89
pub edition: String,
910
pub codename: String,
1011
pub bitness: String,
@@ -50,10 +51,13 @@ pub fn system_info() -> SysInfo {
5051

5152
let proxy = collect_proxy_env();
5253

54+
let kernel_version = kernel_version();
55+
5356
SysInfo {
5457
hostname,
5558
os_type,
5659
os_version,
60+
kernel_version,
5761
edition,
5862
codename,
5963
bitness,
@@ -79,3 +83,57 @@ pub fn collect_proxy_env() -> ProxyEnv {
7983
no_proxy: pick("no_proxy"),
8084
}
8185
}
86+
87+
#[cfg(target_os = "linux")]
88+
/// Linux-specific: get kernel version from /proc/version
89+
fn kernel_version() -> Option<String> {
90+
if let Ok(contents) = std::fs::read_to_string("/proc/version") {
91+
let parts: Vec<&str> = contents.split_whitespace().collect();
92+
if parts.len() >= 3 {
93+
return Some(format!("{} {} {}", parts[0], parts[1], parts[2]));
94+
}
95+
}
96+
None
97+
}
98+
99+
#[cfg(target_os = "macos")]
100+
/// macOS-specific: get kernel version using `uname`
101+
fn kernel_version() -> Option<String> {
102+
use libc::utsname;
103+
use std::ffi::CStr;
104+
unsafe {
105+
let mut uts: utsname = std::mem::zeroed();
106+
if libc::uname(&mut uts) == 0 {
107+
let ver = CStr::from_ptr(uts.version.as_ptr()).to_string_lossy();
108+
let ver_short = ver.split(':').next().unwrap_or(&ver);
109+
return Some(ver_short.trim().to_string());
110+
}
111+
}
112+
return None;
113+
}
114+
115+
#[cfg(target_os = "windows")]
116+
/// Windows-specific: get kernel version using `RtlGetVersion`
117+
fn kernel_version() -> Option<String> {
118+
use windows_sys::Wdk::System::SystemServices::RtlGetVersion;
119+
use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW;
120+
unsafe {
121+
let mut info = OSVERSIONINFOW {
122+
dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
123+
..std::mem::zeroed()
124+
};
125+
let status = RtlGetVersion(&mut info as *mut _ as *mut _);
126+
if status == 0 {
127+
let major = info.dwMajorVersion;
128+
let minor = info.dwMinorVersion;
129+
let build = info.dwBuildNumber;
130+
return Some(format!("Windows NT Kernel {major}.{minor}.{build}"));
131+
}
132+
}
133+
return None;
134+
}
135+
136+
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
137+
fn kernel_version() -> Option<String> {
138+
None
139+
}

0 commit comments

Comments
 (0)