use std::net::IpAddr;
use std::path::Path;
pub const FORWARDED_HEADERS: [&str; 4] = [
"x-forwarded-for",
"x-real-ip",
"forwarded",
"cf-connecting-ip",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pairing {
Unpaired,
Paired(String),
Unreadable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Access {
Accept,
Unauthorized,
}
pub fn data_plane_access(
pairing: &Pairing,
caller: CallerClass,
presented_key: Option<&str>,
) -> Access {
match pairing {
Pairing::Unpaired => Access::Accept,
Pairing::Unreadable if caller == CallerClass::OnBox => Access::Accept,
Pairing::Unreadable => Access::Unauthorized,
Pairing::Paired(expected) => {
if caller == CallerClass::OnBox {
return Access::Accept;
}
match presented_key {
Some(presented) if constant_time_eq(presented.as_bytes(), expected.as_bytes()) => {
Access::Accept
}
_ => Access::Unauthorized,
}
}
}
}
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
std::hint::black_box(diff) == 0
}
pub const RELAY_FORBIDDEN_CONFIG_KEYS: &[&str] =
&["security", "server", "pairing", "remote_access"];
pub fn relay_config_key_forbidden(key: &str) -> bool {
let key = key.trim();
RELAY_FORBIDDEN_CONFIG_KEYS.iter().any(|denied| {
key == *denied
|| key
.strip_prefix(denied)
.is_some_and(|rest| rest.starts_with('.'))
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallerClass {
OnBox,
Lifeline,
OperatorLan,
Remote,
}
impl CallerClass {
pub fn is_first_boot_reach(self) -> bool {
matches!(self, CallerClass::OnBox | CallerClass::Lifeline)
}
}
pub const LIFELINE_LOCAL_ADDRS: [std::net::Ipv4Addr; 2] = [
std::net::Ipv4Addr::new(192, 168, 4, 1),
std::net::Ipv4Addr::new(192, 168, 7, 1),
];
pub fn classify_caller(
peer: Option<IpAddr>,
local: Option<IpAddr>,
has_header: impl Fn(&str) -> bool,
) -> CallerClass {
let Some(peer) = peer else {
return CallerClass::Remote;
};
if FORWARDED_HEADERS.iter().any(|h| has_header(h)) {
return CallerClass::Remote;
}
let on_lifeline_addr = matches!(
local.map(|l| l.to_canonical()),
Some(IpAddr::V4(l)) if LIFELINE_LOCAL_ADDRS.contains(&l)
);
match peer.to_canonical() {
ip if ip.is_loopback() => CallerClass::OnBox,
IpAddr::V4(v4) => {
let o = v4.octets();
if v4.is_link_local() || (on_lifeline_addr && is_rfc1918_v4(o)) {
CallerClass::Lifeline
} else if is_rfc1918_v4(o) {
CallerClass::OperatorLan
} else {
CallerClass::Remote
}
}
IpAddr::V6(v6) if (v6.segments()[0] & 0xffc0) == 0xfe80 => CallerClass::Lifeline,
IpAddr::V6(_) => CallerClass::Remote,
}
}
pub fn local_addr_toward(peer: IpAddr) -> Option<IpAddr> {
let peer = peer.to_canonical();
let bind: std::net::SocketAddr = match peer {
IpAddr::V4(_) => (std::net::Ipv4Addr::UNSPECIFIED, 0).into(),
IpAddr::V6(_) => (std::net::Ipv6Addr::UNSPECIFIED, 0).into(),
};
let socket = std::net::UdpSocket::bind(bind).ok()?;
socket.connect((peer, 9)).ok()?;
socket.local_addr().ok().map(|a| a.ip())
}
fn is_rfc1918_v4(o: [u8; 4]) -> bool {
match o {
[10, _, _, _] => true,
[172, b, _, _] => (16..=31).contains(&b),
[192, 168, _, _] => true,
_ => false,
}
}
pub fn load_pairing(path: &Path) -> Pairing {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Pairing::Unpaired,
Err(_) => return Pairing::Unreadable,
};
let Ok(serde_json::Value::Object(value)) = serde_json::from_str::<serde_json::Value>(&text)
else {
return Pairing::Unreadable;
};
let paired = value
.get("paired")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let key = value.get("api_key").and_then(|v| v.as_str());
match (paired, key) {
(true, Some(k)) if !k.is_empty() => Pairing::Paired(k.to_string()),
(true, _) => Pairing::Unreadable,
_ => Pairing::Unpaired,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::PathBuf;
fn class(ip: &str) -> CallerClass {
class_on(ip, Some("192.168.1.20"))
}
fn class_on(ip: &str, local: Option<&str>) -> CallerClass {
classify_caller(
Some(ip.parse().unwrap()),
local.map(|l| l.parse().unwrap()),
|_| false,
)
}
#[test]
fn a_loopback_peer_with_no_forwarding_header_is_on_box() {
for ip in ["127.0.0.1", "127.0.0.53", "::1", "::ffff:127.0.0.1"] {
assert_eq!(class(ip), CallerClass::OnBox, "{ip}");
}
}
#[test]
fn a_loopback_peer_carrying_any_forwarding_header_is_remote() {
let lo: IpAddr = "127.0.0.1".parse().unwrap();
for header in FORWARDED_HEADERS {
assert_eq!(
classify_caller(Some(lo), Some(lo), |h| h == header),
CallerClass::Remote,
"loopback + {header} is a relayed caller"
);
}
}
#[test]
fn a_forwarded_request_is_remote_from_any_peer() {
for ip in ["192.168.4.20", "192.168.1.50", "169.254.3.4", "fe80::1"] {
let peer: IpAddr = ip.parse().unwrap();
assert_eq!(
classify_caller(Some(peer), Some("192.168.4.1".parse().unwrap()), |h| {
h == "x-forwarded-for"
}),
CallerClass::Remote,
"{ip} + x-forwarded-for"
);
}
}
#[test]
fn an_unknown_peer_is_remote() {
assert_eq!(classify_caller(None, None, |_| false), CallerClass::Remote);
}
#[test]
fn the_first_boot_lifelines_classify_as_lifelines() {
for (ip, local) in [
("192.168.4.37", Some("192.168.4.1")),
("192.168.7.42", Some("192.168.7.1")),
("169.254.11.9", None),
("fe80::1", None),
("::ffff:192.168.4.20", Some("::ffff:192.168.4.1")),
] {
assert_eq!(class_on(ip, local), CallerClass::Lifeline, "{ip}");
}
}
#[test]
fn a_lan_that_shares_the_ap_numbering_is_not_a_lifeline() {
for (ip, local) in [
("192.168.4.37", Some("192.168.4.12")),
("192.168.7.42", Some("192.168.7.9")),
("192.168.4.37", None),
] {
assert_eq!(
class_on(ip, local),
CallerClass::OperatorLan,
"{ip} via {local:?}"
);
}
}
#[test]
fn local_addr_toward_loopback_is_loopback() {
let lo: IpAddr = "127.0.0.1".parse().unwrap();
assert_eq!(local_addr_toward(lo), Some(lo));
}
#[test]
fn a_private_lan_peer_is_an_operator_lan_caller() {
for ip in [
"192.168.1.10",
"192.168.1.50",
"10.0.0.5",
"10.255.255.1",
"172.16.4.4",
"172.31.255.1",
"::ffff:192.168.1.50",
"192.168.40.1",
"192.168.70.1",
"192.168.5.1",
"192.168.6.1",
] {
assert_eq!(class(ip), CallerClass::OperatorLan, "{ip}");
}
}
#[test]
fn a_public_wan_peer_is_remote() {
for ip in [
"8.8.8.8",
"203.0.113.5", "172.15.255.1", "172.32.0.1", "2001:db8::1",
] {
assert_eq!(class(ip), CallerClass::Remote, "{ip}");
}
}
#[test]
fn only_on_box_and_lifelines_reach_an_unpaired_node_without_a_credential() {
assert!(CallerClass::OnBox.is_first_boot_reach());
assert!(CallerClass::Lifeline.is_first_boot_reach());
assert!(!CallerClass::OperatorLan.is_first_boot_reach());
assert!(!CallerClass::Remote.is_first_boot_reach());
}
fn write_pairing(dir: &Path, body: &str) -> PathBuf {
let path = dir.join("pairing.json");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(body.as_bytes()).unwrap();
path
}
#[test]
fn constant_time_eq_matches_byte_equality() {
assert!(constant_time_eq(b"ados_secret", b"ados_secret"));
assert!(!constant_time_eq(b"ados_secret", b"ados_secre1"));
assert!(!constant_time_eq(b"ados_secret", b"xdos_secret"));
assert!(!constant_time_eq(b"ados_secret", b"ados_secret_longer"));
assert!(!constant_time_eq(b"ados_secret", b"short"));
assert!(constant_time_eq(b"", b""));
assert!(!constant_time_eq(b"", b"x"));
}
#[test]
fn absent_file_reads_as_unpaired() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
load_pairing(&dir.path().join("absent.json")),
Pairing::Unpaired
);
}
#[test]
fn paired_with_a_key_reads_as_paired() {
let dir = tempfile::tempdir().unwrap();
let path = write_pairing(dir.path(), r#"{"paired": true, "api_key": "ados_secret"}"#);
assert_eq!(load_pairing(&path), Pairing::Paired("ados_secret".into()));
}
#[test]
fn paired_without_a_key_is_unreadable() {
let dir = tempfile::tempdir().unwrap();
let path = write_pairing(dir.path(), r#"{"paired": true, "api_key": ""}"#);
assert_eq!(load_pairing(&path), Pairing::Unreadable);
}
#[test]
fn a_malformed_file_is_unreadable_and_fails_closed() {
let dir = tempfile::tempdir().unwrap();
for body in [
"this is not json",
"[]",
r#"{"paired": true, "api_key": "k""#,
] {
let path = write_pairing(dir.path(), body);
assert_eq!(load_pairing(&path), Pairing::Unreadable, "{body}");
}
for caller in [
CallerClass::Lifeline,
CallerClass::OperatorLan,
CallerClass::Remote,
] {
assert_eq!(
data_plane_access(&Pairing::Unreadable, caller, Some("anything")),
Access::Unauthorized
);
}
assert_eq!(
data_plane_access(&Pairing::Unreadable, CallerClass::OnBox, None),
Access::Accept
);
}
#[test]
fn a_file_without_paired_true_is_unpaired() {
let dir = tempfile::tempdir().unwrap();
let path = write_pairing(dir.path(), "{}");
assert_eq!(load_pairing(&path), Pairing::Unpaired);
}
#[test]
fn unpaired_accepts_any_caller_at_this_layer() {
for caller in [CallerClass::Remote, CallerClass::OperatorLan] {
assert_eq!(
data_plane_access(&Pairing::Unpaired, caller, None),
Access::Accept
);
assert_eq!(
data_plane_access(&Pairing::Unpaired, caller, Some("anything")),
Access::Accept
);
}
}
#[test]
fn paired_on_box_accepts_without_a_key() {
let p = Pairing::Paired("k".into());
assert_eq!(
data_plane_access(&p, CallerClass::OnBox, None),
Access::Accept
);
}
#[test]
fn paired_non_on_box_callers_need_the_key() {
let p = Pairing::Paired("ados_secret".into());
for caller in [
CallerClass::Lifeline,
CallerClass::OperatorLan,
CallerClass::Remote,
] {
assert_eq!(
data_plane_access(&p, caller, Some("ados_secret")),
Access::Accept,
"{caller:?} with the key"
);
assert_eq!(
data_plane_access(&p, caller, None),
Access::Unauthorized,
"{caller:?} with no key"
);
assert_eq!(
data_plane_access(&p, caller, Some("wrong")),
Access::Unauthorized,
"{caller:?} with a wrong key"
);
}
}
}