use std::net::SocketAddr;
use miette::{Result, miette};
const ROOT_UID: u32 = 0;
#[derive(Debug, Clone, Copy, Default)]
pub struct Permitted {
pub uid: Option<u32>,
}
impl Permitted {
pub fn new(uid: Option<u32>) -> Self {
Self { uid }
}
fn admits(&self, uid: u32) -> bool {
uid == ROOT_UID || self.uid == Some(uid)
}
}
pub fn resolve_user(spec: &str) -> Result<u32> {
let spec = spec.trim();
if let Ok(uid) = spec.parse::<u32>() {
return Ok(uid);
}
let passwd = std::fs::read_to_string("/etc/passwd")
.map_err(|err| miette!("could not read /etc/passwd to resolve {spec:?}: {err}"))?;
passwd
.lines()
.filter_map(|line| {
let mut fields = line.split(':');
let name = fields.next()?;
let uid = fields.nth(1)?.parse::<u32>().ok()?;
(name == spec).then_some(uid)
})
.next()
.ok_or_else(|| miette!("no user {spec:?} on this host; give a numeric uid instead"))
}
#[cfg(target_os = "linux")]
pub async fn caller_permitted(
permitted: Permitted,
peer: SocketAddr,
local: SocketAddr,
) -> Result<()> {
let uid = tokio::task::spawn_blocking(move || socket_owner(peer, local))
.await
.map_err(|err| miette!("looking up the caller's socket did not complete: {err}"))??;
if permitted.admits(uid) {
Ok(())
} else {
Err(miette!(
"uid {uid} may not fetch a certificate; configure --permit-cert-user if this is the user the front end runs as"
))
}
}
#[cfg(not(target_os = "linux"))]
pub async fn caller_permitted(
_permitted: Permitted,
_peer: SocketAddr,
_local: SocketAddr,
) -> Result<()> {
Err(miette!(
"identifying the caller of the certificate endpoint is not implemented on this platform"
))
}
#[cfg(target_os = "linux")]
fn socket_owner(peer: SocketAddr, local: SocketAddr) -> Result<u32> {
let tables = if peer.is_ipv6() {
["/proc/net/tcp6", "/proc/net/tcp"]
} else {
["/proc/net/tcp", "/proc/net/tcp6"]
};
for table in tables {
let Ok(file) = std::fs::File::open(table) else {
continue;
};
if let Some(uid) = find_owner(std::io::BufReader::new(file), peer, local) {
return Ok(uid);
}
}
Err(miette!(
"could not find the caller's socket ({peer} -> {local}) in the kernel's table"
))
}
const ESTABLISHED: &str = "01";
fn find_owner(
table: impl std::io::BufRead,
want_local: SocketAddr,
want_remote: SocketAddr,
) -> Option<u32> {
let port_tag = format!(":{:04X}", want_local.port());
table
.lines()
.map_while(Result::ok)
.skip(1)
.find_map(|line| owner_of_row(&line, &port_tag, want_local, want_remote))
}
fn owner_of_row(
line: &str,
port_tag: &str,
want_local: SocketAddr,
want_remote: SocketAddr,
) -> Option<u32> {
let mut fields = line.split_whitespace();
let _slot = fields.next()?;
let local_cell = fields.next()?;
if !local_cell.ends_with(port_tag) {
return None;
}
let local = parse_address(local_cell)?;
let remote = parse_address(fields.next()?)?;
if fields.next()? != ESTABLISHED {
return None;
}
let uid = fields.nth(3)?.parse::<u32>().ok()?;
(same_endpoint(local, want_local) && same_endpoint(remote, want_remote)).then_some(uid)
}
fn same_endpoint(a: SocketAddr, b: SocketAddr) -> bool {
fn canonical(addr: SocketAddr) -> SocketAddr {
match addr {
SocketAddr::V6(v6) => match v6.ip().to_ipv4_mapped() {
Some(v4) => SocketAddr::new(v4.into(), addr.port()),
None => addr,
},
v4 => v4,
}
}
canonical(a) == canonical(b)
}
fn parse_address(cell: &str) -> Option<SocketAddr> {
let (addr, port) = cell.rsplit_once(':')?;
let port = u16::from_str_radix(port, 16).ok()?;
let mut bytes = [0u8; 16];
let mut filled = 0;
for chunk in addr.as_bytes().chunks(8) {
if chunk.len() != 8 || filled == bytes.len() {
return None;
}
let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
bytes[filled..filled + 4].copy_from_slice(&word.to_le_bytes());
filled += 4;
}
let ip = match filled {
4 => std::net::IpAddr::from([bytes[0], bytes[1], bytes[2], bytes[3]]),
16 => std::net::IpAddr::from(bytes),
_ => return None,
};
Some(SocketAddr::new(ip, port))
}
#[cfg(test)]
mod tests {
use super::*;
const TCP4: &str = "\
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:C142 0100007F:2047 01 00000000:00000000 00:00000000 00000000 997 0 51231 1 0000 20 0
1: 0100007F:2047 0100007F:C142 01 00000000:00000000 00:00000000 00000000 0 0 51232 1 0000 20 0
2: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4113 1 0000 10 0
";
const TCP4_WITH_REMNANT: &str = "\
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:C142 0100007F:2047 06 00000000:00000000 00:00000000 00000000 0 0 51230 1 0000 20 0
1: 0100007F:C142 0100007F:2047 01 00000000:00000000 00:00000000 00000000 997 0 51231 1 0000 20 0
";
const TCP6: &str = "\
sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000000000000000000001000000:C143 00000000000000000000000001000000:2047 01 00000000:00000000 00:00000000 00000000 1001 0 51999 1 0000 20 0
";
fn addr(s: &str) -> SocketAddr {
s.parse().unwrap()
}
#[test]
fn a_connections_owner_is_read_from_the_kernels_table() {
let uid = find_owner(
TCP4.as_bytes(),
addr("127.0.0.1:49474"),
addr("127.0.0.1:8263"),
);
assert_eq!(uid, Some(997));
}
#[test]
fn a_remnant_on_the_same_four_tuple_does_not_answer_for_the_live_connection() {
let uid = find_owner(
TCP4_WITH_REMNANT.as_bytes(),
addr("127.0.0.1:49474"),
addr("127.0.0.1:8263"),
);
assert_eq!(
uid,
Some(997),
"the live connection's owner, not the remnant's"
);
}
#[test]
fn a_connection_that_is_not_there_names_nobody() {
assert_eq!(
find_owner(
TCP4.as_bytes(),
addr("127.0.0.1:49999"),
addr("127.0.0.1:8263")
),
None
);
assert_eq!(
find_owner(
TCP4.as_bytes(),
addr("127.0.0.1:8080"),
addr("127.0.0.1:8263")
),
None
);
}
#[test]
fn the_v6_table_is_read_too() {
let uid = find_owner(TCP6.as_bytes(), addr("[::1]:49475"), addr("[::1]:8263"));
assert_eq!(uid, Some(1001));
}
#[test]
fn a_v4_mapped_endpoint_matches_its_v4_form() {
assert!(same_endpoint(
addr("[::ffff:127.0.0.1]:8263"),
addr("127.0.0.1:8263")
));
assert!(!same_endpoint(
addr("[::ffff:127.0.0.1]:8263"),
addr("127.0.0.1:8264")
));
}
#[test]
fn the_superuser_is_always_permitted_and_one_further_user_may_be() {
let none = Permitted::new(None);
assert!(none.admits(0));
assert!(!none.admits(997));
let caddy = Permitted::new(Some(997));
assert!(caddy.admits(0));
assert!(caddy.admits(997));
assert!(!caddy.admits(1001));
}
#[test]
fn a_numeric_user_resolves_without_a_lookup() {
assert_eq!(resolve_user("997").unwrap(), 997);
assert_eq!(resolve_user(" 0 ").unwrap(), 0);
}
#[test]
fn an_address_cell_parses_as_host_ordered_hex() {
assert_eq!(parse_address("0100007F:1F90"), Some(addr("127.0.0.1:8080")));
assert_eq!(
parse_address("00000000000000000000000001000000:1F90"),
Some(addr("[::1]:8080"))
);
assert_eq!(parse_address("nonsense"), None);
}
}