perf(core): in-place AES-GCM seal + reused wire-buffer pool (host send)
ci / web (push) Failing after 39s
ci / docs-site (push) Failing after 33s
apple / swift (push) Successful in 1m16s
ci / rust (push) Successful in 1m20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 3m3s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 4m35s

The host sealed every packet with ~3 heap allocations: aes-gcm's convenience
encrypt() allocates the ciphertext Vec, seal_for_wire allocates the seq||ct||tag
wire Vec, and seal_frame allocated a fresh Vec<Vec<u8>> per frame. At line rate
(~250k–500k pkt/s for 2.5–5 Gbps) that's the single-core allocator wall.

- SessionCrypto::seal_in_place uses AeadInPlace::encrypt_in_place_detached to
  encrypt into the caller's buffer and write the detached tag at the end —
  byte-identical to seal's ciphertext||tag, no allocation (unit-tested for byte
  equality + decrypt).
- Session keeps a wire_pool the caller returns via reclaim_wires; seal_frame
  seals each packet in place into the reused buffers (clear() keeps capacity), so
  after warmup there's no per-packet ciphertext/wire allocation. paced_submit and
  submit_frame reclaim the pool after sending.

End-to-end encrypted/lossless multi-frame tests stay green (validates the pool
reuse doesn't corrupt across frames). Next: write packetize directly into a
contiguous send buffer (kills the remaining shard allocs + GSO's coalescing copy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 23:47:38 +00:00
parent 448986f41c
commit 9c86f667ca
3 changed files with 82 additions and 18 deletions
+42 -1
View File
@@ -20,7 +20,7 @@
use crate::config::Role;
use crate::error::{PunktfunkError, Result};
use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::aead::{Aead, AeadInPlace, KeyInit, Payload};
use aes_gcm::{Aes128Gcm, Key, Nonce};
/// 16-byte AEAD authentication tag appended by GCM.
@@ -60,6 +60,23 @@ impl SessionCrypto {
.map_err(|_| PunktfunkError::Crypto)
}
/// Seal in place, no per-packet allocation: `buf` is laid out as `[plaintext .. ][TAG_LEN]` (the
/// last `TAG_LEN` bytes are scratch); on return it holds `[ciphertext .. ][tag]` — byte-identical
/// to `seal`'s `ciphertext || tag`, just written in place. The hot-path sealer (`Session`) uses
/// this to avoid the `Vec` that `seal`'s convenience API allocates for every packet.
pub fn seal_in_place(&self, seq: u64, buf: &mut [u8]) -> Result<()> {
debug_assert!(buf.len() >= TAG_LEN);
let nonce = nonce(self.send_salt, seq);
let split = buf.len() - TAG_LEN;
let (plaintext, tag_slot) = buf.split_at_mut(split);
let tag = self
.cipher
.encrypt_in_place_detached(Nonce::from_slice(&nonce), &seq.to_be_bytes(), plaintext)
.map_err(|_| PunktfunkError::Crypto)?;
tag_slot.copy_from_slice(&tag);
Ok(())
}
/// Open `ciphertext || tag` for sequence `seq` (also bound as associated data).
pub fn open(&self, seq: u64, ciphertext: &[u8]) -> Result<Vec<u8>> {
let nonce = nonce(self.recv_salt, seq);
@@ -146,4 +163,28 @@ mod tests {
client.seal(0, b"abc").unwrap()
);
}
#[test]
fn seal_in_place_matches_seal_and_opens() {
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 reference = host.seal(7, msg).unwrap(); // ciphertext || tag
// In-place: [plaintext .. ][TAG_LEN scratch].
let mut buf = msg.to_vec();
buf.resize(msg.len() + TAG_LEN, 0);
host.seal_in_place(7, &mut buf).unwrap();
assert_eq!(
buf, reference,
"in-place seal must be byte-identical to seal"
);
assert_eq!(client.open(7, &buf).unwrap(), msg);
}
}
}