Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nifa"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
authors = ["shellrow <shellrow@foctal.com>"]
description = "Cross-platform CLI tool for network information"
Expand Down Expand Up @@ -33,6 +33,12 @@ url = "2.5"
#tracing-subscriber = { version = "0.3", features = ["time", "chrono"] }
#home = { version = "0.5" }

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_System_SystemInformation", "Wdk_System_SystemServices"] }

# The profile that 'dist' will build with
[profile.dist]
inherits = "release"
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[crates-badge]: https://img.shields.io/crates/v/nifa.svg
[crates-url]: https://crates.io/crates/nifa
[license-badge]: https://img.shields.io/crates/l/nifa.svg

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

## Features
Expand All @@ -17,6 +18,29 @@ Cross-platform CLI tool for network information
- **macOS**
- **Windows**

## Installation

### Install prebuilt binaries via shell script

```sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/shellrow/nifa/releases/latest/download/nifa-installer.sh | sh
```

### Install prebuilt binaries via powershell script

```sh
powershell -ExecutionPolicy Bypass -c "irm https://github.com/shellrow/nifa/releases/latest/download/nifa-installer.ps1 | iex"
```

### From Releases
You can download archives of precompiled binaries from the [releases](https://github.com/shellrow/nifa/releases)

### Using Cargo

```sh
cargo install nifa
```

## Usage
```
Usage: nifa [OPTIONS] [COMMAND]
Expand All @@ -38,7 +62,7 @@ Options:
-V, --version Print version
```

See nifa <sub-command> -h for more detail.
See `nifa <sub-command> -h` for more detail.

## Note for Developers
If you are looking for a Rust library for network interface,
Expand Down
4 changes: 3 additions & 1 deletion src/cmd/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ pub async fn show_public_ip_info(cli: &Cli, args: &PublicArgs) -> Result<()> {

let out = build_public_out(v4, v6);

let default_iface_opt = crate::collector::iface::get_default_interface();

match cli.format {
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&out)?),
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&out)?),
_ => print_public_ip_tree(&out),
_ => print_public_ip_tree(&out, default_iface_opt),
}
Ok(())
}
Expand Down
114 changes: 114 additions & 0 deletions src/collector/iface.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
use netdev::Interface;
use netdev::interface::InterfaceType;

/// Common patterns that indicate a VPN/tunnel adapter
const VPN_NAME_PATTERNS: &[&str] = &[
"tun",
"tap",
"wg",
"tailscale",
"zerotier",
"zt",
"openvpn",
"ovpn",
"ipsec",
"utun",
"vpn",
"adapter",
"wan miniport",
"nord",
"expressvpn",
];

pub fn collect_all_interfaces() -> Vec<Interface> {
netdev::get_interfaces()
Expand All @@ -20,3 +40,97 @@ pub fn get_interface_by_name(name: &str) -> Option<Interface> {
}
None
}

#[derive(Debug)]
pub struct VpnHeuristic {
pub is_vpn_like: bool,
#[allow(dead_code)]
pub score: i32,
#[allow(dead_code)]
pub signals: Vec<String>,
}

/// Check if the given interface looks like a VPN interface using simple heuristics.
pub fn detect_vpn_like(default_if: &Interface) -> VpnHeuristic {
let mut score = 0;
let mut sig = Vec::new();

// Check InterfaceType
match default_if.if_type {
InterfaceType::Tunnel | InterfaceType::Ppp | InterfaceType::ProprietaryVirtual => {
score += 4;
sig.push(format!("type={:?}", default_if.if_type));
}
_ => {}
}

// Check name patterns
let name = default_if.name.to_lowercase();
if VPN_NAME_PATTERNS.iter().any(|p| name.contains(p)) {
score += 3;
sig.push(format!("name={}", default_if.name));
}

// Check friendly_name patterns
if let Some(fname) = &default_if.friendly_name {
let fname_lower = fname.to_lowercase();
if VPN_NAME_PATTERNS.iter().any(|p| fname_lower.contains(p)) {
score += 3;
sig.push(format!("friendly_name={}", fname));
}
}

// Check MTU
if let Some(mtu) = default_if.mtu {
if mtu < 1500 {
// Likely VPN MTU
score += if (1410..=1460).contains(&mtu) { 2 } else { 1 };
sig.push(format!("mtu={}", mtu));
}
}

// Check if IPv4 is 10/8 or 100.64/10
let v4_inner_like = default_if.ipv4.iter().any(|n| {
let ip = n.addr();
let oct = ip.octets();
oct[0] == 10 || (oct[0] == 100 && (oct[1] & 0b1100_0000) == 0b0100_0000) // 100.64.0.0/10
});
if v4_inner_like {
score += 2;
sig.push("ipv4=private(10/8 or 100.64/10)".into());
}

// Check if DNS is 100.64/10
let dns_any_100_64 = default_if.dns_servers.iter().any(|ip| {
if let std::net::IpAddr::V4(v4) = ip {
let o = v4.octets();
o[0] == 100 && (o[1] & 0b1100_0000) == 0b0100_0000
} else {
false
}
});
if dns_any_100_64 {
score += 1;
sig.push("dns=100.64.0.0/10".into());
}

// Check if the type is clearly not physical
match default_if.if_type {
InterfaceType::Ethernet
| InterfaceType::Wireless80211
| InterfaceType::GigabitEthernet
| InterfaceType::FastEthernetT
| InterfaceType::FastEthernetFx => {}
_ => {
score += 1;
sig.push(format!("type-other={:?}", default_if.if_type));
}
}

let is_vpn_like = score >= 5;
VpnHeuristic {
is_vpn_like,
score,
signals: sig,
}
}
58 changes: 58 additions & 0 deletions src/collector/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub struct SysInfo {
pub hostname: String,
pub os_type: String,
pub os_version: String,
pub kernel_version: Option<String>,
pub edition: String,
pub codename: String,
pub bitness: String,
Expand Down Expand Up @@ -50,10 +51,13 @@ pub fn system_info() -> SysInfo {

let proxy = collect_proxy_env();

let kernel_version = kernel_version();

SysInfo {
hostname,
os_type,
os_version,
kernel_version,
edition,
codename,
bitness,
Expand All @@ -79,3 +83,57 @@ pub fn collect_proxy_env() -> ProxyEnv {
no_proxy: pick("no_proxy"),
}
}

#[cfg(target_os = "linux")]
/// Linux-specific: get kernel version from /proc/version
fn kernel_version() -> Option<String> {
if let Ok(contents) = std::fs::read_to_string("/proc/version") {
let parts: Vec<&str> = contents.split_whitespace().collect();
if parts.len() >= 3 {
return Some(format!("{} {} {}", parts[0], parts[1], parts[2]));
}
}
None
}

#[cfg(target_os = "macos")]
/// macOS-specific: get kernel version using `uname`
fn kernel_version() -> Option<String> {
use libc::utsname;
use std::ffi::CStr;
unsafe {
let mut uts: utsname = std::mem::zeroed();
if libc::uname(&mut uts) == 0 {
let ver = CStr::from_ptr(uts.version.as_ptr()).to_string_lossy();
let ver_short = ver.split(':').next().unwrap_or(&ver);
return Some(ver_short.trim().to_string());
}
}
return None;
}

#[cfg(target_os = "windows")]
/// Windows-specific: get kernel version using `RtlGetVersion`
fn kernel_version() -> Option<String> {
use windows_sys::Wdk::System::SystemServices::RtlGetVersion;
use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW;
unsafe {
let mut info = OSVERSIONINFOW {
dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
..std::mem::zeroed()
};
let status = RtlGetVersion(&mut info as *mut _ as *mut _);
if status == 0 {
let major = info.dwMajorVersion;
let minor = info.dwMinorVersion;
let build = info.dwBuildNumber;
return Some(format!("Windows NT Kernel {major}.{minor}.{build}"));
}
}
return None;
}

#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn kernel_version() -> Option<String> {
None
}
Loading