Stream transport boundary

Linux::Event::Stream owns byte-stream semantics: framing, output ordering, backpressure, hard queue limits, read pause, EOF, half-close, errors, and protocol transitions. It must not own TLS policy. Linux::Event::Loop continues to own only descriptor readiness.

The native transport boundary introduced in 0.100_014 separates those roles. Version 0.100_015 publishes its exact-version provider ABI. Applications now declare TLS on a Stream subclass; Stream creates the appropriate provider when the connection is acquired.

Current provider

Every ordinary Stream uses the native plain provider. It implements:

The plain path remains specialized in XS. It performs one predictable provider identity check and then issues read, write, writev, or shutdown directly. It does not make Perl method calls or pay a general callback dispatch on each syscall.

transport_name() reports plain for a live ordinary Stream. transport() returns the configured non-plain provider, if any, and is_transport_ready() reports asynchronous provider setup.

Native operation contract

Each provider operation returns a byte count plus one transport status:

| Status | Meaning | |---|---| | OK | Bytes moved successfully | | EOF | Clean transport EOF | | WANT_READ | Retry after readable readiness | | WANT_WRITE | Retry after writable readiness | | INTERRUPT | Mechanical interruption; retry immediately | | ERROR | Terminal transport failure with an error value |

The distinction between WANT_READ and WANT_WRITE is required for TLS: SSL_read may need writable readiness and SSL_write may need readable readiness. Treating both as ordinary fd EAGAIN would deadlock valid handshakes.

The provider also owns writable shutdown. This moved the final direct socket operation out of Perl and keeps end() transport-neutral.

TLS behavior

Linux::Event::TLS is the distribution's focused OpenSSL transport, not a framer and not a reactor-core feature. Application code declares its policy on the Stream type:

package GatewayStream;
use parent 'Linux::Event::Stream';
use Linux::Event::TLS
    verify => 1,              # default
    alpn   => ['http/1.1'];   # optional

package main;
my $stream = GatewayStream->connect(
    loop => $loop,                # optional: attach immediately
    host => 'gateway.discord.gg', # required
    port => 443,                  # required
    data => $state,               # optional
);

connect() selects the client TLS role and uses its host for SNI and hostname verification by default. Listener selects the server role when it accepts a TLS-declared stream_class; that class must declare cert_file and key_file, which Listener preflights during construction. Each acquisition creates a fresh stateful provider internally.

The initial provider supplies:

TLS defaults to a 10-second handshake deadline and a 5-second shutdown deadline. Zero disables either deadline. One TLS-owned timerfd watcher is created when a deadline is first needed, disarmed after handshake, reused for shutdown, and destroyed with the Stream. Ordinary plain Streams allocate no deadline fd or watcher.

Established idle, read, write, and explicit operation deadlines begin only after the provider reports ready. They are Stream policy, not provider policy, and use the Loop's shared Timer scheduler rather than the TLS deadline fd. Successful TLS plaintext reads and writes update the same optional native activity timestamps as the plain transport. Handshake control traffic does not start or reset established policy.

For an accepted TLS connection, Listener's optional on_accept callback runs after Stream construction and Loop attachment but before TLS transport readiness. Stream on_ready remains the callback for successful handshake and application-protocol readiness.

Clean peer close_notify enters ordinary EOF handling. Socket EOF without close_notify is instead a typed TLS read error. Native counters and the plain-versus-TLS benchmark expose both outcomes and the transport's handshake/read/write/shutdown activity without changing the ABI.

Application read pause must not prevent TLS control traffic needed to complete a write or shutdown. A provider may continue handshake/control processing while withholding plaintext callbacks. Any plaintext retained during pause remains subject to Stream input limits.

STARTTLS and transport replacement

Protocol replacement and transport replacement are different operations. transition_to() changes framing/callback behavior while retaining the current transport. The planned generic replace_transport() operation will change the transport while retaining Stream protocol state.

A STARTTLS boundary may already have encrypted handshake bytes in the same kernel read as the plaintext upgrade response. The replacement operation must therefore accept an explicit untouched suffix and hand it to the new transport as ciphertext, never to the old or new plaintext parser. It must validate and allocate everything before changing live state.

Transport replacement will initially require an empty plaintext output queue. That makes the boundary unambiguous: the application writes and drains the plaintext upgrade response, then installs TLS. Relaxing this rule would require per-segment transport ownership and is not justified without a real protocol that needs it.

Dependency boundary

The Linux-Event distribution builds TLS and therefore requires OpenSSL 1.1.1 or newer development files. The dependency remains mechanically isolated: Linux::Event::TLS is its own native extension, while the reactor and plain Stream extensions do not link OpenSSL. An ordinary plain Stream allocates no TLS state, calls no OpenSSL code, and retains its specialized direct-syscall path.

The common native contract is exact-versioned. Stream retains the provider object so its native operations table and OpenSSL state remain alive until connection destruction. The TLS extension includes the canonical ABI header from the Stream extension rather than carrying a second copy.