use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
const DEFAULT_TTL_SECS: u64 = 300;
const MAX_CODE_LEN: usize = 128;
struct BootstrapCode {
code: String,
expires_at: Instant,
}
fn store() -> &'static Mutex<HashMap<String, BootstrapCode>> {
static STORE: OnceLock<Mutex<HashMap<String, BootstrapCode>>> = OnceLock::new();
STORE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn is_loopback(addr: &std::net::SocketAddr) -> bool {
match addr.ip() {
IpAddr::V4(v4) => v4 == Ipv4Addr::LOCALHOST,
IpAddr::V6(v6) => v6 == Ipv6Addr::LOCALHOST,
}
}
fn peer_is_loopback(addr: &std::net::SocketAddr, forwarded_for: Option<&str>) -> bool {
if !is_loopback(addr) {
return false;
}
let Some(ff) = forwarded_for else {
return true;
};
let first = ff.split(',').next().unwrap_or("").trim();
match first.parse::<IpAddr>() {
Ok(ip) => ip.is_loopback(),
Err(_) => true,
}
}
pub fn issue_code() -> String {
let code = random_code();
let ttl = ttl();
let mut map = store().lock().unwrap_or_else(|p| p.into_inner());
map.clear();
map.insert(
code.clone(),
BootstrapCode {
code: code.clone(),
expires_at: Instant::now() + Duration::from_secs(ttl),
},
);
code
}
fn ttl() -> u64 {
std::env::var("GHOSTLINK_BOOTSTRAP_TTL_SECS")
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.filter(|v| *v > 0)
.unwrap_or(DEFAULT_TTL_SECS)
}
fn random_code() -> String {
let mut bytes = [0u8; 16];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes);
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn redeem(
presented: &str,
addr: &std::net::SocketAddr,
forwarded_for: Option<&str>,
) -> Result<String, &'static str> {
if presented.is_empty() || presented.len() > MAX_CODE_LEN {
return Err("invalid bootstrap code");
}
if !peer_is_loopback(addr, forwarded_for) {
return Err("bootstrap exchange is only available from loopback");
}
let now = Instant::now();
let mut map = store().lock().unwrap_or_else(|p| p.into_inner());
map.retain(|_, v| v.expires_at > now);
let Some(entry) = map.get(presented) else {
return Err("invalid or expired bootstrap code");
};
if entry.code != presented {
return Err("invalid or expired bootstrap code");
}
map.remove(presented);
Ok(presented.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard, OnceLock};
fn test_guard() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let m = LOCK.get_or_init(|| Mutex::new(()));
m.lock().unwrap_or_else(|p| p.into_inner())
}
fn loopback_v4() -> std::net::SocketAddr {
"127.0.0.1:5000".parse().unwrap()
}
fn remote() -> std::net::SocketAddr {
"10.0.0.5:5000".parse().unwrap()
}
#[test]
fn issued_code_is_128_bits_of_hex() {
let _guard = test_guard();
let c = issue_code();
assert_eq!(c.len(), 32);
assert!(c.chars().all(|ch| ch.is_ascii_hexdigit()));
}
#[test]
fn two_issues_differ() {
let _guard = test_guard();
assert_ne!(issue_code(), issue_code());
}
#[test]
fn redemption_is_single_use() {
let _guard = test_guard();
let code = issue_code();
assert!(redeem(&code, &loopback_v4(), None).is_ok());
assert!(redeem(&code, &loopback_v4(), None).is_err());
}
#[test]
fn refused_from_non_loopback() {
let _guard = test_guard();
let code = issue_code();
assert!(redeem(&code, &remote(), None).is_err());
assert!(redeem(&code, &loopback_v4(), None).is_ok());
}
#[test]
fn forwarded_for_cannot_spoof_loopback() {
let _guard = test_guard();
let code = issue_code();
assert!(redeem(&code, &remote(), Some("127.0.0.1")).is_err());
assert!(redeem(&code, &remote(), Some("8.8.8.8")).is_err());
}
#[test]
fn forwarded_for_from_loopback_peer_is_honoured() {
let _guard = test_guard();
let code = issue_code();
assert!(redeem(&code, &loopback_v4(), Some("127.0.0.1")).is_ok());
}
#[test]
fn ipv6_loopback_counts() {
let _guard = test_guard();
let code = issue_code();
let v6: std::net::SocketAddr = "[::1]:5000".parse().unwrap();
assert!(redeem(&code, &v6, None).is_ok());
}
#[test]
fn garbage_and_empty_are_rejected() {
let _guard = test_guard();
let _ = issue_code();
assert!(redeem("", &loopback_v4(), None).is_err());
assert!(redeem("deadbeef", &loopback_v4(), None).is_err());
assert!(redeem(&"a".repeat(MAX_CODE_LEN + 1), &loopback_v4(), None).is_err());
}
#[test]
fn expired_code_is_rejected() {
let _guard = test_guard();
let code = "expired-code-for-test".to_string();
store().lock().unwrap().insert(
code.clone(),
BootstrapCode {
code: code.clone(),
expires_at: Instant::now() - Duration::from_secs(1),
},
);
assert!(redeem(&code, &loopback_v4(), None).is_err());
}
}