Skip to content

feat(secret-stream): add SecretStream::into_split() for owned read/write halves#26

Open
eshork wants to merge 1 commit into
Rightbracket:mainfrom
eshork:feat/secretstream-into-split
Open

feat(secret-stream): add SecretStream::into_split() for owned read/write halves#26
eshork wants to merge 1 commit into
Rightbracket:mainfrom
eshork:feat/secretstream-into-split

Conversation

@eshork

@eshork eshork commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds SecretStream::into_split() so a caller can own the read half and write half in separate tasks:

pub fn into_split(self) -> (
    SecretStreamReadHalf<ReadHalf<T>>,
    SecretStreamWriteHalf<WriteHalf<T>>,
)

It splits the underlying transport with tokio::io::split and moves the decryptor (Pull) into SecretStreamReadHalf and the encryptor (Push) into SecretStreamWriteHalf.

Motivation

A SecretStream is inherently bidirectional, but read() and write() both take &mut self, so full-duplex use over a single stream forces either an Arc<Mutex<...>> (the reader holds the lock across read_exact, starving writes) or a single-task select!(read, outbound) driver (dropping an in-flight read() mid-frame loses bytes -> framing desync -> AEAD failure -> link reset). Owned halves eliminate both hazards: because the two halves hold disjoint cipher state, neither task can cancel the other's in-flight framed read/write. This is exactly what a mux carrying many TCP flows over one SecretStream needs.

What changed

Purely additive to peeroxide-dht/src/secret_stream.rs:

  • one import (ReadHalf, WriteHalf),
  • SecretStream::into_split(),
  • SecretStreamReadHalf<R> with read(),
  • SecretStreamWriteHalf<W> with write() and shutdown(),
  • two tests.

The half methods reuse the existing read_frame/write_frame helpers and the same Push/Pull state, so wire framing and AEAD behavior are byte-for-byte identical to the unsplit read()/write() (same 3-byte LE length prefix, same empty-keepalive skipping, same CiphertextTooShort guard). No change to the Noise handshake or the secretstream cipher.

Tests

cargo test -p peeroxide-dht passes, including two new tests: into_split_roundtrip (concurrent bidirectional bulk in separate tasks) and into_split_empty_keepalive (keepalive handling parity).

…ite halves

Adds `SecretStream::into_split(self) -> (SecretStreamReadHalf<ReadHalf<T>>,
SecretStreamWriteHalf<WriteHalf<T>>)`, splitting the underlying transport with
`tokio::io::split` and moving the decryptor (`Pull`) into the read half and the
encryptor (`Push`) into the write half.

This lets a caller drive read and write from separate tasks concurrently
without either half cancelling the other's in-flight `read_exact`/`write_all`,
which is required for full-duplex use over a single stream (e.g. multiplexing
many TCP flows). The half methods reuse the existing `read_frame`/`write_frame`
helpers and the same `Push`/`Pull` cipher state, so wire framing and AEAD
behavior are byte-for-byte identical to the unsplit `read()`/`write()`.

No change to the Noise handshake, secretstream cipher, or framing. Purely
additive: a new import, `into_split()`, two half structs, and two tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant