|
CX Framework
Cross-platform C utility framework
|
Data Structures | |
| struct | NetFilter |
| Socket-level factory for per-flow filters. More... | |
| struct | NetFlowFilter |
| Shared lifecycle base for all flow filters. More... | |
| struct | NetStreamFilter |
| Base for byte-stream filters (attached between the application and a stream socket) More... | |
| struct | NetDatagramFilter |
| Base for datagram filters (attached between the application and a datagram flow) More... | |
Macros | |
| #define | netfilterCreateFlow(self, type) (self)->_->createFlow(NetFilter(self), type) |
| #define | netfilterCanFilter(self, type) (self)->_->canFilter(NetFilter(self), type) |
| #define | netflowfilterNotify(self, note) NetFlowFilter_notify(NetFlowFilter(self), note) |
| #define | netflowfilterShutdown(self) (self)->_->shutdown(NetFlowFilter(self)) |
| #define | netstreamfilterNotify(self, note) NetFlowFilter_notify(NetFlowFilter(self), note) |
| #define | netstreamfilterShutdown(self) (self)->_->shutdown(NetStreamFilter(self)) |
| #define | netstreamfilterEncode(self, src) (self)->_->encode(NetStreamFilter(self), src) |
| #define | netstreamfilterDecode(self, src) (self)->_->decode(NetStreamFilter(self), src) |
| #define | netdatagramfilterNotify(self, note) NetFlowFilter_notify(NetFlowFilter(self), note) |
| #define | netdatagramfilterShutdown(self) (self)->_->shutdown(NetDatagramFilter(self)) |
| #define | netdatagramfilterEncodeMsg(self, src) (self)->_->encodeMsg(NetDatagramFilter(self), src) |
| #define | netdatagramfilterDecodeMsg(self, src) (self)->_->decodeMsg(NetDatagramFilter(self), src) |
Typedefs | |
| typedef struct NetFilter | NetFilter |
| Socket-level factory for per-flow filters. | |
| typedef struct NetFlowFilter | NetFlowFilter |
| Shared lifecycle base for all flow filters. | |
| typedef struct NetStreamFilter | NetStreamFilter |
| Base for byte-stream filters (attached between the application and a stream socket) | |
| typedef struct NetDatagramFilter | NetDatagramFilter |
| Base for datagram filters (attached between the application and a datagram flow) | |
Chainable interception hooks that transform data on its way to and from the wire.
The filter system has two levels. A NetFilter is a factory attached to a NetSocket; it creates one NetFlowFilter per flow the socket owns (one for a stream socket, one per peer for a datagram socket). The NetFlowFilter instances sit on the flow and do the actual byte or message transformation. The typical use is a TLS/DTLS session, but the same mechanism works for compression, framing, or logging.
Attach a NetFilter to a socket and it handles the rest: it creates a NetFlowFilter for every flow already open, and the socket calls it again each time a new flow opens. Applications never create or manage flow filters directly. A listening socket passes its filters on to every connection it accepts, before that connection becomes reachable, so one filter attached to a listener covers the whole server.
Both levels are plain arrays, ordered from the application toward the wire:
Element 0 is the stage closest to the application; the last element touches the wire. The common case is a chain of length one. A NetFilter holds no per-socket state of its own, so the same instance can be attached to many sockets – for example, one filter holding a TLS certificate and key, shared by every connection a server accepts.
Each stage owns its own state – a TLS session lives on the filter object, never on flow->user, which stays entirely the application's.
There is no separate handshake phase. A filter that needs to negotiate (a TLS session, say) does it entirely inside encode()/decode() (or encodeMsg()/decodeMsg() for a datagram filter): it withholds application-side output until its own logic decides the channel is ready, and may produce wire output at any time, even from empty input. That's what the priming pass is for: the driver calls encode() once with nothing queued, so a filter gets a chance to open a negotiation it initiates. A datagram flow is primed as soon as its chain is built; a stream flow is primed once the transport comes up.
A stage need not consume everything it's handed – declining to consume application input is how a filter mid-handshake blocks app data from reaching the wire; no separate gate is needed. A negative return from encode()/decode() signals a fatal failure. shutdown() begins an orderly close and always runs before a filter is freed, regardless of what phase it was in.
A stage is never re-entered concurrently: decode() runs on the worker holding the flow's claim, and encode() may be driven from any thread that calls netsocketSend(), but the flow serializes every driver pass, so a filter needs no synchronization of its own.
The framework never infers application-visible events from a filter's behavior. When a filter reaches a milestone the application should know about – a TLS session becoming ready, say – it calls netflowfilterNotify() with a NetFilterNotify code, and the driver delivers it as a NET_FilterNotify event. Only a filter with such a milestone needs to call it; a transform like compression or framing simply never does.
| #define netdatagramfilterDecodeMsg | ( | self, | |
| src | |||
| ) | (self)->_->decodeMsg(NetDatagramFilter(self), src) |
intptr netdatagramfilterDecodeMsg(NetDatagramFilter* self, NetMsgQueue* src);
Consume from src and append transformed output toward the app into decOut.
A filter still negotiating consumes its handshake records from src here (a reply may be allocated from pool and pushed onto this stage's own encOut as a side effect) while producing nothing into decOut until it decides the channel is ready – typically the same point it calls notify(NFN_Secured). A stage that reassembles several wire fragments into one application message consumes several from src and produces one into decOut; the reverse is equally valid.
| src | Input queue to consume from (the receive queue, or the next stage's decOut) |
| #define netdatagramfilterEncodeMsg | ( | self, | |
| src | |||
| ) | (self)->_->encodeMsg(NetDatagramFilter(self), src) |
intptr netdatagramfilterEncodeMsg(NetDatagramFilter* self, NetMsgQueue* src);
Consume from src and append transformed output toward the wire into encOut.
This is also where a filter drives any handshake it needs: it may append to encOut regardless of what (if anything) it consumes from src, which is how a client emits its first handshake flight – the driver calls this once right after the flow opens even with src empty, purely to give the filter the opportunity. Fragmenting one oversized message into several wire-sized ones is just appending more than one message to encOut for the one consumed from src. A filter still negotiating declines to consume application messages from src until its own logic decides the channel is ready.
Allocate any message appended to encOut (a handshake flight, or a fragment) through netpoolAllocMsg() on this stage's own pool.
| src | Input queue to consume from (the app-submitted queue, or the previous stage's encOut) |
| #define netdatagramfilterNotify | ( | self, | |
| note | |||
| ) | NetFlowFilter_notify(NetFlowFilter(self), note) |
void netdatagramfilterNotify(NetDatagramFilter* self, NetFilterNotify note);
Raise an out-of-band notification for the application, delivered by the data-plane driver.
Call this from encode()/decode() (or encodeMsg()/decodeMsg()) when the filter reaches a milestone the application should hear about – most commonly NFN_Secured, once a handshake decides the channel is ready. The driver delivers it as a NET_FilterNotify event right after the filter returns, ahead of any application data produced on the same pass. A transform with no such milestone (compression, framing) never needs to call this.
| note | The NetFilterNotify code to deliver (NFN_Secured, or a custom NFN_AppCustom+). |
| #define netdatagramfilterShutdown | ( | self | ) | (self)->_->shutdown(NetDatagramFilter(self)) |
void netdatagramfilterShutdown(NetDatagramFilter* self);
Begin an orderly close of the filter (for TLS, emit close_notify).
Called once, before the filter is freed, regardless of what phase it was in – so any close record it needs to send is produced while the wire is still available. Whatever this pushes into the boundary buffers is drained by one more encode()/encodeMsg() pass before the filter is destroyed.
| #define netfilterCanFilter | ( | self, | |
| type | |||
| ) | (self)->_->canFilter(NetFilter(self), type) |
bool netfilterCanFilter(NetFilter* self, NetSocketType type);
Reports whether this NetFilter supports attaching to a socket of the given type.
Checked once, when the filter is attached to a socket – before any flow exists and before createFlow() is ever called for it.
| type | The socket type the filter would be attached to (NST_Stream or NST_Datagram). |
type. | #define netfilterCreateFlow | ( | self, | |
| type | |||
| ) | (self)->_->createFlow(NetFilter(self), type) |
NetFlowFilter* netfilterCreateFlow(NetFilter* self, NetSocketType type);
Create a per-flow filter instance for a flow of the given socket type.
Called once per flow: immediately for every flow already open when this NetFilter is attached, and again each time the socket creates a new flow afterward. The returned NetFlowFilter takes this NetFilter's position in the flow's chain, so the flow's stages end up in the same application-to-wire order as the socket's filters. Use type to choose between building a NetStreamFilter or a NetDatagramFilter if this filter supports both; a single-type filter can ignore it. Keep a reference to this NetFilter (self) on the created flow filter if it needs shared, socket-wide configuration such as a TLS certificate and key.
| type | The socket type the new flow belongs to (NST_Stream or NST_Datagram). |
| #define netflowfilterNotify | ( | self, | |
| note | |||
| ) | NetFlowFilter_notify(NetFlowFilter(self), note) |
void netflowfilterNotify(NetFlowFilter* self, NetFilterNotify note);
Raise an out-of-band notification for the application, delivered by the data-plane driver.
Call this from encode()/decode() (or encodeMsg()/decodeMsg()) when the filter reaches a milestone the application should hear about – most commonly NFN_Secured, once a handshake decides the channel is ready. The driver delivers it as a NET_FilterNotify event right after the filter returns, ahead of any application data produced on the same pass. A transform with no such milestone (compression, framing) never needs to call this.
| note | The NetFilterNotify code to deliver (NFN_Secured, or a custom NFN_AppCustom+). |
| #define netflowfilterShutdown | ( | self | ) | (self)->_->shutdown(NetFlowFilter(self)) |
void netflowfilterShutdown(NetFlowFilter* self);
Begin an orderly close of the filter (for TLS, emit close_notify).
Called once, before the filter is freed, regardless of what phase it was in – so any close record it needs to send is produced while the wire is still available. Whatever this pushes into the boundary buffers is drained by one more encode()/encodeMsg() pass before the filter is destroyed.
| #define netstreamfilterDecode | ( | self, | |
| src | |||
| ) | (self)->_->decode(NetStreamFilter(self), src) |
intptr netstreamfilterDecode(NetStreamFilter* self, BufRing* src);
Consume from src and append transformed output toward the app into decOut.
A filter still negotiating consumes its handshake records from src here (a response may be pushed onto its own encOut as a side effect) while producing nothing into decOut until it decides the channel is ready – typically the same point it calls notify(NFN_Secured).
| src | Input ring to consume from (the receive ring, or the next stage's decOut) |
| #define netstreamfilterEncode | ( | self, | |
| src | |||
| ) | (self)->_->encode(NetStreamFilter(self), src) |
intptr netstreamfilterEncode(NetStreamFilter* self, BufRing* src);
Consume from src and append transformed output toward the wire into encOut.
This is also where a filter drives any handshake it needs: it may append to encOut regardless of what (if anything) it consumes from src, which is how a client emits its first handshake flight – the driver calls this once right after connect even with src empty, purely to give the filter the opportunity. A stage need not consume all of src; unconsumed bytes stay in the boundary ring and resume on the next pass. A filter still negotiating simply declines to consume application data until its own logic decides the channel is ready.
| src | Input ring to consume from (the staging ring, or the previous stage's encOut) |
| #define netstreamfilterNotify | ( | self, | |
| note | |||
| ) | NetFlowFilter_notify(NetFlowFilter(self), note) |
void netstreamfilterNotify(NetStreamFilter* self, NetFilterNotify note);
Raise an out-of-band notification for the application, delivered by the data-plane driver.
Call this from encode()/decode() (or encodeMsg()/decodeMsg()) when the filter reaches a milestone the application should hear about – most commonly NFN_Secured, once a handshake decides the channel is ready. The driver delivers it as a NET_FilterNotify event right after the filter returns, ahead of any application data produced on the same pass. A transform with no such milestone (compression, framing) never needs to call this.
| note | The NetFilterNotify code to deliver (NFN_Secured, or a custom NFN_AppCustom+). |
| #define netstreamfilterShutdown | ( | self | ) | (self)->_->shutdown(NetStreamFilter(self)) |
void netstreamfilterShutdown(NetStreamFilter* self);
Begin an orderly close of the filter (for TLS, emit close_notify).
Called once, before the filter is freed, regardless of what phase it was in – so any close record it needs to send is produced while the wire is still available. Whatever this pushes into the boundary buffers is drained by one more encode()/encodeMsg() pass before the filter is destroyed.
| typedef struct NetDatagramFilter NetDatagramFilter |
Base for datagram filters (attached between the application and a datagram flow)
A datagram filter transforms whole NetMessages at message boundaries – DTLS is one datagram per record. Each stage owns a pair of boundary queues, encOut/decOut, playing the same role NetStreamFilter's boundary rings play for a byte stream: a stage need not produce exactly one output per input. Consuming a handshake record and producing nothing is how a stage swallows it; producing more messages than consumed is how a stage fragments an oversized message for the wire's MTU.
Socket-level factory for per-flow filters.
A NetFilter is attached to a NetSocket and acts as a factory for NetFlowFilter instances. When attached, the socket immediately calls createFlow() for every flow already open, and calls createFlow() again each time a new flow opens. For a stream socket this produces exactly one NetFlowFilter; for a datagram socket, one per peer flow.
A NetFilter may carry socket-wide configuration shared by all of its created flow filters – TLS certificate and key material, for example – which a created flow filter accesses by keeping its own reference to self (this NetFilter), captured inside createFlow(). One instance may be attached to as many sockets as needed; each socket acquires its own reference. That is what attaching one to a listener does: every accepted connection inherits it and acquires a reference of its own, so a single filter holding a certificate and key serves the whole server.
A factory that cannot build a usable stage should still return one, in a state that fails on its first pass. Returning NULL leaves the flow unfiltered, which for a transform is harmless and for a security filter means traffic the application believes is protected going out in the clear.
| typedef struct NetFlowFilter NetFlowFilter |
Shared lifecycle base for all flow filters.
Holds the notification slot and the one lifecycle verb every concrete filter must implement besides its data-plane transform. A stage carries no chain link: the flow owns the chain as an ordered array and releases every stage when it is torn down. Not instantiable directly; use a concrete stream or datagram filter.
| typedef struct NetStreamFilter NetStreamFilter |
Base for byte-stream filters (attached between the application and a stream socket)
A stream filter transforms a byte stream in each direction. It owns two boundary ring buffers: encOut collects this stage's output toward the wire, decOut its output toward the application. The driver feeds each stage from the previous stage's boundary ring – at the ends of the chain, from the flow's staging ring going out and the socket's receive ring coming in – so a stage holding a partial framing/record unit simply produces nothing that pass, without losing the buffered remainder.