use std::{
collections::HashMap,
fs::File,
io::{BufRead, BufReader},
path::Path,
sync::LazyLock,
};
static SVC: LazyLock<HashMap<(u16, &'static str), String>> = LazyLock::new(load_services);
#[inline]
fn normalize_proto(proto: &str) -> &'static str {
if proto.eq_ignore_ascii_case("udp") {
"udp"
} else {
"tcp"
}
}
fn load_services() -> HashMap<(u16, &'static str), String> {
let mut map = HashMap::new();
let candidates = ["/etc/services"];
for p in candidates {
if Path::new(p).exists() {
if let Ok(f) = File::open(p) {
let r = BufReader::new(f);
for line in r.lines().map_while(Result::ok) {
let s = line.trim();
if s.is_empty() || s.starts_with('#') {
continue;
}
let mut it = s.split_whitespace();
let name = match it.next() {
Some(x) => x,
None => continue,
};
let port_proto = match it.next() {
Some(x) => x,
None => continue,
};
if let Some((port_s, proto)) = port_proto.split_once('/') {
if let Ok(port) = port_s.parse::<u16>() {
let proto = normalize_proto(proto);
map.entry((port, proto)).or_insert_with(|| name.to_string());
}
}
}
}
}
}
map
}
#[inline]
fn svc_from_file(port: u16, proto: &str) -> Option<String> {
let key = (port, normalize_proto(proto));
SVC.get(&key).cloned()
}
#[cfg(unix)]
fn svc_from_libc(port: u16, proto: &str) -> Option<String> {
use libc::{endservent, getservbyport, setservent};
use std::{
ffi::{CStr, CString},
ptr,
};
unsafe { setservent(1) }
let be = (port as i32).to_be();
let proto_c = CString::new(normalize_proto(proto)).ok();
let se_ptr = unsafe {
let with_proto = proto_c
.as_ref()
.map(|c| getservbyport(be, c.as_ptr()))
.unwrap_or(ptr::null_mut());
if !with_proto.is_null() {
with_proto
} else {
getservbyport(be, ptr::null())
}
};
let out = if se_ptr.is_null() {
None
} else {
Some(
unsafe { CStr::from_ptr((*se_ptr).s_name) }
.to_string_lossy()
.into_owned(),
)
};
unsafe { endservent() }
out
}
#[cfg(not(unix))]
fn svc_from_libc(_port: u16, _proto: &str) -> Option<String> {
None
}
fn service_name(port: u16, proto: &str) -> Option<String> {
svc_from_file(port, proto).or_else(|| svc_from_libc(port, proto))
}
fn is_ephemeral(port: u16) -> bool {
(49152..=65535).contains(&port)
}
pub fn get_port_annotation(port_str: &str, proto: &str) -> Option<String> {
let Ok(port) = port_str.parse::<u16>() else {
return None;
};
if port == 0 {
return None;
}
if is_ephemeral(port) {
return Some("ephemeral".to_string());
}
service_name(port, proto)
}
#[cfg(test)]
mod tests {
use super::{get_port_annotation, normalize_proto};
#[test]
fn non_numeric_returns_none() {
assert_eq!(get_port_annotation("-", "tcp"), None);
}
#[test]
fn port_zero_returns_none() {
assert_eq!(get_port_annotation("0", "tcp"), None);
}
#[test]
fn annotates_service_name() {
assert_eq!(get_port_annotation("443", "tcp"), Some("https".to_string()));
}
#[test]
fn annotates_service_name_invalid_proto() {
assert_eq!(
get_port_annotation("22", "notaproto"),
Some("ssh".to_string())
);
}
#[test]
fn marks_ephemeral_range() {
assert_eq!(
get_port_annotation("59345", "tcp"),
Some("ephemeral".to_string())
);
}
#[test]
fn out_of_ephemeral_range_returns_none() {
assert_eq!(get_port_annotation("1000000", "tcp"), None);
}
#[test]
fn normalize_uppercase_proto() {
assert_eq!(normalize_proto("UDp"), "udp");
}
#[test]
fn normalize_non_existant_proto_to_tcp() {
assert_eq!(normalize_proto("notaproto"), "tcp");
}
}