Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions crates/busbar-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,7 @@ pub use redacted::{constant_time_eq, sha256_hex, Redacted};
pub use scratch::{Scratch, ScratchRefused};
pub use signal::{Signal, SignalBag, SignalValue};
pub use transport::{
check_composition, CompositionError, FrameStream, Fut, Registered, Transport,
TransportConfigView, TransportMeta, TRANSPORT_ABI,
check_composition, CompositionError, FrameStream, Fut, Registered, TransportMeta, TRANSPORT_ABI,
};
pub use transport::{Carrier, Framer};
pub use unit::{
Expand All @@ -209,8 +208,8 @@ pub use unit::{
pub use wire::{
ArrivalRecord, CertFacts, CloseReason, Conn, ConnHandle, Decode, Direction, DiscardCode,
Encode, EnvelopeField, Frame, FrameCursor, FrameMeta, Framing, Handoff, HandshakeTrigger,
Listener, ListenerHandle, RawIo, RawStream, StatusAt, TransportEnvelope, TransportError,
Unit0Trigger, WireStatus, WireStatusClass,
RawIo, RawStream, StatusAt, TransportEnvelope, TransportError, Unit0Trigger, WireStatus,
WireStatusClass,
};

/// The default admin-scope CEILING for an identity provider that names none (`read-only`). Relocated
Expand Down
145 changes: 7 additions & 138 deletions crates/busbar-contract/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
//!
//! A transport cannot name a plane and cannot name a unit. It yields and writes frames, inbound
//! and outbound, and it knows no protocol and no principal. A transport is a swappable kind (#3):
//! compiled in, or dropped in over the HOT lane's `#[repr(C)]` transport decl and admitted by the same
//! signed-load pipeline as every plugin. The controls on one are that admission, review, the source
//! denylist, and the frame-honesty tests that turn red for a transport whose reported byte counts
//! inflate or deflate against what actually moved.
//! compiled in or dropped in, it is one memory-ABI framer door the connector serves over the host's
//! sockets, admitted by the same signed-load pipeline as every plugin. There is no transport trait:
//! the connector reaches a transport only through its door (step 36). The controls on one are that
//! admission, review, the source denylist, and the frame-honesty tests that turn red for a transport
//! whose reported byte counts inflate or deflate against what actually moved.

// The transport-facing vocabulary, folded in from the former `busbar-contract-transport` crate
// (DECISIONS #38): a KIND is a match-arm, not a crate, so the transport half lives here as one
Expand All @@ -29,11 +30,7 @@ pub use stack::{
Framer, FramerOut, HostTime, Located, Role, Side, TransportRow,
};

use crate::bounded::ScratchBytes;
use crate::dest::{TransportKeyHandle, VerifiedDestination};
use crate::ids::StreamId;
use crate::plugin::Plugin;
use crate::unit::{ConfigView, Refusal};
// `Frame` is the plane-facing frame, which lives one level up in the contract's own `wire` module
// (it borrows the arena the contract owns). Every other wire name below arrives via the
// `pub use wire::{…}` re-export of this module's own transport `wire` submodule.
Expand Down Expand Up @@ -102,8 +99,8 @@ pub use trust::{ClientIdentity, EgressTrust};
/// `busbar_contract::transport::<name>` resolves exactly as `busbar_contract::transport::<name>` did.
pub use wire::{
ArrivalRecord, CertFacts, CloseReason, Conn, ConnHandle, Decode, Direction, DiscardCode,
Encode, FrameMeta, Framing, Handoff, HandshakeTrigger, Listener, ListenerHandle, RawIo,
RawStream, StatusAt, TransportError, Unit0Trigger, WireStatus, WireStatusClass,
Encode, FrameMeta, Framing, Handoff, HandshakeTrigger, RawIo, RawStream, StatusAt,
TransportError, Unit0Trigger, WireStatus, WireStatusClass,
};

/// Everything a transport declares about itself.
Expand Down Expand Up @@ -175,131 +172,3 @@ pub trait TransportMeta {
selector_forms: Self::SELECTOR_FORMS,
}];
}

/// The transport's own configuration block, as a read-only view.
pub trait TransportConfigView: ConfigView {
/// The address this transport should bind to.
fn bind(&self) -> Option<&str>;
}

/// How bytes move.
///
/// Every call is asynchronous except closing, which must be able to run on a drop path.
pub trait Transport: Plugin + Send + Sync + 'static {
/// What the bottom layer knows about a connection, before any plane is chosen.
fn arrival(&self, conn: &Conn) -> ArrivalRecord;

/// Open a listening socket.
fn listen<'a>(
&'a self,
cfg: &'a dyn TransportConfigView,
keys: &'a TransportKeyHandle,
) -> Fut<'a, Listener>;

/// Take the next connection off a listener.
fn accept<'a>(&'a self, l: &'a Listener) -> Fut<'a, Conn>;

/// Dial a verified destination.
fn dial<'a>(
&'a self,
dest: &'a VerifiedDestination,
keys: &'a TransportKeyHandle,
) -> Fut<'a, Conn>;

/// The frame pump for one connection.
///
/// This takes one clone of the connection handle; writing, closing and upgrading take another.
fn frames(&self, conn: Conn) -> FrameStream;

/// Queue bytes for one stream, returning how many were queued.
///
/// The bytes are copied into a per-connection slab, because the arena they came from is reset
/// as soon as the frame is queued.
fn write<'a>(
&'a self,
conn: &'a Conn,
stream: StreamId,
bytes: ScratchBytes<'a>,
) -> Fut<'a, usize>;

/// Render an outbound envelope and body as this transport's own wire bytes.
///
/// The byte layout of an envelope belongs to the transport, and only to the transport: an
/// `http` request line and folded headers, a gRPC length-prefixed message, a WebSocket payload
/// are three different objects that no neutral layout describes. The egress unit used to write
/// one anyway — `name: value`, a blank line, the body — because it must run the lane
/// cross-check over the same bytes it hands to `write`, and had nothing else to run it over.
/// That made the check honest about ONE buffer and wrong about which bytes were in it.
///
/// The fields arrive POST-DECORATION: what the egress-auth unit added, and what it substituted
/// a secret into, are already here. The bytes that come back are the bytes the cross-check
/// reads and the bytes `write` is given, which is what the design means by the envelope still
/// equalling the verified destination after decoration.
///
/// Into the arena, because the hot path allocates nowhere else.
///
/// # Errors
///
/// The arena had no room, or the envelope names something this transport cannot express.
fn encode_envelope<'a>(
&self,
fields: &[(&str, &[u8])],
body: &[u8],
arena: &'a dyn crate::bounded::PlaneAlloc,
) -> Result<crate::bounded::ScratchBytes<'a>, crate::wire::Encode>;

/// Adopt a connection a lower layer is handing up, becoming the new top of the stack.
///
/// The upgrade belongs to the TARGET, not the source. The connection that comes out belongs to
/// this transport's registry, and only this transport can put it there — which is why the
/// source could never express the upgrade as a method of its own: it would have had to return a
/// handle it has no way to build. Here the target asks the source for its stream through
/// [`Transport::detach`], and what it gets back is a stream the source has already given up.
///
/// A source this transport does not compose over is refused with
/// [`TransportError::HandoffMismatch`], and so is a stream that is not the shape this layer can
/// adopt: an upgrade neither leg declared is not one the session may continue on.
fn adopt<'a>(
&'a self,
from: &'a dyn Transport,
conn: Conn,
keys: &'a TransportKeyHandle,
) -> Fut<'a, Conn>;

/// Give up the byte stream under a connection, for the layer adopting it.
///
/// Removes the connection from this transport's own registry, so it is never read from or
/// written to here again. `None` when the connection is unknown, when a reader still holds it
/// (an upgrade never races an in-flight read, by the at-most-one-upgrade-in-flight rule; a
/// caller that violates that ordering sees `None` rather than a torn stream), or when this
/// transport has no raw stream to give — which every implementor that is a bottom layer or
/// hands nothing up must still answer, explicitly, as `None`.
fn detach(&self, conn: &Conn) -> Option<crate::wire::RawStream>;

/// The layer this instance was actually built over, where it was built over one.
///
/// What the registry's boot check compares against `COMPOSES_OVER`. A transport that opens its
/// own socket answers `None` and is checked only on what it declares; a composed one names the
/// layer under it, and a name that is not in its declaration refuses the boot.
fn composed_over(&self) -> Option<&'static str>;

/// Close a connection.
fn close(&self, conn: Conn, reason: CloseReason);

/// Write a refusal for bytes that never reached a plane.
///
/// This is the pre-decode path: no plane is known yet, so the kernel renders the refusal
/// through the transport's own generic envelope rather than through a dialect.
///
/// `stream` names how much of the connection the refusal is about. On a transport whose first
/// unit opens per stream, a refusal is one stream's — the other streams on that connection
/// belong to other units, which have done nothing wrong and must go on to complete. `None` is
/// the whole connection, which is the only honest reading on a transport that carries one.
fn unit0_refusal<'a>(
&'a self,
conn: Conn,
stream: Option<StreamId>,
refusal: &'a Refusal,
bytes: ScratchBytes<'a>,
) -> Fut<'a, ()>;
}
57 changes: 0 additions & 57 deletions crates/busbar-contract/src/transport/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use core::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

/// Which way bytes are moving.
Expand Down Expand Up @@ -389,62 +388,6 @@ pub struct Handoff {
pub binding_fact: &'static str,
}

/// A listening socket a transport opened.
///
/// The kernel holds it; a plane never sees one. The handle is opaque because what is behind it is
/// the transport's business and nothing else's.
///
/// The identity is the contract's, minted here at construction and never by a transport, because a
/// node always holds at least two listeners — data and admin — and the only thing that told them
/// apart was a bound address string a configuration is free to duplicate. A transport keying its
/// own per-listener state on that string was keying it on something two listeners could share.
#[derive(Clone)]
pub struct Listener {
id: u64,
handle: Arc<dyn ListenerHandle>,
}

/// The next listener identity. Node-local and monotonic; nothing outside the node ever sees one.
static NEXT_LISTENER_ID: AtomicU64 = AtomicU64::new(1);

impl Listener {
/// Wrap a transport's own listener, giving it an identity nothing else holds.
#[must_use]
pub fn new(handle: Arc<dyn ListenerHandle>) -> Self {
Self {
id: NEXT_LISTENER_ID.fetch_add(1, Ordering::Relaxed),
handle,
}
}

/// The listener's node-local identity.
#[must_use]
pub fn id(&self) -> u64 {
self.id
}

/// The local address the listener is bound to.
#[must_use]
pub fn local_addr(&self) -> String {
self.handle.local_addr()
}
}

impl fmt::Debug for Listener {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Listener")
.field("id", &self.id)
.field("local_addr", &self.handle.local_addr())
.finish()
}
}

/// What a transport puts behind a [`Listener`].
pub trait ListenerHandle: Send + Sync {
/// The local address the listener is bound to.
fn local_addr(&self) -> String;
}

/// A connection a transport accepted or dialled.
///
/// The type index calls this a cloneable, reference-counted handle: the frame pump takes one
Expand Down
4 changes: 2 additions & 2 deletions crates/busbar-contract/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

pub use crate::transport::wire::{
ArrivalRecord, CertFacts, CloseReason, Conn, ConnHandle, Decode, Direction, DiscardCode,
Encode, FrameMeta, Framing, Handoff, HandshakeTrigger, Listener, ListenerHandle, RawIo,
RawStream, StatusAt, TransportError, Unit0Trigger, WireStatus, WireStatusClass,
Encode, FrameMeta, Framing, Handoff, HandshakeTrigger, RawIo, RawStream, StatusAt,
TransportError, Unit0Trigger, WireStatus, WireStatusClass,
};

use crate::bounded::{BoundedVec, ScratchBytes, SlabBytes, MAX_CURSOR_BYTES, MAX_KEYS};
Expand Down
40 changes: 4 additions & 36 deletions crates/busbar-contract/tests/handles_and_handoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,19 @@

//! The opaque handles, and the one value that crosses an in-band upgrade.
//!
//! A listener, a connection and a detached stream are all "a handle plus the few facts the layer
//! above is allowed to read". Every one of those facts was reachable and none of them was read back
//! by a test: the bound address a listener reports, the peer a connection names, and — the one that
//! A connection and a detached stream are each "a handle plus the few facts the layer above is
//! allowed to read". Every one of those facts was reachable and none of them was read back by a
//! test: the peer a connection names, and — the one that
//! decides whether a session happens at all — the layer a detached stream says handed it up. A
//! handoff is only admissible between the layers that declared it, so a stream that forgot which
//! layer made it is a session adopted by whoever dialled it.
//!
//! The `Debug` renderings are proofs too, not decoration: they are what a transport author reads off
//! a log line while an upgrade is failing, and each of these types hand-rolls one.

use busbar_contract::transport::{Conn, ConnHandle, Listener, ListenerHandle, RawStream};
use busbar_contract::transport::{Conn, ConnHandle, RawStream};
use std::sync::Arc;

struct Bound(&'static str);
impl ListenerHandle for Bound {
fn local_addr(&self) -> String {
self.0.to_string()
}
}

struct Socket {
id: u64,
peer: &'static str,
Expand All @@ -36,31 +29,6 @@ impl ConnHandle for Socket {
}
}

#[test]
fn a_listener_reports_the_address_its_own_handle_is_bound_to() {
// The address comes from the transport's handle, not from the contract: a listener that
// answered with something of its own would have the kernel reporting a socket nobody is on.
let listener = Listener::new(Arc::new(Bound("127.0.0.1:8443")));
assert_eq!(listener.local_addr(), "127.0.0.1:8443");
let rendered = format!("{listener:?}");
assert!(rendered.contains("127.0.0.1:8443"), "{rendered}");
assert!(rendered.contains("Listener"), "{rendered}");
}

#[test]
fn listener_identities_are_minted_here_and_never_repeat() {
// Two listeners on one address are still two listeners, and the identity is what tells them
// apart. Minted at construction, monotonic, and never handed to a transport to choose.
let first = Listener::new(Arc::new(Bound("0.0.0.0:0")));
let second = Listener::new(Arc::new(Bound("0.0.0.0:0")));
assert!(
second.id() > first.id(),
"identities go forward: {} then {}",
first.id(),
second.id()
);
}

#[test]
fn a_connection_answers_with_its_own_handles_identity_and_peer() {
let conn = Conn::new(Arc::new(Socket {
Expand Down
Loading