use std::collections::HashMap;
use std::io;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant};
use crate::wg::constants::{
NoisePresharedKey, NoisePrivateKey, NoisePublicKey, CHACHAPOLY_KEY_SIZE,
DEFAULT_LOAD_THRESHOLD, REJECT_AFTER_TIME, TAI64N_TIMESTAMP_SIZE,
};
use crate::wg::crypto::x25519_public;
use crate::wg::replay::SlidingWindow;
use crate::wg::transport::EncryptError;
use crate::Result;
pub type UnknownPeerFn = Arc<dyn Fn(NoisePublicKey, SocketAddr, &[u8]) + Send + Sync + 'static>;
#[derive(Clone, Default)]
pub struct Config {
pub private_key: NoisePrivateKey,
pub on_unknown_peer: Option<UnknownPeerFn>,
pub load_threshold: Option<usize>,
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("private_key", &self.private_key)
.field("on_unknown_peer", &self.on_unknown_peer.is_some())
.field("load_threshold", &self.load_threshold)
.finish()
}
}
#[derive(Clone, Debug)]
pub struct PacketResult {
pub ty: PacketType,
pub response: Vec<u8>,
pub data: Vec<u8>,
pub peer_key: NoisePublicKey,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PacketType {
HandshakeResponse,
CookieReply,
TransportData,
Keepalive,
CookieReceived,
}
#[derive(Clone, Debug)]
pub struct PeerInfo {
pub public_key: NoisePublicKey,
pub has_psk: bool,
pub created_at: Instant,
pub expires_at: Option<Instant>,
pub last_handshake: Option<Instant>,
}
struct PeerEntry {
public_key: NoisePublicKey,
preshared_key: NoisePresharedKey,
has_psk: bool,
created_at: Instant,
expires_at: Option<Instant>,
last_handshake: Option<Instant>,
last_timestamp: [u8; TAI64N_TIMESTAMP_SIZE],
has_timestamp: bool,
cookie_gen: Mutex<crate::wg::cookie::CookieGenerator>,
}
impl PeerEntry {
fn new(key: NoisePublicKey, psk: NoisePresharedKey, has_psk: bool) -> PeerEntry {
PeerEntry {
public_key: key,
preshared_key: psk,
has_psk,
created_at: Instant::now(),
expires_at: None,
last_handshake: None,
last_timestamp: [0u8; TAI64N_TIMESTAMP_SIZE],
has_timestamp: false,
cookie_gen: Mutex::new(crate::wg::cookie::CookieGenerator::new(&key)),
}
}
}
pub(crate) struct Keypair {
pub send_key: [u8; CHACHAPOLY_KEY_SIZE],
pub receive_key: [u8; CHACHAPOLY_KEY_SIZE],
pub send_counter: AtomicU64,
pub created: Instant,
pub local_index: u32,
pub remote_index: u32,
pub peer_key: NoisePublicKey,
#[allow(dead_code)]
pub is_initiator: bool,
pub replay_filter: SlidingWindow,
}
#[allow(dead_code)] pub(crate) struct KeypairView<'a> {
pub send_key: &'a [u8; CHACHAPOLY_KEY_SIZE],
pub receive_key: &'a [u8; CHACHAPOLY_KEY_SIZE],
pub send_counter: &'a AtomicU64,
pub created: Instant,
pub remote_index: u32,
pub peer_key: NoisePublicKey,
pub replay_filter: &'a SlidingWindow,
}
pub(crate) struct Session {
pub keypair_current: Option<Arc<Keypair>>,
pub keypair_prev: Option<Arc<Keypair>>,
pub last_received: Instant,
pub last_sent: Instant,
#[allow(dead_code)]
pub peer_key: NoisePublicKey,
}
pub struct Handler {
private_key: NoisePrivateKey,
public_key: NoisePublicKey,
peers: RwLock<HashMap<NoisePublicKey, PeerEntry>>,
pub(crate) handshakes: Mutex<HashMap<u32, crate::wg::handshake::Handshake>>,
pub(crate) keypairs: RwLock<HashMap<u32, Arc<Keypair>>>,
pub(crate) sessions: RwLock<HashMap<NoisePublicKey, Session>>,
on_unknown_peer: Mutex<Option<UnknownPeerFn>>,
load_threshold: usize,
active_handshakes: AtomicUsize,
cookie_checker: Mutex<crate::wg::cookie::CookieChecker>,
}
impl std::fmt::Debug for Handler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Handler")
.field("public_key", &self.public_key)
.field("load_threshold", &self.load_threshold)
.finish()
}
}
impl Handler {
pub fn new(cfg: Config) -> Result<Arc<Self>> {
let priv_key = if cfg.private_key.is_zero() {
crate::wg::crypto::generate_private_key()?
} else {
cfg.private_key
};
let pub_key = x25519_public(&priv_key);
let lt = cfg.load_threshold.unwrap_or(DEFAULT_LOAD_THRESHOLD);
Ok(Arc::new(Handler {
private_key: priv_key,
public_key: pub_key,
peers: RwLock::new(HashMap::new()),
handshakes: Mutex::new(HashMap::new()),
keypairs: RwLock::new(HashMap::new()),
sessions: RwLock::new(HashMap::new()),
on_unknown_peer: Mutex::new(cfg.on_unknown_peer),
load_threshold: lt,
active_handshakes: AtomicUsize::new(0),
cookie_checker: Mutex::new(crate::wg::cookie::CookieChecker::new(&pub_key)),
}))
}
#[inline]
pub fn public_key(&self) -> NoisePublicKey {
self.public_key
}
#[inline]
pub(crate) fn private_key(&self) -> &NoisePrivateKey {
&self.private_key
}
pub fn add_peer(&self, peer_key: NoisePublicKey) {
let mut peers = self.peers.write().expect("peers lock");
peers
.entry(peer_key)
.or_insert_with(|| PeerEntry::new(peer_key, NoisePresharedKey::zero(), false));
}
pub fn set_on_unknown_peer(&self, cb: UnknownPeerFn) {
*self.on_unknown_peer.lock().expect("unknown lock") = Some(cb);
}
pub fn add_peer_with_psk(&self, peer_key: NoisePublicKey, psk: NoisePresharedKey) {
let mut peers = self.peers.write().expect("peers lock");
peers.insert(peer_key, PeerEntry::new(peer_key, psk, true));
}
pub fn remove_peer(&self, peer_key: &NoisePublicKey) {
self.peers.write().expect("peers lock").remove(peer_key);
let removed = self
.sessions
.write()
.expect("sessions lock")
.remove(peer_key);
if let Some(sess) = removed {
let mut kps = self.keypairs.write().expect("keypairs lock");
if let Some(kp) = sess.keypair_current.as_ref() {
kps.remove(&kp.local_index);
}
if let Some(kp) = sess.keypair_prev.as_ref() {
kps.remove(&kp.local_index);
}
}
}
pub fn is_authorized_peer(&self, peer_key: &NoisePublicKey) -> bool {
let peers = self.peers.read().expect("peers lock");
let Some(p) = peers.get(peer_key) else {
return false;
};
if let Some(exp) = p.expires_at {
if Instant::now() > exp {
return false;
}
}
true
}
pub fn set_peer_expiry(&self, peer_key: &NoisePublicKey, at: Instant) {
let mut peers = self.peers.write().expect("peers lock");
if let Some(p) = peers.get_mut(peer_key) {
p.expires_at = Some(at);
}
}
pub fn peers(&self) -> Vec<NoisePublicKey> {
self.peers
.read()
.expect("peers lock")
.keys()
.copied()
.collect()
}
pub fn get_peer_info(&self, peer_key: &NoisePublicKey) -> Option<PeerInfo> {
let peers = self.peers.read().expect("peers lock");
peers.get(peer_key).map(|p| PeerInfo {
public_key: p.public_key,
has_psk: p.has_psk,
created_at: p.created_at,
expires_at: p.expires_at,
last_handshake: p.last_handshake,
})
}
pub(crate) fn preshared_key(&self, peer_key: &NoisePublicKey) -> NoisePresharedKey {
let peers = self.peers.read().expect("peers lock");
match peers.get(peer_key) {
Some(p) if p.has_psk => p.preshared_key.clone(),
_ => NoisePresharedKey::zero(),
}
}
pub(crate) fn accept_peer_timestamp(&self, peer_key: &NoisePublicKey, ts: &[u8]) -> bool {
let mut peers = self.peers.write().expect("peers lock");
let Some(p) = peers.get_mut(peer_key) else {
return false;
};
if p.has_timestamp && ts <= &p.last_timestamp[..] {
return false;
}
let n = ts.len().min(TAI64N_TIMESTAMP_SIZE);
p.last_timestamp[..n].copy_from_slice(&ts[..n]);
p.has_timestamp = true;
p.last_handshake = Some(Instant::now());
true
}
pub(crate) fn touch_peer_handshake(&self, peer_key: &NoisePublicKey) {
let mut peers = self.peers.write().expect("peers lock");
if let Some(p) = peers.get_mut(peer_key) {
p.last_handshake = Some(Instant::now());
}
}
pub(crate) fn notify_unknown_peer(
&self,
peer_key: &NoisePublicKey,
addr: &SocketAddr,
packet: &[u8],
) {
if let Some(cb) = self.on_unknown_peer.lock().expect("unknown lock").clone() {
cb(*peer_key, *addr, packet);
}
}
pub fn process_packet(&self, data: &[u8], remote_addr: &SocketAddr) -> Result<PacketResult> {
if data.len() < 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"packet too short",
));
}
let msg_type = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
use crate::wg::constants::{
MESSAGE_COOKIE_REPLY_TYPE, MESSAGE_INITIATION_TYPE, MESSAGE_RESPONSE_TYPE,
MESSAGE_TRANSPORT_TYPE,
};
match msg_type {
MESSAGE_INITIATION_TYPE => {
crate::wg::handshake::process_handshake_initiation(self, data, remote_addr)
}
MESSAGE_RESPONSE_TYPE => crate::wg::handshake::process_handshake_response(self, data),
MESSAGE_COOKIE_REPLY_TYPE => crate::wg::handshake::process_cookie_reply(self, data),
MESSAGE_TRANSPORT_TYPE => crate::wg::transport::process_data_packet(self, data),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unknown message type: {}", other),
)),
}
}
pub fn encrypt(&self, data: &[u8], peer_key: &NoisePublicKey) -> Result<Vec<u8>> {
match crate::wg::transport::encrypt_data_packet(self, data, peer_key) {
Ok(v) => Ok(v),
Err(EncryptError::RekeyRequired(v)) => Ok(v),
Err(e) => Err(e.into()),
}
}
pub fn generate_keepalive(&self, peer_key: &NoisePublicKey) -> Result<Vec<u8>> {
self.encrypt(&[], peer_key)
}
pub fn initiate_handshake(&self, peer_key: &NoisePublicKey) -> Result<Vec<u8>> {
crate::wg::handshake::initiate_handshake(self, peer_key)
}
pub fn has_session(&self, peer_key: &NoisePublicKey) -> bool {
self.sessions
.read()
.expect("sessions lock")
.get(peer_key)
.and_then(|s| s.keypair_current.as_ref())
.is_some()
}
pub fn maintenance(&self) {
self.cleanup_handshakes();
self.cleanup_sessions();
}
fn cleanup_handshakes(&self) {
let mut hs = self.handshakes.lock().expect("handshakes lock");
let n = Instant::now();
hs.retain(|_, h| n.duration_since(h.created) <= REJECT_AFTER_TIME);
}
fn cleanup_sessions(&self) {
let n = Instant::now();
let mut sess = self.sessions.write().expect("sessions lock");
let mut to_remove = Vec::new();
for (k, s) in sess.iter() {
let last_active = s.last_received.max(s.last_sent);
if n.duration_since(last_active) > REJECT_AFTER_TIME {
to_remove.push(*k);
}
}
let mut kps = self.keypairs.write().expect("keypairs lock");
for k in &to_remove {
if let Some(s) = sess.remove(k) {
if let Some(kp) = s.keypair_current.as_ref() {
kps.remove(&kp.local_index);
}
if let Some(kp) = s.keypair_prev.as_ref() {
kps.remove(&kp.local_index);
}
}
}
}
pub fn close(&self) -> Result<()> {
self.handshakes.lock().expect("handshakes lock").clear();
self.keypairs.write().expect("keypairs lock").clear();
self.sessions.write().expect("sessions lock").clear();
Ok(())
}
pub(crate) fn insert_handshake(
&self,
idx: u32,
hs: crate::wg::handshake::Handshake,
) -> Result<()> {
let mut g = self.handshakes.lock().expect("handshakes lock");
if g.len() >= crate::wg::constants::MAX_HANDSHAKES && !g.contains_key(&idx) {
return Err(io::Error::other("handshake table full"));
}
g.insert(idx, hs);
Ok(())
}
pub(crate) fn take_handshake(&self, idx: u32) -> Option<crate::wg::handshake::Handshake> {
self.handshakes
.lock()
.expect("handshakes lock")
.remove(&idx)
}
pub(crate) fn has_handshake_index(&self, idx: u32) -> bool {
self.handshakes
.lock()
.expect("handshakes lock")
.contains_key(&idx)
}
pub(crate) fn install_keypair(&self, idx: u32, kp: Arc<Keypair>) {
self.keypairs
.write()
.expect("keypairs lock")
.insert(idx, kp);
}
pub(crate) fn lookup_keypair(&self, idx: u32) -> Option<Arc<Keypair>> {
self.keypairs
.read()
.expect("keypairs lock")
.get(&idx)
.cloned()
}
pub(crate) fn has_keypair_index(&self, idx: u32) -> bool {
self.keypairs
.read()
.expect("keypairs lock")
.contains_key(&idx)
}
pub(crate) fn check_keypair_capacity(&self, cap: usize) -> Result<()> {
if self.keypairs.read().expect("keypairs lock").len() >= cap {
return Err(io::Error::other("keypair table full"));
}
Ok(())
}
pub(crate) fn check_session_capacity(&self, cap: usize, k: &NoisePublicKey) -> Result<()> {
let g = self.sessions.read().expect("sessions lock");
if g.len() >= cap && !g.contains_key(k) {
return Err(io::Error::other("session table full"));
}
Ok(())
}
pub(crate) fn upsert_session(&self, peer_key: NoisePublicKey, kp: Arc<Keypair>) {
let mut sess = self.sessions.write().expect("sessions lock");
match sess.get_mut(&peer_key) {
Some(s) => {
if let Some(prev) = s.keypair_current.take() {
s.keypair_prev = Some(prev);
}
s.keypair_current = Some(kp);
}
None => {
sess.insert(
peer_key,
Session {
keypair_current: Some(kp),
keypair_prev: None,
last_received: Instant::now(),
last_sent: Instant::now(),
peer_key,
},
);
}
}
}
pub fn session_info(&self, peer_key: &NoisePublicKey) -> Option<(Instant, Instant)> {
self.sessions
.read()
.expect("sessions lock")
.get(peer_key)
.map(|s| (s.last_received, s.last_sent))
}
pub(crate) fn touch_session_received(&self, peer_key: &NoisePublicKey) {
if let Some(s) = self
.sessions
.write()
.expect("sessions lock")
.get_mut(peer_key)
{
s.last_received = Instant::now();
}
}
pub(crate) fn touch_session_sent(&self, peer_key: &NoisePublicKey) {
if let Some(s) = self
.sessions
.write()
.expect("sessions lock")
.get_mut(peer_key)
{
s.last_sent = Instant::now();
}
}
pub(crate) fn with_current_keypair<'a>(
&'a self,
peer_key: &NoisePublicKey,
) -> Option<(KeypairView<'a>, Duration)> {
let kp = {
let s = self.sessions.read().expect("sessions lock");
s.get(peer_key)
.and_then(|s| s.keypair_current.as_ref().cloned())?
};
self.touch_session_sent(peer_key);
let age = Instant::now().duration_since(kp.created);
let leaked: &'a Keypair = Box::leak(Box::new(KeypairCarrier(kp))).0.as_ref();
let view = KeypairView {
send_key: &leaked.send_key,
receive_key: &leaked.receive_key,
send_counter: &leaked.send_counter,
created: leaked.created,
remote_index: leaked.remote_index,
peer_key: leaked.peer_key,
replay_filter: &leaked.replay_filter,
};
Some((view, age))
}
pub(crate) fn inc_active_handshakes(&self) {
self.active_handshakes.fetch_add(1, Ordering::SeqCst);
}
pub(crate) fn is_under_load(&self) -> bool {
self.active_handshakes.load(Ordering::SeqCst) > self.load_threshold
}
pub(crate) fn cookie_check_mac1(&self, data: &[u8]) -> bool {
self.cookie_checker
.lock()
.expect("cookie lock")
.check_mac1(data)
}
pub(crate) fn cookie_check_mac2(&self, data: &[u8], src: &[u8]) -> bool {
self.cookie_checker
.lock()
.expect("cookie lock")
.check_mac2(data, src)
}
pub(crate) fn cookie_generate_reply(
&self,
src: &[u8],
receiver_idx: u32,
init_mac1: &[u8],
) -> Result<Vec<u8>> {
self.cookie_checker
.lock()
.expect("cookie lock")
.generate_reply(src, receiver_idx, init_mac1)
}
pub(crate) fn cookie_add_macs(&self, peer: &NoisePublicKey, pkt: &mut [u8]) {
let peers = self.peers.read().expect("peers lock");
if let Some(entry) = peers.get(peer) {
entry
.cookie_gen
.lock()
.expect("cookie_gen lock")
.add_macs(pkt);
} else {
let n = pkt.len();
if n >= crate::wg::constants::BLAKE2S_128_SIZE * 2 {
let smac2 = n - crate::wg::constants::BLAKE2S_128_SIZE;
let smac1 = smac2 - crate::wg::constants::BLAKE2S_128_SIZE;
let key = crate::wg::crypto::calculate_mac1_key(peer);
let mac1 = crate::wg::crypto::blake2s_mac_128(&key, &pkt[..smac1]);
pkt[smac1..smac2].copy_from_slice(&mac1);
}
}
}
pub(crate) fn handshake_remote_static(&self, idx: u32) -> Option<NoisePublicKey> {
self.handshakes
.lock()
.expect("handshakes lock")
.get(&idx)
.map(|hs| hs.remote_static)
}
pub(crate) fn peer_consume_cookie(
&self,
peer: &NoisePublicKey,
nonce: &[u8; 24],
ct: &[u8],
) -> Result<()> {
let peers = self.peers.read().expect("peers lock");
let entry = match peers.get(peer) {
Some(e) => e,
None => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no peer for cookie reply",
))
}
};
let result = entry
.cookie_gen
.lock()
.expect("cookie_gen lock")
.consume_reply(nonce, ct);
result
}
pub(crate) fn dec_active_handshakes(&self) {
let prev = self.active_handshakes.fetch_sub(1, Ordering::SeqCst);
if prev == 0 {
self.active_handshakes.store(0, Ordering::SeqCst);
}
}
pub fn accept_unknown_peer(
&self,
peer_key: NoisePublicKey,
initiation_packet: &[u8],
remote_addr: &SocketAddr,
) -> Result<Vec<u8>> {
self.add_peer(peer_key);
let res = self.process_packet(initiation_packet, remote_addr)?;
Ok(res.response)
}
}
struct KeypairCarrier(#[allow(dead_code)] pub Arc<Keypair>);
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
use std::net::SocketAddrV4;
fn loopback() -> SocketAddr {
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 51820))
}
#[test]
fn handler_roundtrip_handshake_and_transport() {
let a = Handler::new(Config::default()).unwrap();
let b = Handler::new(Config::default()).unwrap();
a.add_peer(b.public_key());
b.add_peer(a.public_key());
let init = a.initiate_handshake(&b.public_key()).unwrap();
let addr = loopback();
let resp = b.process_packet(&init, &addr).unwrap();
assert_eq!(resp.ty, PacketType::HandshakeResponse);
assert_eq!(resp.peer_key, a.public_key());
let res = a.process_packet(&resp.response, &addr).unwrap();
assert_eq!(res.ty, PacketType::HandshakeResponse);
assert!(!res.response.is_empty(), "expect keepalive ciphertext");
let kp_res = b.process_packet(&res.response, &addr).unwrap();
assert_eq!(kp_res.ty, PacketType::Keepalive);
assert!(kp_res.data.is_empty());
let pt = b"hello over wg";
let enc = a.encrypt(pt, &b.public_key()).unwrap();
let dec = b.process_packet(&enc, &addr).unwrap();
assert_eq!(dec.ty, PacketType::TransportData);
assert_eq!(dec.data, pt);
}
#[test]
fn unknown_peer_rejected() {
let a = Handler::new(Config::default()).unwrap();
let b = Handler::new(Config::default()).unwrap();
a.add_peer(b.public_key());
let init = a.initiate_handshake(&b.public_key()).unwrap();
let addr = loopback();
let err = b.process_packet(&init, &addr).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn transport_counter_advances_and_replays_blocked() {
let a = Handler::new(Config::default()).unwrap();
let b = Handler::new(Config::default()).unwrap();
a.add_peer(b.public_key());
b.add_peer(a.public_key());
let init = a.initiate_handshake(&b.public_key()).unwrap();
let addr = loopback();
let resp = b.process_packet(&init, &addr).unwrap();
let _ = a.process_packet(&resp.response, &addr).unwrap();
let c1 = a.encrypt(b"one", &b.public_key()).unwrap();
let c2 = a.encrypt(b"two", &b.public_key()).unwrap();
assert_ne!(c1, c2);
assert_eq!(crate::wg::encrypted_size(3), c1.len());
let r1 = b.process_packet(&c1, &addr).unwrap();
assert_eq!(r1.data, b"one");
let err = b.process_packet(&c1, &addr).unwrap_err();
assert!(err.to_string().contains("replay"), "got: {}", err);
}
#[test]
fn peer_lookup_smoke() {
let h = Handler::new(Config::default()).unwrap();
let k1 = NoisePublicKey([1u8; 32]);
let k2 = NoisePublicKey([2u8; 32]);
assert!(!h.is_authorized_peer(&k1));
h.add_peer(k1);
assert!(h.is_authorized_peer(&k1));
assert!(!h.is_authorized_peer(&k2));
let list = h.peers();
assert_eq!(list.len(), 1);
assert_eq!(list[0], k1);
h.remove_peer(&k1);
assert!(!h.is_authorized_peer(&k1));
}
}