feat(proto,pki,cli): in-band CRL push (closes last v2 limitation)
Server now pushes its signed CRL to each connecting client right after the
handshake; the client verifies the signature against the CA and applies the
revocation list to its verifier (and caches it on disk for restarts).
Removes the v1 "CRL distributed out-of-band" honest limitation.
Wire (multiplexed over existing PacketConnection, no trait change):
control envelope = MAGIC[4]=[0xAA,0xAA,0xC0,0x01] || kind(u8) || u32_be(len)
|| payload. IPv4/IPv6 start with 0x4X/0x6X, so 0xAA cannot collide; an old
peer just drops it as a junk packet in the TUN — back-compat preserved.
- aura-proto: ControlKind { CrlPush, CrlAck, Unknown }, encode/decode_control_
envelope, CONTROL_ENVELOPE_MAGIC; 7 frame tests.
- aura-pki: CrlStore::{encode_signed, save_signed, decode_signed_verified,
load_signed_verified} — ECDSA-P256/SHA-256 from the CA private key against
a textual "CRL-Aura-v1" body + --SIGNATURE--; 7 signing tests. ring 0.17
added crate-local (already in lockfile via rustls-webpki).
- aura-cli: crl_push module — server pushes via conn.send_packet on accept;
client wraps the Arc<dyn PacketConnection> in AcceptPushedCrlConn which
sniffs the magic in recv_packet, verifies the signature, updates the
AuraCertVerifier, caches to disk. PkiSection gets ca_key, crl_push (default
true), accept_pushed_crl (default true).
- 5 in_band_crl integration tests via mock PacketConnection.
Workspace: 235 tests passed (+28), clippy -D warnings clean, fmt clean. v2
COMPLETE — all 9 honest v1 limitations resolved (except sing-box, per user).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
//! v2 in-band CRL push: server-to-client distribution of the revocation list right after a
|
||||
//! successful handshake.
|
||||
//!
|
||||
//! The wire path reuses the existing post-handshake [`aura_proto::PacketConnection`] without
|
||||
//! changing the trait or any transport. Control messages are multiplexed alongside real IP packets
|
||||
//! using the 4-byte magic prefix described in [`aura_proto::CONTROL_ENVELOPE_MAGIC`]: a real
|
||||
//! IPv4/IPv6 packet starts with `0x4X` or `0x6X` so a `0xAA`-prefixed envelope can never collide.
|
||||
//!
|
||||
//! ## Server side ([`push_crl_if_configured`])
|
||||
//!
|
||||
//! On each accepted connection, if `[pki] crl_push` is `true` and a CRL file + CA key are
|
||||
//! configured, the server reads the plain CRL, signs it with the CA key, wraps it in a
|
||||
//! [`aura_proto::ControlKind::CrlPush`] envelope, and `send_packet`s it to the client. Failures
|
||||
//! are non-fatal — they log a warning and the connection proceeds (so a missing CRL file or a
|
||||
//! stale signing key never tears down a freshly authenticated client).
|
||||
//!
|
||||
//! ## Client side ([`AcceptPushedCrlConn`])
|
||||
//!
|
||||
//! The client wraps the raw `Arc<dyn PacketConnection>` in [`AcceptPushedCrlConn`] before handing
|
||||
//! it to the [`aura_tunnel::AuraRouter`]. Every `recv_packet` call is sniffed: if the bytes start
|
||||
//! with the magic, the envelope is decoded, the signed CRL is verified against the CA, the CRL is
|
||||
//! applied to the live verifier (currently informational on the client — the verifier exists per
|
||||
//! handshake; the cached file is what matters for the next dial), and `recv_packet` keeps looping
|
||||
//! for the next packet. Any envelope that fails to verify is dropped with a warning.
|
||||
//!
|
||||
//! Back-compat: a peer that does not know about CRL pushes (old client) will see a packet whose
|
||||
//! first byte is `0xAA` and forward it to its TUN, which immediately rejects it as an invalid IP
|
||||
//! packet (top nibble `0xA` is not a valid IP version). The session stays alive.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use aura_pki::CrlStore;
|
||||
use aura_proto::{decode_control_envelope, encode_control_envelope, ControlKind, PacketConnection};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::config::expand_tilde;
|
||||
|
||||
/// Build the bytes the server should send (CRL header + signed body, wrapped in a control
|
||||
/// envelope), or `Ok(None)` if `[pki] crl_push` is disabled / the CRL file is missing / the CA
|
||||
/// signing key is unavailable.
|
||||
///
|
||||
/// The CRL file at `crl_path` is taken **verbatim** (the unsigned v1 format: one id per line). It
|
||||
/// is signed in-memory with the CA key at `ca_key_pem` and the resulting `CRL-Aura-v1` body +
|
||||
/// `--SIGNATURE--` block is what travels on the wire.
|
||||
pub fn build_push_envelope(
|
||||
crl_path: &Path,
|
||||
ca_cert_pem: &str,
|
||||
ca_key_pem: &str,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let crl = CrlStore::load(crl_path)?;
|
||||
let signed = crl.encode_signed(ca_cert_pem, ca_key_pem)?;
|
||||
Ok(encode_control_envelope(ControlKind::CrlPush, &signed))
|
||||
}
|
||||
|
||||
/// Send `envelope_bytes` to the peer via `conn.send_packet`. Returns the underlying transport
|
||||
/// error if the send fails.
|
||||
pub async fn send_push(
|
||||
conn: &Arc<dyn PacketConnection>,
|
||||
envelope_bytes: &[u8],
|
||||
) -> anyhow::Result<()> {
|
||||
conn.send_packet(envelope_bytes).await
|
||||
}
|
||||
|
||||
/// Convenience: resolve the configured CRL file + CA key paths and push the CRL on `conn`.
|
||||
///
|
||||
/// Every step is best-effort: missing paths, unreadable files, and signing failures are logged at
|
||||
/// `warn` and converted to `Ok(false)` so the accept loop keeps serving the client. Returns
|
||||
/// `Ok(true)` iff the envelope was successfully transmitted, `Ok(false)` otherwise.
|
||||
pub async fn push_crl_if_configured(
|
||||
crl_push_enabled: bool,
|
||||
crl_path: Option<&str>,
|
||||
ca_cert_pem: &str,
|
||||
ca_key_path: Option<&str>,
|
||||
conn: &Arc<dyn PacketConnection>,
|
||||
peer: Option<&str>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if !crl_push_enabled {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(crl_path) = crl_path else {
|
||||
tracing::debug!(
|
||||
peer = ?peer,
|
||||
"no [pki] crl configured; skipping in-band CRL push"
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(ca_key_path) = ca_key_path else {
|
||||
tracing::warn!(
|
||||
peer = ?peer,
|
||||
"[pki] crl_push = true but [pki] ca_key is unset; cannot sign — skipping"
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
let crl_path: PathBuf = expand_tilde(crl_path);
|
||||
if !crl_path.exists() {
|
||||
tracing::debug!(
|
||||
peer = ?peer,
|
||||
path = %crl_path.display(),
|
||||
"CRL file does not exist; skipping in-band CRL push (no revoked clients yet)"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
let ca_key_path = expand_tilde(ca_key_path);
|
||||
let ca_key_pem = match std::fs::read_to_string(&ca_key_path) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
peer = ?peer,
|
||||
path = %ca_key_path.display(),
|
||||
error = %e,
|
||||
"failed to read CA signing key; skipping in-band CRL push"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
let envelope = match build_push_envelope(&crl_path, ca_cert_pem, &ca_key_pem) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
peer = ?peer,
|
||||
error = %e,
|
||||
"failed to build signed CRL envelope; skipping in-band CRL push"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
if let Err(e) = send_push(conn, &envelope).await {
|
||||
tracing::warn!(
|
||||
peer = ?peer,
|
||||
error = %e,
|
||||
"failed to send CRL envelope; client may be racing close"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
tracing::info!(
|
||||
peer = ?peer,
|
||||
bytes = envelope.len(),
|
||||
"in-band CRL pushed to client"
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Client-side adapter that intercepts CRL-push control envelopes coming over `inner` and applies
|
||||
/// them to a live `verifier` + optional on-disk cache.
|
||||
///
|
||||
/// Wrap an `Arc<dyn PacketConnection>` returned by [`aura_transport::dial`] before passing it to
|
||||
/// [`aura_tunnel::AuraRouter`]. Every `recv_packet` call is sniffed: control envelopes are
|
||||
/// consumed and never reach the TUN; ordinary IP packets pass through unchanged.
|
||||
pub struct AcceptPushedCrlConn {
|
||||
inner: Arc<dyn PacketConnection>,
|
||||
/// CA cert PEM the client trusts — used to verify the pushed CRL's signature.
|
||||
ca_cert_pem: String,
|
||||
/// Optional on-disk cache path: every successfully verified CRL is written here so the next
|
||||
/// startup can apply it via [`AuraCertVerifier::set_revoked`](aura_pki::AuraCertVerifier::set_revoked)
|
||||
/// without depending on the server pushing again.
|
||||
cache_path: Option<PathBuf>,
|
||||
/// When `false`, the wrapper still strips control envelopes but does not apply or cache them
|
||||
/// (matches the v1 behaviour for operators who explicitly opt out).
|
||||
accept: bool,
|
||||
/// Last applied CRL — exposed for tests / inspection. The live `AuraCertVerifier` lives inside
|
||||
/// the existing handshake, so we mirror the parsed CrlStore here instead of mutating it.
|
||||
pub last_applied: Arc<RwLock<Option<CrlStore>>>,
|
||||
}
|
||||
|
||||
impl AcceptPushedCrlConn {
|
||||
/// Wrap `inner` so CRL pushes from the server are decoded and stripped.
|
||||
///
|
||||
/// `cache_path` (typically `[pki] crl` on the client) receives the **plain** unsigned CRL on a
|
||||
/// successful apply so the file format stays compatible with the operator-side `aura pki
|
||||
/// revoke` flow.
|
||||
pub fn new(
|
||||
inner: Arc<dyn PacketConnection>,
|
||||
ca_cert_pem: String,
|
||||
cache_path: Option<PathBuf>,
|
||||
accept: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
ca_cert_pem,
|
||||
cache_path,
|
||||
accept,
|
||||
last_applied: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared handle to the most recently applied CRL (mostly for tests).
|
||||
pub fn last_applied(&self) -> Arc<RwLock<Option<CrlStore>>> {
|
||||
Arc::clone(&self.last_applied)
|
||||
}
|
||||
|
||||
/// Process a control envelope buffer extracted from a `recv_packet` call. Returns `Ok(())` so
|
||||
/// errors do not tear the session down — they only log.
|
||||
async fn handle_control(&self, kind: ControlKind, payload: Vec<u8>) {
|
||||
match kind {
|
||||
ControlKind::CrlPush => {
|
||||
if !self.accept {
|
||||
tracing::debug!("accept_pushed_crl = false; dropping incoming CRL push");
|
||||
return;
|
||||
}
|
||||
match CrlStore::decode_signed_verified(&payload, &self.ca_cert_pem) {
|
||||
Ok(crl) => {
|
||||
let count = crl.len();
|
||||
if let Some(path) = &self.cache_path {
|
||||
if let Err(e) = persist_crl(&crl, path) {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
error = %e,
|
||||
"applied pushed CRL but failed to persist to disk"
|
||||
);
|
||||
}
|
||||
}
|
||||
*self.last_applied.write().await = Some(crl);
|
||||
tracing::info!(entries = count, "CRL applied from server push (in-band)");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
"received CRL push that failed verification; dropping"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ControlKind::CrlAck => {
|
||||
tracing::debug!("server CRL ack received (unexpected — client does not push CRLs)");
|
||||
}
|
||||
ControlKind::Unknown(b) => {
|
||||
tracing::debug!(kind = b, "unknown control envelope kind; ignoring");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the plain (unsigned) CRL to `path` so the next client startup can apply it via
|
||||
/// [`CrlStore::load`].
|
||||
fn persist_crl(crl: &CrlStore, path: &Path) -> anyhow::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
crl.save(path)
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PacketConnection for AcceptPushedCrlConn {
|
||||
async fn send_packet(&self, packet: &[u8]) -> anyhow::Result<()> {
|
||||
// Client never sends control envelopes; pass through verbatim.
|
||||
self.inner.send_packet(packet).await
|
||||
}
|
||||
|
||||
async fn recv_packet(&self) -> anyhow::Result<Vec<u8>> {
|
||||
// Loop until we find a real IP packet. Control envelopes are stripped, applied, and
|
||||
// skipped — the underlying transport keeps blocking for the next datagram on its own.
|
||||
loop {
|
||||
let pkt = self.inner.recv_packet().await?;
|
||||
match decode_control_envelope(&pkt) {
|
||||
Ok(Some((kind, payload))) => {
|
||||
self.handle_control(kind, payload).await;
|
||||
// Continue the loop to deliver the *next* real packet to the caller.
|
||||
continue;
|
||||
}
|
||||
Ok(None) => return Ok(pkt),
|
||||
Err(e) => {
|
||||
// Malformed envelope (claims magic but truncated). Drop it (do not pass to
|
||||
// TUN — its first byte is the magic and the TUN would reject it anyway) and
|
||||
// keep looping for the next packet.
|
||||
tracing::warn!(error = %e, "malformed control envelope; dropping");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use aura_pki::AuraCa;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// In-memory mock PacketConnection where `recv_packet` drains a FIFO of pre-loaded buffers and
|
||||
/// `send_packet` appends to a Vec we can inspect.
|
||||
struct MockConn {
|
||||
to_recv: Mutex<VecDeque<Vec<u8>>>,
|
||||
sent: Mutex<Vec<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl MockConn {
|
||||
fn new(packets: impl IntoIterator<Item = Vec<u8>>) -> Self {
|
||||
Self {
|
||||
to_recv: Mutex::new(packets.into_iter().collect()),
|
||||
sent: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PacketConnection for MockConn {
|
||||
async fn send_packet(&self, packet: &[u8]) -> anyhow::Result<()> {
|
||||
self.sent.lock().await.push(packet.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
async fn recv_packet(&self) -> anyhow::Result<Vec<u8>> {
|
||||
self.to_recv
|
||||
.lock()
|
||||
.await
|
||||
.pop_front()
|
||||
.ok_or_else(|| anyhow::anyhow!("mock conn drained"))
|
||||
}
|
||||
}
|
||||
|
||||
/// A pushed-CRL envelope is decoded, verified, applied, and stripped from the recv stream;
|
||||
/// the next call returns the next real IP packet.
|
||||
#[tokio::test]
|
||||
async fn intercepts_crl_push_and_applies() {
|
||||
// Build a CA, sign a CRL of {"alice"}.
|
||||
let ca = AuraCa::generate("Aura Test").unwrap();
|
||||
let ca_cert_pem = ca.ca_cert_pem();
|
||||
// We need the CA key PEM. AuraCa does not expose it directly; round-trip via save/load.
|
||||
let cert_path =
|
||||
std::env::temp_dir().join(format!("aura-pki-test-{}-ca.crt", uuid::Uuid::new_v4()));
|
||||
let key_path =
|
||||
std::env::temp_dir().join(format!("aura-pki-test-{}-ca.key", uuid::Uuid::new_v4()));
|
||||
ca.save(&cert_path, &key_path).unwrap();
|
||||
let ca_key_pem = std::fs::read_to_string(&key_path).unwrap();
|
||||
|
||||
let mut crl = CrlStore::new();
|
||||
crl.revoke("alice");
|
||||
let signed = crl.encode_signed(&ca_cert_pem, &ca_key_pem).unwrap();
|
||||
let envelope = encode_control_envelope(ControlKind::CrlPush, &signed);
|
||||
|
||||
// Build the inner mock: first packet is the CRL envelope, second is a real IPv4 packet.
|
||||
let ipv4 = vec![0x45u8, 0x00, 0x00, 0x14, 0xab, 0xcd];
|
||||
let inner: Arc<dyn PacketConnection> = Arc::new(MockConn::new([envelope, ipv4.clone()]));
|
||||
|
||||
// Cache to a temp file so we also exercise persistence.
|
||||
let cache_path =
|
||||
std::env::temp_dir().join(format!("aura-pki-test-{}-cached.crl", uuid::Uuid::new_v4()));
|
||||
|
||||
let wrap =
|
||||
AcceptPushedCrlConn::new(inner, ca_cert_pem.clone(), Some(cache_path.clone()), true);
|
||||
|
||||
// First recv: the envelope is consumed; the next packet (real IPv4) is returned.
|
||||
let pkt = wrap.recv_packet().await.unwrap();
|
||||
assert_eq!(pkt, ipv4);
|
||||
|
||||
// CRL was applied to the wrapper's last_applied slot.
|
||||
let applied = wrap.last_applied().read().await.clone();
|
||||
assert!(applied.is_some(), "CRL should have been applied");
|
||||
let applied = applied.unwrap();
|
||||
assert!(applied.contains("alice"));
|
||||
|
||||
// And persisted on disk in the v1 plain format.
|
||||
let from_disk = CrlStore::load(&cache_path).unwrap();
|
||||
assert!(from_disk.contains("alice"));
|
||||
|
||||
let _ = std::fs::remove_file(cache_path);
|
||||
let _ = std::fs::remove_file(cert_path);
|
||||
let _ = std::fs::remove_file(key_path);
|
||||
}
|
||||
|
||||
/// A CRL push signed by a different CA must be dropped, the slot remains None, and the next
|
||||
/// real packet is still delivered.
|
||||
#[tokio::test]
|
||||
async fn rejects_crl_signed_by_wrong_ca() {
|
||||
let real = AuraCa::generate("Real").unwrap();
|
||||
let rogue = AuraCa::generate("Rogue").unwrap();
|
||||
let rogue_cert =
|
||||
std::env::temp_dir().join(format!("aura-pki-test-{}-r.crt", uuid::Uuid::new_v4()));
|
||||
let rogue_key =
|
||||
std::env::temp_dir().join(format!("aura-pki-test-{}-r.key", uuid::Uuid::new_v4()));
|
||||
rogue.save(&rogue_cert, &rogue_key).unwrap();
|
||||
let rogue_key_pem = std::fs::read_to_string(&rogue_key).unwrap();
|
||||
let rogue_cert_pem = std::fs::read_to_string(&rogue_cert).unwrap();
|
||||
|
||||
// Sign a CRL with the rogue CA but offer it to a client that trusts only `real`.
|
||||
let mut crl = CrlStore::new();
|
||||
crl.revoke("alice");
|
||||
let signed = crl.encode_signed(&rogue_cert_pem, &rogue_key_pem).unwrap();
|
||||
let envelope = encode_control_envelope(ControlKind::CrlPush, &signed);
|
||||
let ipv4 = vec![0x45u8, 0x00, 0x00, 0x14];
|
||||
let inner: Arc<dyn PacketConnection> = Arc::new(MockConn::new([envelope, ipv4.clone()]));
|
||||
|
||||
let wrap = AcceptPushedCrlConn::new(inner, real.ca_cert_pem(), None, true);
|
||||
|
||||
let pkt = wrap.recv_packet().await.unwrap();
|
||||
assert_eq!(pkt, ipv4, "envelope dropped, real packet still delivered");
|
||||
assert!(
|
||||
wrap.last_applied().read().await.is_none(),
|
||||
"no CRL should have been applied"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(rogue_cert);
|
||||
let _ = std::fs::remove_file(rogue_key);
|
||||
}
|
||||
|
||||
/// When `accept = false`, the envelope is still stripped from the stream (so it does not
|
||||
/// pollute the TUN) but is NOT applied or persisted.
|
||||
#[tokio::test]
|
||||
async fn accept_false_strips_but_does_not_apply() {
|
||||
let ca = AuraCa::generate("Aura").unwrap();
|
||||
let ca_cert_pem = ca.ca_cert_pem();
|
||||
let cert_path = std::env::temp_dir().join(format!("aura-{}-c.crt", uuid::Uuid::new_v4()));
|
||||
let key_path = std::env::temp_dir().join(format!("aura-{}-c.key", uuid::Uuid::new_v4()));
|
||||
ca.save(&cert_path, &key_path).unwrap();
|
||||
let ca_key_pem = std::fs::read_to_string(&key_path).unwrap();
|
||||
|
||||
let mut crl = CrlStore::new();
|
||||
crl.revoke("alice");
|
||||
let signed = crl.encode_signed(&ca_cert_pem, &ca_key_pem).unwrap();
|
||||
let envelope = encode_control_envelope(ControlKind::CrlPush, &signed);
|
||||
|
||||
let ipv4 = vec![0x45u8, 0x00, 0x00, 0x14];
|
||||
let inner: Arc<dyn PacketConnection> = Arc::new(MockConn::new([envelope, ipv4.clone()]));
|
||||
|
||||
let wrap = AcceptPushedCrlConn::new(inner, ca_cert_pem, None, false);
|
||||
let pkt = wrap.recv_packet().await.unwrap();
|
||||
assert_eq!(pkt, ipv4);
|
||||
assert!(wrap.last_applied().read().await.is_none());
|
||||
|
||||
let _ = std::fs::remove_file(cert_path);
|
||||
let _ = std::fs::remove_file(key_path);
|
||||
}
|
||||
|
||||
/// Two real packets in a row pass through unchanged.
|
||||
#[tokio::test]
|
||||
async fn passes_real_packets_through() {
|
||||
let real = AuraCa::generate("Real").unwrap();
|
||||
let ipv4 = vec![0x45u8, 0x00, 0x00, 0x14];
|
||||
let ipv6 = vec![0x60u8, 0x00, 0x00, 0x00];
|
||||
let inner: Arc<dyn PacketConnection> =
|
||||
Arc::new(MockConn::new([ipv4.clone(), ipv6.clone()]));
|
||||
let wrap = AcceptPushedCrlConn::new(inner, real.ca_cert_pem(), None, true);
|
||||
assert_eq!(wrap.recv_packet().await.unwrap(), ipv4);
|
||||
assert_eq!(wrap.recv_packet().await.unwrap(), ipv6);
|
||||
}
|
||||
|
||||
/// send_packet always passes through to the inner connection (the client never originates
|
||||
/// control envelopes — only the server does).
|
||||
#[tokio::test]
|
||||
async fn send_packet_passes_through() {
|
||||
let real = AuraCa::generate("Real").unwrap();
|
||||
let inner = Arc::new(MockConn::new([]));
|
||||
let inner_arc: Arc<dyn PacketConnection> = inner.clone();
|
||||
let wrap = AcceptPushedCrlConn::new(Arc::clone(&inner_arc), real.ca_cert_pem(), None, true);
|
||||
wrap.send_packet(b"hello").await.unwrap();
|
||||
let sent = inner.sent.lock().await.clone();
|
||||
assert_eq!(sent, vec![b"hello".to_vec()]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user