fix(core): receive path — replay window covers the loss window, zero-alloc open
Two receive-path findings from the networking audit: 1. The anti-replay window (4096 seqs) silently re-tightened the "late ≠ lost" fix: at 1 Gbps (~125k pkt/s) it spans only ~33 ms, so a Wi-Fi-retry-delayed shard the reassembler's 120 ms loss window would still use was dropped HERE first as "older than the window" — recreating the false-loss → recovery-IDR churn the time-based loss window was built to kill, exactly on the high-rate links punktfunk targets. Widened to 32768 (covers 120 ms up to ~270k pkt/s, ≈2 Gbps+); the bitmap costs 4 KiB per session and the replay-hiding bound stays finite. 2. Every received datagram still paid one Vec allocation in the AES-GCM open (and a to_vec on the plaintext probe path) — ~125k allocs/s of cross-thread allocator churn at line rate, the same class of overhead that was the documented single-core wall on the macOS receive path. New `SessionCrypto::open_in_place` (mirror of seal_in_place; GCM verifies the tag BEFORE decrypting, so a forged packet never yields plaintext) lets `poll_frame` decrypt inside the recv ring and hand the reassembler a slice. Byte-identical semantics, unit-tested against `open` incl. tamper/runt cases; criterion entry added next to seal_in_place. Tests: 94 core unit + loopback/c_abi suites green; clippy clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,31 @@ impl SessionCrypto {
|
||||
)
|
||||
.map_err(|_| PunktfunkError::Crypto)
|
||||
}
|
||||
|
||||
/// Open in place, no per-packet allocation: `buf` holds `[ciphertext .. ][tag]` on entry and
|
||||
/// the plaintext in its first `buf.len() - TAG_LEN` bytes on success (returned as the length)
|
||||
/// — byte-identical to `open`, just written in place. GCM verifies the tag *before*
|
||||
/// decrypting, so on failure `buf` still holds the ciphertext (the caller drops the packet
|
||||
/// either way). The hot-path receiver (`Session::poll_frame`) uses this to avoid the `Vec`
|
||||
/// that `open`'s convenience API allocates for every datagram at line rate — the receive
|
||||
/// mirror of [`seal_in_place`](Self::seal_in_place).
|
||||
pub fn open_in_place(&self, seq: u64, buf: &mut [u8]) -> Result<usize> {
|
||||
if buf.len() < TAG_LEN {
|
||||
return Err(PunktfunkError::BadPacket);
|
||||
}
|
||||
let nonce = nonce(self.recv_salt, seq);
|
||||
let split = buf.len() - TAG_LEN;
|
||||
let (ciphertext, tag) = buf.split_at_mut(split);
|
||||
self.cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(&nonce),
|
||||
&seq.to_be_bytes(),
|
||||
ciphertext,
|
||||
aes_gcm::Tag::from_slice(tag),
|
||||
)
|
||||
.map_err(|_| PunktfunkError::Crypto)?;
|
||||
Ok(split)
|
||||
}
|
||||
}
|
||||
|
||||
fn direction(role: Role) -> u8 {
|
||||
@@ -164,6 +189,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_in_place_matches_open_and_rejects_tampering() {
|
||||
let key = random_key();
|
||||
let salt = random_salt();
|
||||
let host = SessionCrypto::new(&key, salt, Role::Host);
|
||||
let client = SessionCrypto::new(&key, salt, Role::Client);
|
||||
for msg in [
|
||||
&b""[..],
|
||||
b"x",
|
||||
b"the quick brown fox jumps over 13 lazy dogs!!",
|
||||
] {
|
||||
let sealed = host.seal(9, msg).unwrap();
|
||||
let mut buf = sealed.clone();
|
||||
let n = client.open_in_place(9, &mut buf).unwrap();
|
||||
assert_eq!(
|
||||
&buf[..n],
|
||||
msg,
|
||||
"in-place open must be byte-identical to open"
|
||||
);
|
||||
// Wrong sequence (nonce + AAD) → authentication failure, like `open`.
|
||||
let mut buf = sealed.clone();
|
||||
assert!(client.open_in_place(8, &mut buf).is_err());
|
||||
// A flipped ciphertext/tag bit → authentication failure.
|
||||
let mut buf = sealed.clone();
|
||||
let last = buf.len() - 1;
|
||||
buf[last] ^= 1;
|
||||
assert!(client.open_in_place(9, &mut buf).is_err());
|
||||
}
|
||||
// Shorter than a tag can't be a sealed packet at all.
|
||||
let mut runt = vec![0u8; TAG_LEN - 1];
|
||||
assert!(client.open_in_place(0, &mut runt).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seal_in_place_matches_seal_and_opens() {
|
||||
let key = random_key();
|
||||
|
||||
Reference in New Issue
Block a user