use std::net::{TcpListener, UdpSocket};
use tracing::{debug, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DeviceClass {
Bluetooth,
Usb,
Nfc,
}
impl DeviceClass {
pub fn as_str(&self) -> &'static str {
match self {
Self::Bluetooth => "Bluetooth adapter",
Self::Usb => "USB device",
Self::Nfc => "NFC reader",
}
}
pub fn access_hint(&self) -> &'static str {
match self {
Self::Bluetooth => {
"Linux: install/start BlueZ and ensure a powered adapter exists \
(`bluetoothctl list`). macOS: grant Bluetooth permission in \
System Settings > Privacy & Security > Bluetooth."
}
Self::Usb => {
"Linux: add a udev rule granting your user access to the device, or \
join the group owning /dev/bus/usb. macOS: no elevation is normally \
needed."
}
Self::Nfc => {
"Linux: install and start pcscd (`systemctl start pcscd`). macOS: the \
PCSC framework is built in."
}
}
}
}
#[derive(Debug, Clone)]
pub struct SystemCapabilities {
pub can_bind_privileged_ports: bool,
pub has_raw_socket_access: bool,
pub has_packet_capture_access: bool,
pub has_bluetooth_access: bool,
pub has_usb_access: bool,
pub has_nfc_access: bool,
pub is_root: bool,
}
impl SystemCapabilities {
pub fn detect() -> Self {
#[cfg(target_arch = "wasm32")]
{
return Self {
can_bind_privileged_ports: true,
has_raw_socket_access: false,
has_packet_capture_access: false,
has_bluetooth_access: false,
has_usb_access: false,
has_nfc_access: false,
is_root: false,
};
}
#[cfg(not(target_arch = "wasm32"))]
Self::probe()
}
#[cfg(not(target_arch = "wasm32"))]
fn probe() -> Self {
let is_root = is_running_as_root();
let can_bind_privileged_ports = can_bind_privileged_port();
let has_raw_socket_access = has_raw_ip_socket_capability();
let has_packet_capture_access = has_packet_capture_capability();
let has_bluetooth_access = has_bluetooth_capability();
let has_usb_access = has_usb_capability();
let has_nfc_access = has_nfc_capability();
debug!(
"Detected capabilities: root={}, privileged_ports={}, raw_ip_sockets={}, \
packet_capture={}, bluetooth={}, usb={}, nfc={}",
is_root,
can_bind_privileged_ports,
has_raw_socket_access,
has_packet_capture_access,
has_bluetooth_access,
has_usb_access,
has_nfc_access
);
Self {
can_bind_privileged_ports,
has_raw_socket_access,
has_packet_capture_access,
has_bluetooth_access,
has_usb_access,
has_nfc_access,
is_root,
}
}
pub fn has_device_access(&self, class: DeviceClass) -> bool {
match class {
DeviceClass::Bluetooth => self.has_bluetooth_access,
DeviceClass::Usb => self.has_usb_access,
DeviceClass::Nfc => self.has_nfc_access,
}
}
pub fn description(&self) -> String {
let mut parts = Vec::new();
if self.is_root {
parts.push("running as root/admin");
}
parts.push(if self.can_bind_privileged_ports {
"privileged ports available"
} else {
"privileged ports unavailable"
});
parts.push(if self.has_raw_socket_access {
"raw IP sockets available"
} else {
"raw IP sockets unavailable"
});
parts.push(if self.has_packet_capture_access {
"packet capture available"
} else {
"packet capture unavailable"
});
if !self.has_bluetooth_access {
parts.push("no Bluetooth adapter");
}
if !self.has_usb_access {
parts.push("no USB access");
}
if !self.has_nfc_access {
parts.push("no NFC reader stack");
}
parts.join(", ")
}
}
fn is_running_as_root() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(windows)]
{
false
}
#[cfg(not(any(unix, windows)))]
{
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortProbe {
Bound,
Denied,
Inconclusive,
}
pub fn classify_bind_error(err: &std::io::Error) -> PortProbe {
match err.kind() {
std::io::ErrorKind::PermissionDenied => PortProbe::Denied,
std::io::ErrorKind::AddrInUse | std::io::ErrorKind::AddrNotAvailable => {
PortProbe::Inconclusive
}
_ => PortProbe::Inconclusive,
}
}
pub fn privileged_port_capability(probes: &[PortProbe]) -> bool {
if probes.contains(&PortProbe::Bound) {
return true;
}
if probes.contains(&PortProbe::Denied) {
return false;
}
true
}
fn can_bind_privileged_port() -> bool {
if is_running_as_root() {
return true;
}
let test_ports = [80, 67, 123, 53];
let mut probes = Vec::with_capacity(test_ports.len() * 2);
for port in test_ports {
let addr = format!("127.0.0.1:{}", port);
match TcpListener::bind(&addr) {
Ok(listener) => {
drop(listener);
debug!("Successfully test-bound to privileged TCP port {}", port);
return true;
}
Err(e) => {
let outcome = classify_bind_error(&e);
debug!(
"TCP bind to privileged port {} failed: {} ({:?})",
port, e, outcome
);
probes.push(outcome);
}
}
match UdpSocket::bind(&addr) {
Ok(socket) => {
drop(socket);
debug!("Successfully test-bound to privileged UDP port {}", port);
return true;
}
Err(e) => {
let outcome = classify_bind_error(&e);
debug!(
"UDP bind to privileged port {} failed: {} ({:?})",
port, e, outcome
);
probes.push(outcome);
}
}
}
if TcpListener::bind("127.0.0.1:0").is_err() {
warn!("Cannot bind to any ports - networking may be broken");
}
let capable = privileged_port_capability(&probes);
if capable {
warn!(
"Every privileged test port ({:?}) was unavailable for reasons other than \
permission - assuming privileged ports are bindable. A server on a port \
below 1024 will report the real error if this assumption is wrong.",
test_ports
);
} else {
debug!("Cannot bind to privileged ports - requires elevated privileges");
}
capable
}
fn has_raw_ip_socket_capability() -> bool {
#[cfg(unix)]
{
if is_running_as_root() {
return true;
}
let raw_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_RAW, libc::IPPROTO_ICMP) };
if raw_fd >= 0 {
unsafe { libc::close(raw_fd) };
debug!("Opened a raw ICMP socket - raw IP socket access available");
return true;
}
debug!(
"Raw ICMP socket failed ({}) and not root - raw-socket protocols \
(ICMP, IGMP, OSPF) will be refused at startup.",
std::io::Error::last_os_error()
);
false
}
#[cfg(windows)]
{
is_running_as_root()
}
#[cfg(not(any(unix, windows)))]
{
false
}
}
fn has_packet_capture_capability() -> bool {
#[cfg(unix)]
{
if is_running_as_root() {
return true;
}
#[cfg(target_os = "linux")]
{
let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, 0) };
if fd >= 0 {
unsafe { libc::close(fd) };
debug!("Opened an AF_PACKET socket - L2 capture available");
return true;
}
}
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
{
let mut denied = false;
for n in 0..256 {
match std::fs::File::open(format!("/dev/bpf{}", n)) {
Ok(_) => {
debug!("Opened /dev/bpf{} - L2 capture available without root", n);
return true;
}
Err(e) => match e.raw_os_error() {
Some(libc::EBUSY) => {
debug!("/dev/bpf{} is busy - capture access is present", n);
return true;
}
Some(libc::EACCES) | Some(libc::EPERM) => {
denied = true;
continue;
}
Some(libc::ENOENT) | Some(libc::ENXIO) => break,
_ => continue,
},
}
}
if denied {
debug!("every /dev/bpf* refused permission - no L2 capture access");
}
}
debug!(
"No capture handle available ({}) and not root - capture protocols \
(ARP, DataLink, IS-IS) will be refused at startup.",
std::io::Error::last_os_error()
);
false
}
#[cfg(windows)]
{
is_running_as_root()
}
#[cfg(not(any(unix, windows)))]
{
false
}
}
#[cfg(target_os = "linux")]
fn any_path_exists(paths: &[&str]) -> bool {
paths.iter().any(|p| std::path::Path::new(p).exists())
}
fn has_bluetooth_capability() -> bool {
#[cfg(target_os = "linux")]
{
let bus = any_path_exists(&[
"/run/dbus/system_bus_socket",
"/var/run/dbus/system_bus_socket",
]);
let adapter = std::fs::read_dir("/sys/class/bluetooth")
.map(|mut d| d.next().is_some())
.unwrap_or(false);
if !bus {
debug!("No D-Bus system bus socket - BlueZ is unreachable");
} else if !adapter {
debug!("No adapter under /sys/class/bluetooth");
}
bus && adapter
}
#[cfg(not(target_os = "linux"))]
{
true
}
}
fn has_usb_capability() -> bool {
#[cfg(target_os = "linux")]
{
let present = std::path::Path::new("/dev/bus/usb").is_dir();
if !present {
debug!("/dev/bus/usb is absent - usbfs is not mounted");
}
present
}
#[cfg(not(target_os = "linux"))]
{
true
}
}
fn has_nfc_capability() -> bool {
#[cfg(target_os = "linux")]
{
let present = any_path_exists(&["/run/pcscd/pcscd.comm", "/var/run/pcscd/pcscd.comm"]);
if !present {
debug!("No pcscd socket - the PC/SC daemon is not running");
}
present
}
#[cfg(not(target_os = "linux"))]
{
true
}
}