use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::net::{TcpListener, TcpStream};
use tokio_util::sync::CancellationToken;
use crate::connect::gate::ConnectDenialReason;
use crate::connect::{evaluate_connect_gate, ConnectDiagnostics, ConnectPolicy};
use crate::error::{NetworkError, NetworkResult};
use crate::identity::{AgentId, AgentKeypair, MachineId};
use crate::streams::{PeerStream, StreamProtocol};
use crate::trust::TrustDecision;
use ant_quic::crypto::raw_public_keys::pqc::{
sign_with_ml_dsa, verify_with_ml_dsa, MlDsaSignature,
};
use ant_quic::{HighLevelRecvStream, HighLevelSendStream, MlDsaPublicKey};
const RESP_CONNECTED: u8 = 0x01;
const RESP_DENIED: u8 = 0x00;
const MAX_HEADER_BYTES: u32 = 256;
pub const MAX_HEADER_V2_BYTES: u32 = 8192;
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const HEADER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const MAX_INBOUND_STREAMS: usize = 256;
const MAX_OUTBOUND_STREAMS: usize = 256;
const MAX_STREAMS_PER_PEER: u32 = 32;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForwardHeader {
pub target_host: String,
pub target_port: u16,
}
impl ForwardHeader {
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let body = bincode::serialize(self).unwrap_or_default();
let mut out = Vec::with_capacity(4 + body.len());
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
out.extend_from_slice(&body);
out
}
pub fn decode(buf: &[u8]) -> Result<(Self, usize), ForwardError> {
if buf.len() < 4 {
return Err(ForwardError::Truncated);
}
let len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
if len > MAX_HEADER_BYTES {
return Err(ForwardError::Oversize(len));
}
let end = 4 + len as usize;
if buf.len() < end {
return Err(ForwardError::Truncated);
}
let header: Self =
bincode::deserialize(&buf[4..end]).map_err(|e| ForwardError::Decode(e.to_string()))?;
Ok((header, end))
}
}
const FORWARD_V2_ATTESTATION_DOMAIN: &[u8] = b"x0x-forward-v2-attestation.v2";
const FORWARD_V2_ATTESTATION_TTL_MS: u64 = 60_000;
const FORWARD_V2_ATTESTATION_FUTURE_SKEW_MS: u64 = 5_000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForwardV2Header {
pub target_host: String,
pub target_port: u16,
pub opener_agent_id: AgentId,
pub opener_agent_public_key: Vec<u8>,
pub recipient_machine_id: MachineId,
pub issued_at_ms: u64,
pub signature: Vec<u8>,
}
impl ForwardV2Header {
#[must_use]
pub fn new(
target_host: String,
target_port: u16,
opener_agent_id: AgentId,
opener_agent_public_key: Vec<u8>,
recipient_machine_id: MachineId,
) -> Self {
Self {
target_host,
target_port,
opener_agent_id,
opener_agent_public_key,
recipient_machine_id,
issued_at_ms: 0,
signature: Vec::new(),
}
}
#[must_use]
pub fn signable_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(160);
buf.extend_from_slice(FORWARD_V2_ATTESTATION_DOMAIN);
buf.extend_from_slice(&(self.target_host.len() as u32).to_le_bytes());
buf.extend_from_slice(self.target_host.as_bytes());
buf.extend_from_slice(&self.target_port.to_le_bytes());
buf.extend_from_slice(&self.opener_agent_id.0);
buf.extend_from_slice(&(self.opener_agent_public_key.len() as u32).to_le_bytes());
buf.extend_from_slice(&self.opener_agent_public_key);
buf.extend_from_slice(&self.recipient_machine_id.0);
buf.extend_from_slice(&self.issued_at_ms.to_le_bytes());
buf
}
pub fn sign(&mut self, keypair: &AgentKeypair) -> Result<(), ForwardError> {
self.issued_at_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let sig = sign_with_ml_dsa(keypair.secret_key(), &self.signable_bytes())
.map_err(|e| ForwardError::AttestationSign(format!("{e:?}")))?;
self.signature = sig.as_bytes().to_vec();
Ok(())
}
pub fn verify_attestation(&self, agent_public_key: &[u8]) -> Result<(), ForwardError> {
if self.signature.is_empty() {
return Err(ForwardError::AttestationMissing);
}
let pubkey = MlDsaPublicKey::from_bytes(agent_public_key)
.map_err(|e| ForwardError::AttestationKeyMismatch(format!("bad pubkey: {e:?}")))?;
let derived = AgentId::from_public_key(&pubkey);
if derived != self.opener_agent_id {
return Err(ForwardError::AttestationKeyMismatch(format!(
"agent_id {} does not match key-derived id {}",
hex::encode(self.opener_agent_id.as_bytes()),
hex::encode(derived.as_bytes()),
)));
}
let sig = MlDsaSignature::from_bytes(&self.signature)
.map_err(|e| ForwardError::AttestationInvalid(format!("bad sig: {e:?}")))?;
verify_with_ml_dsa(&pubkey, &self.signable_bytes(), &sig)
.map_err(|e| ForwardError::AttestationInvalid(format!("verify: {e:?}")))?;
Ok(())
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let body = bincode::serialize(self).unwrap_or_default();
let mut out = Vec::with_capacity(4 + body.len());
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
out.extend_from_slice(&body);
out
}
pub fn decode(buf: &[u8]) -> Result<(Self, usize), ForwardError> {
if buf.len() < 4 {
return Err(ForwardError::Truncated);
}
let len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
if len > MAX_HEADER_V2_BYTES {
return Err(ForwardError::Oversize(len));
}
let end = 4 + len as usize;
if buf.len() < end {
return Err(ForwardError::Truncated);
}
let header: Self =
bincode::deserialize(&buf[4..end]).map_err(|e| ForwardError::Decode(e.to_string()))?;
Ok((header, end))
}
}
#[must_use]
fn encode_response_connected() -> [u8; 1] {
[RESP_CONNECTED]
}
#[must_use]
fn encode_response_denied(reason: ConnectDenialReason) -> Vec<u8> {
let reason_bytes = serde_json::to_vec(&reason).unwrap_or_default();
let mut out = Vec::with_capacity(1 + 4 + reason_bytes.len());
out.push(RESP_DENIED);
out.extend_from_slice(&(reason_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(&reason_bytes);
out
}
#[must_use]
fn response_connected(byte: u8) -> Option<bool> {
match byte {
RESP_CONNECTED => Some(true),
RESP_DENIED => Some(false),
_ => None,
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ForwardError {
#[error("truncated forward frame")]
Truncated,
#[error("oversize forward frame: {0} bytes")]
Oversize(u32),
#[error("forward frame decode failed: {0}")]
Decode(String),
#[error("target host is not a numeric loopback IP: {0}")]
NotLoopbackTarget(String),
#[error("forward v2 attestation missing")]
AttestationMissing,
#[error("forward v2 attestation key mismatch: {0}")]
AttestationKeyMismatch(String),
#[error("forward v2 attestation invalid: {0}")]
AttestationInvalid(String),
#[error("forward v2 attestation sign failed: {0}")]
AttestationSign(String),
}
fn resolve_loopback_target(
target_host: &str,
target_port: u16,
) -> Result<SocketAddr, ForwardError> {
let ip: IpAddr = target_host
.parse()
.map_err(|_| ForwardError::NotLoopbackTarget(target_host.to_string()))?;
if !crate::connect::is_loopback(ip) {
return Err(ForwardError::NotLoopbackTarget(target_host.to_string()));
}
Ok(SocketAddr::new(ip, target_port))
}
fn decide_inbound(
header: &ForwardHeader,
policy: &ConnectPolicy,
agents: &[AgentId],
machine_id: &MachineId,
) -> Result<SocketAddr, ConnectDenialReason> {
let target = resolve_loopback_target(&header.target_host, header.target_port)
.map_err(|_| ConnectDenialReason::TargetNotLoopback)?;
for agent_id in agents {
evaluate_connect_gate(
true,
Some(TrustDecision::Accept),
policy,
agent_id,
machine_id,
&target,
)?;
}
Ok(target)
}
struct AttestationVerifyCtx {
discovery_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<AgentId, crate::DiscoveredAgent>>,
>,
contact_store: std::sync::Arc<tokio::sync::RwLock<crate::contacts::ContactStore>>,
own_machine_id: MachineId,
now_ms: u64,
}
async fn decide_inbound_attested(
header: &ForwardV2Header,
policy: &ConnectPolicy,
peer_machine: &MachineId,
ctx: &AttestationVerifyCtx,
) -> Result<SocketAddr, ConnectDenialReason> {
let target = resolve_loopback_target(&header.target_host, header.target_port)
.map_err(|_| ConnectDenialReason::TargetNotLoopback)?;
if header.recipient_machine_id != ctx.own_machine_id {
return Err(ConnectDenialReason::AttestationFailed);
}
if header.issued_at_ms == 0 {
return Err(ConnectDenialReason::AttestationFailed);
}
if ctx.now_ms
> header
.issued_at_ms
.saturating_add(FORWARD_V2_ATTESTATION_TTL_MS)
{
return Err(ConnectDenialReason::AttestationFailed);
}
if header.issued_at_ms
> ctx
.now_ms
.saturating_add(FORWARD_V2_ATTESTATION_FUTURE_SKEW_MS)
{
return Err(ConnectDenialReason::AttestationFailed);
}
let cached = {
let cache = ctx.discovery_cache.read().await;
cache.get(&header.opener_agent_id).cloned()
};
let agent = cached.ok_or(ConnectDenialReason::AttestationFailed)?;
if agent.machine_id != *peer_machine {
return Err(ConnectDenialReason::AgentNotOnMachine);
}
header
.verify_attestation(&header.opener_agent_public_key)
.map_err(|_| ConnectDenialReason::AttestationFailed)?;
if agent.agent_public_key.is_empty() && !header.opener_agent_public_key.is_empty() {
let mut cache = ctx.discovery_cache.write().await;
if let Some(entry) = cache.get_mut(&header.opener_agent_id) {
entry.agent_public_key = header.opener_agent_public_key.clone();
}
}
let trust_decision = {
let contacts = ctx.contact_store.read().await;
let evaluator = crate::trust::TrustEvaluator::new(&contacts);
evaluator.evaluate(&crate::trust::TrustContext {
agent_id: &header.opener_agent_id,
machine_id: peer_machine,
})
};
evaluate_connect_gate(
true,
Some(trust_decision),
policy,
&header.opener_agent_id,
peer_machine,
&target,
)?;
Ok(target)
}
#[derive(Debug, Default)]
pub struct ForwardDiagnostics {
connect_failed: AtomicU64,
active_streams: AtomicU64,
streams_over_cap: AtomicU64,
revoked_mid_flight: AtomicU64,
header_timeout: AtomicU64,
}
impl ForwardDiagnostics {
pub fn record_connect_failed(&self) {
self.connect_failed.fetch_add(1, Ordering::Relaxed);
}
pub fn enter_stream(&self) -> u64 {
self.active_streams.fetch_add(1, Ordering::Relaxed) + 1
}
pub fn leave_stream(&self) {
self.active_streams.fetch_sub(1, Ordering::Relaxed);
}
pub fn record_over_cap(&self) {
self.streams_over_cap.fetch_add(1, Ordering::Relaxed);
}
pub fn record_revoked_mid_flight(&self) {
self.revoked_mid_flight.fetch_add(1, Ordering::Relaxed);
}
pub fn record_header_timeout(&self) {
self.header_timeout.fetch_add(1, Ordering::Relaxed);
}
#[must_use]
pub fn connect_failed(&self) -> u64 {
self.connect_failed.load(Ordering::Relaxed)
}
#[must_use]
pub fn active_streams(&self) -> u64 {
self.active_streams.load(Ordering::Relaxed)
}
#[must_use]
pub fn streams_over_cap(&self) -> u64 {
self.streams_over_cap.load(Ordering::Relaxed)
}
#[must_use]
pub fn revoked_mid_flight(&self) -> u64 {
self.revoked_mid_flight.load(Ordering::Relaxed)
}
#[must_use]
pub fn header_timeout(&self) -> u64 {
self.header_timeout.load(Ordering::Relaxed)
}
}
pub(crate) struct InboundCtx {
pub policy: Arc<ConnectPolicy>,
pub connect_diag: Arc<ConnectDiagnostics>,
pub fwd_diag: Arc<ForwardDiagnostics>,
pub revocation_set: Arc<tokio::sync::RwLock<crate::revocation::RevocationSet>>,
pub discovery_cache:
Arc<tokio::sync::RwLock<std::collections::HashMap<AgentId, crate::DiscoveredAgent>>>,
pub contact_store: Arc<tokio::sync::RwLock<crate::contacts::ContactStore>>,
pub own_machine_id: MachineId,
pub require_attestation: bool,
}
pub(crate) async fn handle_inbound(mut stream: PeerStream, ctx: &InboundCtx) {
let agents: Vec<AgentId> = stream.peer_agents().to_vec();
let machine_id = stream.peer();
let peer = machine_id;
{
let revoked = ctx.revocation_set.read().await;
let any_agent_revoked = agents.iter().any(|a| revoked.is_agent_revoked(a));
if any_agent_revoked || revoked.is_machine_revoked(&machine_id) {
ctx.fwd_diag.record_revoked_mid_flight();
tracing::info!(
target: "x0x::forward",
agent = %hex::encode(agents[0].as_bytes()),
agent_count = agents.len(),
machine = %hex::encode(peer.as_bytes()),
outcome = "drop_revoked_mid_flight",
"inbound forward: peer revoked after accept — dropping before header read"
);
return;
}
}
let target = match stream.protocol() {
StreamProtocol::ForwardV2 => {
let header =
match tokio::time::timeout(HEADER_READ_TIMEOUT, read_header_v2(stream.recv_mut()))
.await
{
Ok(Ok(h)) => h,
Ok(Err(e)) => {
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
error = %e,
"inbound forward v2: header read failed — closing stream"
);
return;
}
Err(_) => {
ctx.fwd_diag.record_header_timeout();
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
"inbound forward v2: header read timed out — resetting stream"
);
return;
}
};
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let verify_ctx = AttestationVerifyCtx {
discovery_cache: Arc::clone(&ctx.discovery_cache),
contact_store: Arc::clone(&ctx.contact_store),
own_machine_id: ctx.own_machine_id,
now_ms,
};
match decide_inbound_attested(&header, &ctx.policy, &machine_id, &verify_ctx).await {
Ok(addr) => addr,
Err(reason) => {
ctx.connect_diag.record_denied(reason);
let _ = stream
.send_mut()
.write_all(&encode_response_denied(reason))
.await;
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
?reason,
target = %header.target_host,
port = header.target_port,
"inbound forward v2 denied at attestation/connect gate"
);
return;
}
}
}
_ => {
if ctx.require_attestation {
ctx.connect_diag
.record_denied(ConnectDenialReason::AttestationFailed);
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
"inbound forward v1 denied: attestation required (ctx.require_attestation=true)"
);
return;
}
let header =
match tokio::time::timeout(HEADER_READ_TIMEOUT, read_header(stream.recv_mut()))
.await
{
Ok(Ok(h)) => h,
Ok(Err(e)) => {
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
error = %e,
"inbound forward: header read failed — closing stream"
);
return;
}
Err(_) => {
ctx.fwd_diag.record_header_timeout();
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
"inbound forward: header read timed out — resetting stream"
);
return;
}
};
match decide_inbound(&header, &ctx.policy, &agents, &machine_id) {
Ok(addr) => addr,
Err(reason) => {
ctx.connect_diag.record_denied(reason);
let _ = stream
.send_mut()
.write_all(&encode_response_denied(reason))
.await;
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
?reason,
target = %header.target_host,
port = header.target_port,
"inbound forward denied at connect gate"
);
return;
}
}
}
};
if !target.ip().is_loopback() {
ctx.connect_diag
.record_denied(ConnectDenialReason::TargetNotLoopback);
let _ = stream
.send_mut()
.write_all(&encode_response_denied(
ConnectDenialReason::TargetNotLoopback,
))
.await;
return;
}
let local = match tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect(target)).await {
Ok(Ok(tcp)) => tcp,
Ok(Err(e)) => {
ctx.fwd_diag.record_connect_failed();
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
target = %target,
error = %e,
"inbound forward: local connect failed"
);
return;
}
Err(_) => {
ctx.fwd_diag.record_connect_failed();
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer.as_bytes()),
target = %target,
"inbound forward: local connect timed out"
);
return;
}
};
ctx.connect_diag.record_allowed();
if stream
.send_mut()
.write_all(&encode_response_connected())
.await
.is_err()
{
return;
}
let (send, recv) = stream.into_split();
bridge(local, send, recv).await;
}
struct StreamLeaveGuard(Arc<ForwardDiagnostics>);
impl Drop for StreamLeaveGuard {
fn drop(&mut self) {
self.0.leave_stream();
}
}
async fn bridge(tcp: TcpStream, mut send: HighLevelSendStream, mut recv: HighLevelRecvStream) {
let (mut tcp_read, mut tcp_write) = tcp.into_split();
let to_stream = tokio::io::copy(&mut tcp_read, &mut send);
let from_stream = tokio::io::copy(&mut recv, &mut tcp_write);
let _ = tokio::join!(to_stream, from_stream);
}
async fn read_header<R: tokio::io::AsyncRead + Unpin>(
r: &mut R,
) -> Result<ForwardHeader, ForwardError> {
use tokio::io::AsyncReadExt;
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf)
.await
.map_err(|_| ForwardError::Truncated)?;
let len = u32::from_be_bytes(len_buf);
if len > MAX_HEADER_BYTES {
return Err(ForwardError::Oversize(len));
}
let mut body = vec![0u8; len as usize];
r.read_exact(&mut body)
.await
.map_err(|_| ForwardError::Truncated)?;
bincode::deserialize(&body).map_err(|e| ForwardError::Decode(e.to_string()))
}
async fn write_header<W: tokio::io::AsyncWrite + Unpin>(
w: &mut W,
header: &ForwardHeader,
) -> Result<(), NetworkError> {
use tokio::io::AsyncWriteExt;
let frame = header.encode();
w.write_all(&frame)
.await
.map_err(|e| NetworkError::StreamError(format!("write forward header: {e}")))
}
async fn read_header_v2<R: tokio::io::AsyncRead + Unpin>(
r: &mut R,
) -> Result<ForwardV2Header, ForwardError> {
use tokio::io::AsyncReadExt;
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf)
.await
.map_err(|_| ForwardError::Truncated)?;
let len = u32::from_be_bytes(len_buf);
if len > MAX_HEADER_V2_BYTES {
return Err(ForwardError::Oversize(len));
}
let mut body = vec![0u8; len as usize];
r.read_exact(&mut body)
.await
.map_err(|_| ForwardError::Truncated)?;
bincode::deserialize(&body).map_err(|e| ForwardError::Decode(e.to_string()))
}
async fn write_header_v2<W: tokio::io::AsyncWrite + Unpin>(
w: &mut W,
header: &ForwardV2Header,
) -> Result<(), NetworkError> {
use tokio::io::AsyncWriteExt;
let frame = header.encode();
w.write_all(&frame)
.await
.map_err(|e| NetworkError::StreamError(format!("write forward v2 header: {e}")))
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ForwardConfig {
#[serde(default = "default_require_attestation")]
pub require_attestation: bool,
}
fn default_require_attestation() -> bool {
true
}
impl Default for ForwardConfig {
fn default() -> Self {
Self {
require_attestation: true,
}
}
}
impl ForwardConfig {
#[must_use]
pub fn secure() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ForwardSpec {
pub local_addr: SocketAddr,
pub peer_agent: AgentId,
pub target_host: String,
pub target_port: u16,
}
impl ForwardSpec {
#[must_use]
pub fn peer_agent_hex(&self) -> String {
hex::encode(self.peer_agent.as_bytes())
}
}
struct ForwardEntry {
spec: ForwardSpec,
cancel: CancellationToken,
}
pub struct ForwardService {
agent: Arc<crate::Agent>,
policy: Arc<ConnectPolicy>,
connect_diag: Arc<ConnectDiagnostics>,
fwd_diag: Arc<ForwardDiagnostics>,
forwards: std::sync::Mutex<Vec<ForwardEntry>>,
inbound_token: CancellationToken,
inbound_permits: Arc<tokio::sync::Semaphore>,
outbound_permits: Arc<tokio::sync::Semaphore>,
per_peer: Arc<std::sync::Mutex<HashMap<AgentId, u32>>>,
revocation_set: Arc<tokio::sync::RwLock<crate::revocation::RevocationSet>>,
discovery_cache:
Arc<tokio::sync::RwLock<std::collections::HashMap<AgentId, crate::DiscoveredAgent>>>,
contact_store: Arc<tokio::sync::RwLock<crate::contacts::ContactStore>>,
require_attestation: bool,
}
struct PeerSlot {
peer: AgentId,
map: Arc<std::sync::Mutex<HashMap<AgentId, u32>>>,
}
impl Drop for PeerSlot {
fn drop(&mut self) {
if let Ok(mut m) = self.map.lock() {
if let Some(count) = m.get_mut(&self.peer) {
*count = count.saturating_sub(1);
if *count == 0 {
m.remove(&self.peer);
}
}
}
}
}
struct Admission {
_permit: tokio::sync::OwnedSemaphorePermit,
_slot: PeerSlot,
}
fn admit_to(
permits: &Arc<tokio::sync::Semaphore>,
per_peer: &Arc<std::sync::Mutex<HashMap<AgentId, u32>>>,
peer: AgentId,
) -> Option<Admission> {
let permit = permits.clone().try_acquire_owned().ok()?;
let slot = try_peer_slot(per_peer, peer)?;
Some(Admission {
_permit: permit,
_slot: slot,
})
}
fn try_peer_slot(
map: &Arc<std::sync::Mutex<HashMap<AgentId, u32>>>,
peer: AgentId,
) -> Option<PeerSlot> {
let Ok(mut m) = map.lock() else {
return None;
};
let count = m.entry(peer).or_insert(0);
if *count >= MAX_STREAMS_PER_PEER {
return None;
}
*count += 1;
Some(PeerSlot {
peer,
map: Arc::clone(map),
})
}
impl ForwardService {
#[must_use]
pub fn new(
agent: Arc<crate::Agent>,
policy: Arc<ConnectPolicy>,
connect_diag: Arc<ConnectDiagnostics>,
require_attestation: bool,
) -> Self {
let revocation_set = agent.revocation_set();
let discovery_cache = agent.identity_discovery_cache();
let contact_store = agent.contact_store();
Self {
agent,
policy,
connect_diag,
fwd_diag: Arc::new(ForwardDiagnostics::default()),
forwards: std::sync::Mutex::new(Vec::new()),
inbound_token: CancellationToken::new(),
inbound_permits: Arc::new(tokio::sync::Semaphore::new(MAX_INBOUND_STREAMS)),
outbound_permits: Arc::new(tokio::sync::Semaphore::new(MAX_OUTBOUND_STREAMS)),
per_peer: Arc::new(std::sync::Mutex::new(HashMap::new())),
revocation_set,
discovery_cache,
contact_store,
require_attestation,
}
}
#[must_use]
pub fn diagnostics(&self) -> &ForwardDiagnostics {
&self.fwd_diag
}
fn admit(&self, peer: AgentId, inbound: bool) -> Option<Admission> {
let permits = if inbound {
&self.inbound_permits
} else {
&self.outbound_permits
};
admit_to(permits, &self.per_peer, peer)
}
pub fn spawn_inbound(self: &Arc<Self>) {
let this = Arc::clone(self);
let token = self.inbound_token.clone();
tokio::spawn(async move {
tracing::info!(target: "x0x::forward", "inbound forward consumer started");
loop {
let stream = tokio::select! {
_ = token.cancelled() => break,
s = this.agent.next_incoming_stream() => match s {
Some(s) => s,
None => break,
},
};
if !matches!(
stream.protocol(),
crate::streams::StreamProtocol::ForwardV1
| crate::streams::StreamProtocol::ForwardV2
) {
tracing::debug!(
target: "x0x::forward",
protocol = ?stream.protocol(),
"non-forward inbound stream — not handled by the forwarder"
);
continue;
}
let peer_agent = stream.agent();
let admission = match this.admit(peer_agent, true) {
Some(a) => a,
None => {
this.fwd_diag.record_over_cap();
tracing::info!(
target: "x0x::forward",
agent = %hex::encode(peer_agent.as_bytes()),
"inbound forward refused: concurrency cap reached — resetting"
);
continue;
}
};
let this = Arc::clone(&this);
tokio::spawn(async move {
let _admission = admission;
this.fwd_diag.enter_stream();
let _leave = StreamLeaveGuard(Arc::clone(&this.fwd_diag));
let inbound_ctx = InboundCtx {
policy: Arc::clone(&this.policy),
connect_diag: Arc::clone(&this.connect_diag),
fwd_diag: Arc::clone(&this.fwd_diag),
revocation_set: Arc::clone(&this.revocation_set),
discovery_cache: Arc::clone(&this.discovery_cache),
contact_store: Arc::clone(&this.contact_store),
own_machine_id: this.agent.machine_id(),
require_attestation: this.require_attestation,
};
handle_inbound(stream, &inbound_ctx).await;
});
}
});
}
pub async fn add_forward(&self, spec: ForwardSpec) -> NetworkResult<SocketAddr> {
let listener = TcpListener::bind(spec.local_addr).await.map_err(|e| {
NetworkError::ConnectionFailed(format!("bind {}: {e}", spec.local_addr))
})?;
let bound = listener
.local_addr()
.map_err(|e| NetworkError::ConnectionFailed(format!("local_addr: {e}")))?;
let cancel = CancellationToken::new();
if let Ok(mut forwards) = self.forwards.lock() {
forwards.push(ForwardEntry {
spec: spec.clone(),
cancel: cancel.clone(),
});
}
let agent = Arc::clone(&self.agent);
let fwd_diag = Arc::clone(&self.fwd_diag);
let outbound_permits = Arc::clone(&self.outbound_permits);
let per_peer = Arc::clone(&self.per_peer);
let peer_agent = spec.peer_agent;
let target_host = spec.target_host;
let target_port = spec.target_port;
let require_attestation = self.require_attestation;
tokio::spawn(async move {
tracing::info!(
target: "x0x::forward",
local = %bound,
peer = %hex::encode(peer_agent.as_bytes()),
"outbound forward listener started"
);
loop {
let (tcp, _) = tokio::select! {
_ = cancel.cancelled() => break,
a = listener.accept() => match a {
Ok(a) => a,
Err(e) => {
tracing::warn!(
target: "x0x::forward",
local = %bound,
error = %e,
"accept failed; listener stopping"
);
break;
}
},
};
let admission = match admit_to(&outbound_permits, &per_peer, peer_agent) {
Some(a) => a,
None => {
fwd_diag.record_over_cap();
tracing::info!(
target: "x0x::forward",
local = %bound,
"outbound forward refused: concurrency cap reached — closing local TCP"
);
continue;
}
};
let agent = Arc::clone(&agent);
let fwd_diag = Arc::clone(&fwd_diag);
let target_host = target_host.clone();
tokio::spawn(async move {
let _admission = admission;
fwd_diag.enter_stream();
let _guard = StreamLeaveGuard(Arc::clone(&fwd_diag));
drive_outbound(
agent,
peer_agent,
target_host,
target_port,
tcp,
require_attestation,
)
.await;
});
}
});
Ok(bound)
}
#[must_use]
pub fn list_forwards(&self) -> Vec<ForwardSpec> {
self.forwards
.lock()
.map(|f| f.iter().map(|e| e.spec.clone()).collect())
.unwrap_or_default()
}
pub fn remove_forward(&self, local_addr: SocketAddr) -> bool {
let mut removed = false;
if let Ok(mut forwards) = self.forwards.lock() {
if let Some(pos) = forwards
.iter()
.position(|e| e.spec.local_addr == local_addr)
{
forwards[pos].cancel.cancel();
forwards.remove(pos);
removed = true;
}
}
removed
}
pub fn shutdown(&self) {
self.inbound_token.cancel();
if let Ok(forwards) = self.forwards.lock() {
for entry in forwards.iter() {
entry.cancel.cancel();
}
}
}
}
async fn drive_outbound(
agent: Arc<crate::Agent>,
peer_agent: AgentId,
target_host: String,
target_port: u16,
tcp: TcpStream,
require_attestation: bool,
) {
match try_outbound_v2(&agent, &peer_agent, &target_host, target_port, tcp).await {
OutboundOutcome::Done => (),
OutboundOutcome::PeerRejectedV2(tcp) => {
if require_attestation {
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer_agent.as_bytes()),
"outbound forward: peer rejected V2 and require_attestation=true — closing local TCP"
);
} else {
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer_agent.as_bytes()),
"outbound forward: peer does not support ForwardV2 — falling back to V1"
);
drive_outbound_v1(&agent, &peer_agent, &target_host, target_port, tcp).await;
}
}
}
}
enum OutboundOutcome {
Done,
PeerRejectedV2(TcpStream),
}
async fn try_outbound_v2(
agent: &Arc<crate::Agent>,
peer_agent: &AgentId,
target_host: &str,
target_port: u16,
tcp: TcpStream,
) -> OutboundOutcome {
let mut stream = match agent
.open_peer_stream(peer_agent, StreamProtocol::ForwardV2)
.await
{
Ok(s) => s,
Err(e) => {
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer_agent.as_bytes()),
error = %e,
"outbound forward v2: could not open peer stream"
);
return OutboundOutcome::PeerRejectedV2(tcp);
}
};
let mut header = ForwardV2Header::new(
target_host.to_string(),
target_port,
agent.agent_id(),
agent
.identity()
.agent_keypair()
.public_key()
.as_bytes()
.to_vec(),
stream.peer(),
);
if let Err(e) = header.sign(agent.identity().agent_keypair()) {
tracing::warn!(
target: "x0x::forward",
error = %e,
"outbound forward v2: failed to sign attestation — falling back to V1"
);
return OutboundOutcome::PeerRejectedV2(tcp);
}
if write_header_v2(stream.send_mut(), &header).await.is_err() {
return OutboundOutcome::PeerRejectedV2(tcp);
}
let mut resp = [0u8; 1];
if stream.recv_mut().read_exact(&mut resp).await.is_err() {
return OutboundOutcome::Done;
}
finish_outbound(stream, resp, peer_agent, tcp).await;
OutboundOutcome::Done
}
async fn drive_outbound_v1(
agent: &Arc<crate::Agent>,
peer_agent: &AgentId,
target_host: &str,
target_port: u16,
tcp: TcpStream,
) {
let mut stream = match agent
.open_peer_stream(peer_agent, StreamProtocol::ForwardV1)
.await
{
Ok(s) => s,
Err(e) => {
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer_agent.as_bytes()),
error = %e,
"outbound forward v1: could not open peer stream"
);
return;
}
};
let header = ForwardHeader {
target_host: target_host.to_string(),
target_port,
};
if write_header(stream.send_mut(), &header).await.is_err() {
return;
}
let mut resp = [0u8; 1];
if stream.recv_mut().read_exact(&mut resp).await.is_err() {
return;
}
finish_outbound(stream, resp, peer_agent, tcp).await;
}
async fn finish_outbound(stream: PeerStream, resp: [u8; 1], peer_agent: &AgentId, tcp: TcpStream) {
match response_connected(resp[0]) {
Some(true) => {
let (send, recv) = stream.into_split();
bridge(tcp, send, recv).await;
}
Some(false) => {
tracing::info!(
target: "x0x::forward",
peer = %hex::encode(peer_agent.as_bytes()),
"outbound forward: peer denied at connect gate — closing local TCP"
);
}
None => {
tracing::info!(
target: "x0x::forward",
"outbound forward: malformed connect response — closing local TCP"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::connect::acl::{ConnectAcl, ConnectAllowEntry};
fn header(host: &str, port: u16) -> ForwardHeader {
ForwardHeader {
target_host: host.to_string(),
target_port: port,
}
}
#[test]
fn header_frame_round_trips() {
let h = header("127.0.0.1", 22);
let bytes = h.encode();
let (decoded, n) = ForwardHeader::decode(&bytes).expect("decode");
assert_eq!(decoded, h);
assert_eq!(n, bytes.len());
}
fn h_frame_with_len(len: u32, body: &[u8]) -> Vec<u8> {
let mut out = len.to_be_bytes().to_vec();
out.extend_from_slice(body);
out
}
#[test]
fn header_decode_rejects_truncated_and_oversize() {
assert_eq!(
ForwardHeader::decode(&[0u8, 0]).unwrap_err(),
ForwardError::Truncated
);
let bytes = h_frame_with_len(10, &[]);
assert_eq!(
ForwardHeader::decode(&bytes).unwrap_err(),
ForwardError::Truncated
);
let bytes = h_frame_with_len(MAX_HEADER_BYTES + 1, &[]);
assert_eq!(
ForwardHeader::decode(&bytes).unwrap_err(),
ForwardError::Oversize(MAX_HEADER_BYTES + 1)
);
}
#[test]
fn resolve_rejects_hostnames_and_non_loopback() {
assert!(resolve_loopback_target("127.0.0.1", 22).is_ok());
assert!(resolve_loopback_target("::1", 22).is_ok());
assert_eq!(
resolve_loopback_target("localhost", 22).unwrap_err(),
ForwardError::NotLoopbackTarget("localhost".to_string())
);
assert_eq!(
resolve_loopback_target("10.0.0.1", 22).unwrap_err(),
ForwardError::NotLoopbackTarget("10.0.0.1".to_string())
);
}
fn policy_with_allow(agent: AgentId, machine: MachineId, target: SocketAddr) -> ConnectPolicy {
ConnectPolicy::Enabled(ConnectAcl {
loaded_from: "test".into(),
loaded_at_unix_ms: 0,
allow: vec![ConnectAllowEntry {
description: None,
agent_id: agent,
machine_id: machine,
targets: vec![target],
}],
})
}
#[test]
fn decide_inbound_matrix() {
let agent = AgentId([1u8; 32]);
let machine = MachineId([2u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let disabled = ConnectPolicy::default();
assert_eq!(
decide_inbound(&header("127.0.0.1", 22), &disabled, &[agent], &machine).unwrap_err(),
ConnectDenialReason::ConnectDisabled
);
let other_agent = AgentId([9u8; 32]);
let policy = policy_with_allow(other_agent, machine, target);
assert_eq!(
decide_inbound(&header("127.0.0.1", 22), &policy, &[agent], &machine).unwrap_err(),
ConnectDenialReason::AgentMachineNotInAcl
);
let policy = policy_with_allow(agent, machine, "127.0.0.1:2222".parse().unwrap());
assert_eq!(
decide_inbound(&header("127.0.0.1", 22), &policy, &[agent], &machine).unwrap_err(),
ConnectDenialReason::TargetNotAllowed
);
let policy = policy_with_allow(agent, machine, target);
assert_eq!(
decide_inbound(&header("127.0.0.1", 22), &policy, &[agent], &machine).unwrap(),
target
);
let policy = policy_with_allow(agent, machine, target);
assert_eq!(
decide_inbound(&header("10.0.0.1", 22), &policy, &[agent], &machine).unwrap_err(),
ConnectDenialReason::TargetNotLoopback
);
}
fn policy_multi(entries: Vec<ConnectAllowEntry>) -> ConnectPolicy {
ConnectPolicy::Enabled(ConnectAcl {
loaded_from: "test".into(),
loaded_at_unix_ms: 0,
allow: entries,
})
}
fn allow_entry(
agent: AgentId,
machine: MachineId,
targets: &[SocketAddr],
) -> ConnectAllowEntry {
ConnectAllowEntry {
description: None,
agent_id: agent,
machine_id: machine,
targets: targets.to_vec(),
}
}
#[test]
fn decide_inbound_multi_agent_fail_closed() {
let machine = MachineId([2u8; 32]);
let agent_a = AgentId([1u8; 32]);
let agent_b = AgentId([3u8; 32]);
let ssh: SocketAddr = "127.0.0.1:22".parse().unwrap();
let vnc: SocketAddr = "127.0.0.1:5900".parse().unwrap();
let policy = policy_multi(vec![
allow_entry(agent_a, machine, &[ssh]),
allow_entry(agent_b, machine, &[ssh]),
]);
assert_eq!(
decide_inbound(
&header("127.0.0.1", 22),
&policy,
&[agent_a, agent_b],
&machine
)
.unwrap(),
ssh,
);
let policy = policy_multi(vec![allow_entry(agent_a, machine, &[ssh])]);
assert_eq!(
decide_inbound(
&header("127.0.0.1", 22),
&policy,
&[agent_a, agent_b],
&machine
)
.unwrap_err(),
ConnectDenialReason::AgentMachineNotInAcl,
);
let policy = policy_multi(vec![
allow_entry(agent_a, machine, &[ssh]),
allow_entry(agent_b, machine, &[vnc]),
]);
assert_eq!(
decide_inbound(
&header("127.0.0.1", 22),
&policy,
&[agent_a, agent_b],
&machine
)
.unwrap_err(),
ConnectDenialReason::TargetNotAllowed,
);
let policy = policy_multi(vec![
allow_entry(agent_a, machine, &[ssh]),
allow_entry(agent_b, machine, &[ssh]),
]);
assert_eq!(
decide_inbound(
&header("127.0.0.1", 22),
&policy,
&[agent_b, agent_a],
&machine
)
.unwrap(),
ssh,
);
let policy = policy_multi(vec![allow_entry(agent_a, machine, &[ssh])]);
assert_eq!(
decide_inbound(&header("127.0.0.1", 22), &policy, &[agent_a], &machine).unwrap(),
ssh,
);
}
#[test]
fn response_frames_are_well_formed() {
let connected = encode_response_connected();
assert_eq!(response_connected(connected[0]), Some(true));
let denied = encode_response_denied(ConnectDenialReason::ConnectDisabled);
assert_eq!(denied[0], RESP_DENIED);
let len = u32::from_be_bytes([denied[1], denied[2], denied[3], denied[4]]) as usize;
assert_eq!(
denied.len(),
5 + len,
"denied frame length must match the prefix"
);
assert!(len > 0, "a serialized ConnectDenialReason is non-empty");
assert_eq!(response_connected(RESP_DENIED), Some(false));
assert_eq!(response_connected(0xFF), None);
}
#[test]
fn admit_enforces_global_and_per_peer_caps() {
let global = Arc::new(tokio::sync::Semaphore::new(2));
let per_peer: Arc<std::sync::Mutex<HashMap<AgentId, u32>>> =
Arc::new(std::sync::Mutex::new(HashMap::new()));
let peer_a = AgentId([1u8; 32]);
let peer_b = AgentId([2u8; 32]);
let a1 = admit_to(&global, &per_peer, peer_a).expect("first admit");
let a2 = admit_to(&global, &per_peer, peer_b).expect("second admit");
assert!(
admit_to(&global, &per_peer, peer_a).is_none(),
"global cap must refuse the third admit"
);
drop(a2);
let a3 = admit_to(&global, &per_peer, peer_b).expect("readmit after release");
drop(a1);
drop(a3);
let big = Arc::new(tokio::sync::Semaphore::new(1024));
let pp: Arc<std::sync::Mutex<HashMap<AgentId, u32>>> =
Arc::new(std::sync::Mutex::new(HashMap::new()));
let mut held = Vec::new();
for _ in 0..MAX_STREAMS_PER_PEER {
held.push(admit_to(&big, &pp, peer_a).expect("under per-peer cap"));
}
assert!(
admit_to(&big, &pp, peer_a).is_none(),
"per-peer cap must refuse the (N+1)th admit for the same peer"
);
assert!(
admit_to(&big, &pp, peer_b).is_some(),
"a second peer must still be admitted under its own per-peer cap"
);
held.pop();
assert!(
admit_to(&big, &pp, peer_a).is_some(),
"per-peer cap must re-admit after a slot is released"
);
}
use crate::contacts::{ContactStore, IdentityType, TrustLevel};
use crate::identity::AgentKeypair;
use std::collections::HashMap;
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
fn cache_with_agent(
keypair: &AgentKeypair,
machine: MachineId,
) -> Arc<tokio::sync::RwLock<HashMap<AgentId, crate::DiscoveredAgent>>> {
let agent_id = keypair.agent_id();
let mut cache = HashMap::new();
cache.insert(
agent_id,
crate::DiscoveredAgent {
agent_id,
machine_id: machine,
user_id: None,
addresses: Vec::new(),
announced_at: 0,
last_seen: 0,
machine_public_key: Vec::new(),
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: keypair.public_key().as_bytes().to_vec(),
},
);
Arc::new(tokio::sync::RwLock::new(cache))
}
fn trusted_store(agent: AgentId) -> Arc<tokio::sync::RwLock<ContactStore>> {
let dir = tempfile::tempdir().unwrap();
let mut store = ContactStore::new(dir.path().join("contacts.json"));
store.set_identity_type(&agent, IdentityType::Anonymous);
store.set_trust(&agent, TrustLevel::Trusted);
std::mem::forget(dir);
Arc::new(tokio::sync::RwLock::new(store))
}
fn blocked_store(agent: AgentId) -> Arc<tokio::sync::RwLock<ContactStore>> {
let dir = tempfile::tempdir().unwrap();
let mut store = ContactStore::new(dir.path().join("contacts.json"));
store.set_trust(&agent, TrustLevel::Blocked);
std::mem::forget(dir);
Arc::new(tokio::sync::RwLock::new(store))
}
fn signed_v2_header(
target: &str,
port: u16,
keypair: &AgentKeypair,
recipient: MachineId,
) -> ForwardV2Header {
let mut h = ForwardV2Header::new(
target.to_string(),
port,
keypair.agent_id(),
keypair.public_key().as_bytes().to_vec(),
recipient,
);
h.sign(keypair).expect("sign");
h
}
#[test]
fn v2_header_frame_round_trips() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let h = signed_v2_header("127.0.0.1", 22, &kp, machine);
let bytes = h.encode();
let (decoded, n) = ForwardV2Header::decode(&bytes).expect("decode");
assert_eq!(decoded, h);
assert_eq!(n, bytes.len());
}
#[test]
fn v2_header_decode_rejects_oversize() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let h = signed_v2_header("127.0.0.1", 22, &kp, machine);
let bytes = h.encode();
let mut bad = bytes.clone();
let len = (MAX_HEADER_V2_BYTES + 1).to_be_bytes();
bad[..4].copy_from_slice(&len);
assert_eq!(
ForwardV2Header::decode(&bad).unwrap_err(),
ForwardError::Oversize(MAX_HEADER_V2_BYTES + 1)
);
}
#[test]
fn v2_attestation_sign_and_verify() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let h = signed_v2_header("127.0.0.1", 22, &kp, machine);
h.verify_attestation(kp.public_key().as_bytes())
.expect("valid attestation must verify");
}
#[test]
fn v2_attestation_sign_encode_decode_and_verify() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let h = signed_v2_header("127.0.0.1", 22, &kp, machine);
let bytes = h.encode();
let (decoded, _) = ForwardV2Header::decode(&bytes).expect("decode");
let verification = decoded.verify_attestation(&decoded.opener_agent_public_key);
eprintln!("wire-round-trip verify_attestation result: {verification:?}");
assert_eq!(verification, Ok(()));
}
#[test]
fn v2_attestation_rejects_missing_signature() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let h = ForwardV2Header::new(
"127.0.0.1".to_string(),
22,
kp.agent_id(),
kp.public_key().as_bytes().to_vec(),
machine,
);
assert_eq!(
h.verify_attestation(kp.public_key().as_bytes())
.unwrap_err(),
ForwardError::AttestationMissing
);
}
#[test]
fn v2_attestation_rejects_forged_signature() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let mut h = signed_v2_header("127.0.0.1", 22, &kp, machine);
let sig_len = h.signature.len();
h.signature[sig_len / 2] ^= 0xFF;
assert!(matches!(
h.verify_attestation(kp.public_key().as_bytes())
.unwrap_err(),
ForwardError::AttestationInvalid(_)
));
}
#[test]
fn v2_attestation_rejects_wrong_key() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let h = signed_v2_header("127.0.0.1", 22, &kp, machine);
let other = AgentKeypair::generate().unwrap();
assert!(matches!(
h.verify_attestation(other.public_key().as_bytes())
.unwrap_err(),
ForwardError::AttestationKeyMismatch(_)
));
}
#[test]
fn v2_signable_bytes_bind_target_port_and_recipient() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let mut h = signed_v2_header("127.0.0.1", 22, &kp, machine);
let pubkey = kp.public_key().as_bytes().to_vec();
assert!(h.verify_attestation(&pubkey).is_ok());
h.target_port = 2222;
assert!(h.verify_attestation(&pubkey).is_err());
h.target_port = 22;
h.target_host = "::1".to_string();
assert!(h.verify_attestation(&pubkey).is_err());
h.target_host = "127.0.0.1".to_string();
h.recipient_machine_id = MachineId([9u8; 32]);
assert!(h.verify_attestation(&pubkey).is_err());
}
#[test]
fn v2_signable_bytes_are_deterministic_and_domain_separated() {
let kp = AgentKeypair::generate().unwrap();
let machine = MachineId([2u8; 32]);
let h1 = ForwardV2Header::new(
"127.0.0.1".to_string(),
22,
kp.agent_id(),
kp.public_key().as_bytes().to_vec(),
machine,
);
let h2 = ForwardV2Header::new(
"127.0.0.1".to_string(),
22,
kp.agent_id(),
kp.public_key().as_bytes().to_vec(),
machine,
);
assert_eq!(h1.signable_bytes(), h2.signable_bytes());
let h3 = ForwardV2Header::new(
"::1".to_string(),
22,
kp.agent_id(),
kp.public_key().as_bytes().to_vec(),
machine,
);
assert_ne!(h1.signable_bytes(), h3.signable_bytes());
assert!(h1
.signable_bytes()
.starts_with(FORWARD_V2_ATTESTATION_DOMAIN));
}
#[test]
fn forward_config_defaults_to_require_attestation() {
assert!(ForwardConfig::default().require_attestation);
assert!(ForwardConfig::secure().require_attestation);
}
#[tokio::test]
async fn decide_inbound_attested_matrix() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let machine = MachineId([2u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let cache = cache_with_agent(&kp, machine);
let contacts = trusted_store(agent);
let ts = now_ms();
let policy = policy_with_allow(agent, machine, target);
let hdr = signed_v2_header("127.0.0.1", 22, &kp, machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap(),
target,
);
let hdr = ForwardV2Header::new("127.0.0.1".to_string(), 22, agent, Vec::new(), machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap_err(),
ConnectDenialReason::AttestationFailed,
);
let mut hdr = signed_v2_header("127.0.0.1", 22, &kp, machine);
let _mid = hdr.signature.len() / 2;
hdr.signature[_mid] ^= 0xFF;
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap_err(),
ConnectDenialReason::AttestationFailed,
);
let stranger = AgentKeypair::generate().unwrap();
let hdr = signed_v2_header("127.0.0.1", 22, &stranger, machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap_err(),
ConnectDenialReason::AttestationFailed,
);
let policy = policy_with_allow(agent, machine, "127.0.0.1:9999".parse().unwrap());
let hdr = signed_v2_header("127.0.0.1", 22, &kp, machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap_err(),
ConnectDenialReason::TargetNotAllowed,
);
let policy = policy_with_allow(agent, machine, target);
let hdr = signed_v2_header("10.0.0.1", 22, &kp, machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap_err(),
ConnectDenialReason::TargetNotLoopback,
);
}
#[tokio::test]
async fn v2_cross_node_wire_header_verifies_then_distinct_machine_allows() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let opener_machine = MachineId([2u8; 32]);
let own_machine = MachineId([3u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let cache = cache_with_agent(&kp, opener_machine);
let contacts = trusted_store(agent);
let policy = policy_with_allow(agent, opener_machine, target);
let header = signed_v2_header("127.0.0.1", 22, &kp, own_machine);
let bytes = header.encode();
let (decoded, _) = ForwardV2Header::decode(&bytes).expect("decode");
decoded
.verify_attestation(&decoded.opener_agent_public_key)
.expect("wire-round-trip attestation must verify");
assert_eq!(
decide_inbound_attested(
&decoded,
&policy,
&opener_machine,
&AttestationVerifyCtx {
discovery_cache: cache,
contact_store: contacts,
own_machine_id: own_machine,
now_ms: now_ms(),
},
)
.await
.unwrap(),
target,
);
}
#[tokio::test]
async fn decide_inbound_attested_wrong_recipient_denied() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let opener_machine = MachineId([2u8; 32]);
let own_machine = MachineId([3u8; 32]);
let other_recipient = MachineId([9u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let cache = cache_with_agent(&kp, opener_machine);
let contacts = trusted_store(agent);
let policy = policy_with_allow(agent, opener_machine, target);
let hdr = signed_v2_header("127.0.0.1", 22, &kp, other_recipient);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&opener_machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: own_machine,
now_ms: now_ms()
}
)
.await
.unwrap_err(),
ConnectDenialReason::AttestationFailed,
);
}
#[tokio::test]
async fn decide_inbound_attested_wrong_cached_machine_denied() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let real_machine = MachineId([2u8; 32]);
let other_machine = MachineId([9u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let cache = cache_with_agent(&kp, real_machine);
let contacts = trusted_store(agent);
let policy = policy_with_allow(agent, real_machine, target);
let hdr = signed_v2_header("127.0.0.1", 22, &kp, other_machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&other_machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: other_machine,
now_ms: now_ms()
}
)
.await
.unwrap_err(),
ConnectDenialReason::AgentNotOnMachine,
);
}
#[tokio::test]
async fn decide_inbound_attested_replay_expired() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let machine = MachineId([2u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let cache = cache_with_agent(&kp, machine);
let contacts = trusted_store(agent);
let policy = policy_with_allow(agent, machine, target);
let ts = now_ms();
let mut hdr = ForwardV2Header::new("127.0.0.1".to_string(), 22, agent, Vec::new(), machine);
hdr.issued_at_ms = ts.saturating_sub(FORWARD_V2_ATTESTATION_TTL_MS + 1000);
let _sig = sign_with_ml_dsa(kp.secret_key(), &hdr.signable_bytes()).unwrap();
hdr.signature = _sig.as_bytes().to_vec();
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap_err(),
ConnectDenialReason::AttestationFailed,
);
}
#[tokio::test]
async fn decide_inbound_attested_replay_future() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let machine = MachineId([2u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let cache = cache_with_agent(&kp, machine);
let contacts = trusted_store(agent);
let policy = policy_with_allow(agent, machine, target);
let ts = now_ms();
let mut hdr = ForwardV2Header::new("127.0.0.1".to_string(), 22, agent, Vec::new(), machine);
hdr.issued_at_ms = ts + FORWARD_V2_ATTESTATION_FUTURE_SKEW_MS + 1000;
let _sig = sign_with_ml_dsa(kp.secret_key(), &hdr.signable_bytes()).unwrap();
hdr.signature = _sig.as_bytes().to_vec();
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: ts
}
)
.await
.unwrap_err(),
ConnectDenialReason::AttestationFailed,
);
}
#[tokio::test]
async fn decide_inbound_attested_blocked_agent_denied() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let machine = MachineId([2u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let cache = cache_with_agent(&kp, machine);
let contacts = blocked_store(agent);
let policy = policy_with_allow(agent, machine, target);
let hdr = signed_v2_header("127.0.0.1", 22, &kp, machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: now_ms()
}
)
.await
.unwrap_err(),
ConnectDenialReason::TrustRejected,
);
}
#[tokio::test]
async fn decide_inbound_attested_multi_agent_checks_only_attested() {
let machine = MachineId([2u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let kp_a = AgentKeypair::generate().unwrap();
let agent_a = kp_a.agent_id();
let kp_b = AgentKeypair::generate().unwrap();
let agent_b = kp_b.agent_id();
let mut cache_map = HashMap::new();
for kp in [&kp_a, &kp_b] {
let id = kp.agent_id();
cache_map.insert(
id,
crate::DiscoveredAgent {
agent_id: id,
machine_id: machine,
user_id: None,
addresses: Vec::new(),
announced_at: 0,
last_seen: 0,
machine_public_key: Vec::new(),
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: kp.public_key().as_bytes().to_vec(),
},
);
}
let cache = Arc::new(tokio::sync::RwLock::new(cache_map));
let contacts = trusted_store(agent_a);
{
let mut cs = contacts.write().await;
cs.set_identity_type(&agent_b, IdentityType::Anonymous);
cs.set_trust(&agent_b, TrustLevel::Trusted);
}
let policy = policy_multi(vec![allow_entry(agent_a, machine, &[target])]);
let hdr_a = signed_v2_header("127.0.0.1", 22, &kp_a, machine);
assert_eq!(
decide_inbound_attested(
&hdr_a,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: now_ms()
}
)
.await
.unwrap(),
target,
);
let hdr_b = signed_v2_header("127.0.0.1", 22, &kp_b, machine);
assert_eq!(
decide_inbound_attested(
&hdr_b,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: now_ms()
}
)
.await
.unwrap_err(),
ConnectDenialReason::AgentMachineNotInAcl,
);
assert_eq!(
decide_inbound(
&header("127.0.0.1", 22),
&policy,
&[agent_a, agent_b],
&machine
)
.unwrap_err(),
ConnectDenialReason::AgentMachineNotInAcl,
);
}
#[tokio::test]
async fn decide_inbound_attested_empty_cache_key_allowed_via_header() {
let kp = AgentKeypair::generate().unwrap();
let agent = kp.agent_id();
let machine = MachineId([2u8; 32]);
let target: SocketAddr = "127.0.0.1:22".parse().unwrap();
let mut cache_map = HashMap::new();
cache_map.insert(
agent,
crate::DiscoveredAgent {
agent_id: agent,
machine_id: machine,
user_id: None,
addresses: Vec::new(),
announced_at: 0,
last_seen: 0,
machine_public_key: Vec::new(),
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: Vec::new(), },
);
let cache = Arc::new(tokio::sync::RwLock::new(cache_map));
let contacts = trusted_store(agent);
let policy = policy_with_allow(agent, machine, target);
let hdr = signed_v2_header("127.0.0.1", 22, &kp, machine);
assert_eq!(
decide_inbound_attested(
&hdr,
&policy,
&machine,
&AttestationVerifyCtx {
discovery_cache: cache.clone(),
contact_store: contacts.clone(),
own_machine_id: machine,
now_ms: now_ms(),
}
)
.await
.unwrap(),
target,
);
}
}