CX Framework
Cross-platform C utility framework
Loading...
Searching...
No Matches
filter.cxh
1#include <cx/net/net_shared.h>
2#include <cx/net/pool.cxh>
3#include <cx/buffer/bufring.h>
4
5/// @addtogroup net_filter
6/// @{
7/// Chainable interception hooks that transform data on its way to and from the wire.
8///
9/// The filter system has two levels. A **NetFilter** is a factory attached to a **NetSocket**;
10/// it creates one **NetFlowFilter** per flow the socket owns (one for a stream socket, one per
11/// peer for a datagram socket). The NetFlowFilter instances sit on the flow and do the actual
12/// byte or message transformation. The typical use is a TLS/DTLS session, but the same mechanism
13/// works for compression, framing, or logging.
14///
15/// @section net_filter_model Mental model
16///
17/// Attach a NetFilter to a socket and it handles the rest: it creates a NetFlowFilter for every
18/// flow already open, and the socket calls it again each time a new flow opens. Applications never
19/// create or manage flow filters directly. A listening socket passes its filters on to every
20/// connection it accepts, before that connection becomes reachable, so one filter attached to a
21/// listener covers the whole server.
22///
23/// Both levels are plain arrays, ordered from the application toward the wire:
24///
25/// @code
26/// app <-> sock->filters.a[0] <-> ... <-> sock->filters.a[N-1] <-> wire (factories)
27/// app <-> flow->filters.a[0] <-> ... <-> flow->filters.a[N-1] <-> wire (per-flow stages)
28/// @endcode
29///
30/// Element 0 is the stage closest to the application; the last element touches the wire. The
31/// common case is a chain of length one. A NetFilter holds no per-socket state of its own, so the
32/// same instance can be attached to many sockets -- for example, one filter holding a TLS
33/// certificate and key, shared by every connection a server accepts.
34///
35/// Each stage owns its own state -- a TLS session lives on the filter object, never on
36/// `flow->user`, which stays entirely the application's.
37///
38/// @section net_filter_lifecycle Lifecycle
39///
40/// There is no separate handshake phase. A filter that needs to negotiate (a TLS session, say)
41/// does it entirely inside encode()/decode() (or encodeMsg()/decodeMsg() for a datagram filter):
42/// it withholds application-side output until its own logic decides the channel is ready, and may
43/// produce wire output at any time, even from empty input. That's what the **priming pass** is
44/// for: the driver calls encode() once with nothing queued, so a filter gets a chance to open a
45/// negotiation it initiates. A datagram flow is primed as soon as its chain is built; a stream
46/// flow is primed once the transport comes up.
47///
48/// A stage need not consume everything it's handed -- declining to consume application input is
49/// how a filter mid-handshake blocks app data from reaching the wire; no separate gate is needed.
50/// A negative return from encode()/decode() signals a fatal failure. shutdown() begins an orderly
51/// close and always runs before a filter is freed, regardless of what phase it was in.
52///
53/// A stage is never re-entered concurrently: decode() runs on the worker holding the flow's claim,
54/// and encode() may be driven from any thread that calls netsocketSend(), but the flow serializes
55/// every driver pass, so a filter needs no synchronization of its own.
56///
57/// @section net_filter_signal Out-of-band notifications
58///
59/// The framework never infers application-visible events from a filter's behavior. When a filter
60/// reaches a milestone the application should know about -- a TLS session becoming ready, say --
61/// it calls netflowfilterNotify() with a NetFilterNotify code, and the driver delivers it as a
62/// NET_FilterNotify event. Only a filter with such a milestone needs to call it; a transform like
63/// compression or framing simply never does.
64
65/// @brief Socket-level factory for per-flow filters
66///
67/// A NetFilter is attached to a NetSocket and acts as a factory for NetFlowFilter instances.
68/// When attached, the socket immediately calls createFlow() for every flow already open, and
69/// calls createFlow() again each time a new flow opens. For a stream socket this produces
70/// exactly one NetFlowFilter; for a datagram socket, one per peer flow.
71///
72/// A NetFilter may carry socket-wide configuration shared by all of its created flow filters --
73/// TLS certificate and key material, for example -- which a created flow filter accesses by
74/// keeping its own reference to `self` (this NetFilter), captured inside createFlow(). One
75/// instance may be attached to as many sockets as needed; each socket acquires its own reference.
76/// That is what attaching one to a listener does: every accepted connection inherits it and
77/// acquires a reference of its own, so a single filter holding a certificate and key serves the
78/// whole server.
79///
80/// A factory that cannot build a usable stage should still return one, in a state that fails on its
81/// first pass. Returning NULL leaves the flow unfiltered, which for a transform is harmless and for
82/// a security filter means traffic the application believes is protected going out in the clear.
83abstract class NetFilter
84{
85 /// Create a per-flow filter instance for a flow of the given socket type.
86 ///
87 /// Called once per flow: immediately for every flow already open when this NetFilter is
88 /// attached, and again each time the socket creates a new flow afterward. The returned
89 /// NetFlowFilter takes this NetFilter's position in the flow's chain, so the flow's stages
90 /// end up in the same application-to-wire order as the socket's filters. Use `type` to
91 /// choose between building a NetStreamFilter or a NetDatagramFilter if this filter supports
92 /// both; a single-type filter can ignore it. Keep a reference to this NetFilter (`self`) on
93 /// the created flow filter if it needs shared, socket-wide configuration such as a TLS
94 /// certificate and key.
95 ///
96 /// @param type The socket type the new flow belongs to (NST_Stream or NST_Datagram).
97 /// @return A new NetFlowFilter to attach to the flow, or NULL to leave the flow unfiltered.
98 [abstract] NetFlowFilter* createFlow(NetSocketType type);
99
100 /// Reports whether this NetFilter supports attaching to a socket of the given type.
101 ///
102 /// Checked once, when the filter is attached to a socket -- before any flow exists and
103 /// before createFlow() is ever called for it.
104 ///
105 /// @param type The socket type the filter would be attached to (NST_Stream or NST_Datagram).
106 /// @return true if this filter can be attached to a socket of `type`.
107 [abstract] bool canFilter(NetSocketType type);
108}
109
110/// @brief Shared lifecycle base for all flow filters
111///
112/// Holds the notification slot and the one lifecycle verb every concrete filter must implement
113/// besides its data-plane transform. A stage carries no chain link: the flow owns the chain as an
114/// ordered array and releases every stage when it is torn down. Not instantiable directly; use a
115/// concrete stream or datagram filter.
116abstract class NetFlowFilter
117{
118 // Pending out-of-band notification, or NFN_None. Written by notify() and cleared by the
119 // data-plane driver once it delivers the matching NET_FilterNotify event; concrete filters
120 // never read or write this directly.
121 NetFilterNotify pendingNotify;
122
123 /// Raise an out-of-band notification for the application, delivered by the data-plane driver.
124 ///
125 /// Call this from encode()/decode() (or encodeMsg()/decodeMsg()) when the filter reaches a
126 /// milestone the application should hear about -- most commonly NFN_Secured, once a handshake
127 /// decides the channel is ready. The driver delivers it as a NET_FilterNotify event right
128 /// after the filter returns, ahead of any application data produced on the same pass. A
129 /// transform with no such milestone (compression, framing) never needs to call this.
130 ///
131 /// @param note The NetFilterNotify code to deliver (NFN_Secured, or a custom NFN_AppCustom+).
132 unbound void notify(NetFilterNotify note);
133
134 /// Begin an orderly close of the filter (for TLS, emit close_notify).
135 ///
136 /// Called once, before the filter is freed, regardless of what phase it was in -- so any
137 /// close record it needs to send is produced while the wire is still available. Whatever this
138 /// pushes into the boundary buffers is drained by one more encode()/encodeMsg() pass before
139 /// the filter is destroyed.
140 [abstract] void shutdown();
141}
142
143/// @brief Base for byte-stream filters (attached between the application and a stream socket)
144///
145/// A stream filter transforms a byte stream in each direction. It owns two boundary ring buffers:
146/// `encOut` collects this stage's output toward the wire, `decOut` its output toward the
147/// application. The driver feeds each stage from the previous stage's boundary ring -- at the ends
148/// of the chain, from the flow's staging ring going out and the socket's receive ring coming in --
149/// so a stage holding a partial framing/record unit simply produces nothing that pass, without
150/// losing the buffered remainder.
151abstract class NetStreamFilter extends NetFlowFilter
152{
153 BufRing encOut; ///< This stage's output toward the wire (a chain boundary buffer)
154 BufRing decOut; ///< This stage's output toward the app (a chain boundary buffer)
155
156 init(); // allocates encOut / decOut
157 destroy(); // tears down encOut / decOut
158
159 /// Consume from `src` and append transformed output toward the wire into `encOut`.
160 ///
161 /// This is also where a filter drives any handshake it needs: it may append to `encOut`
162 /// regardless of what (if anything) it consumes from `src`, which is how a client emits its
163 /// first handshake flight -- the driver calls this once right after connect even with `src`
164 /// empty, purely to give the filter the opportunity. A stage need not consume all of `src`;
165 /// unconsumed bytes stay in the boundary ring and resume on the next pass. A filter still
166 /// negotiating simply declines to consume application data until its own logic decides the
167 /// channel is ready.
168 ///
169 /// @param src Input ring to consume from (the staging ring, or the previous stage's encOut)
170 /// @return Bytes produced into encOut, or negative on a fatal transform error.
171 [abstract] intptr encode(BufRing *src);
172
173 /// Consume from `src` and append transformed output toward the app into `decOut`.
174 ///
175 /// A filter still negotiating consumes its handshake records from `src` here (a response may
176 /// be pushed onto its own `encOut` as a side effect) while producing nothing into `decOut`
177 /// until it decides the channel is ready -- typically the same point it calls
178 /// notify(NFN_Secured).
179 ///
180 /// @param src Input ring to consume from (the receive ring, or the next stage's decOut)
181 /// @return Bytes produced into decOut, or negative on a fatal transform error.
182 [abstract] intptr decode(BufRing *src);
183}
184
185/// @brief Base for datagram filters (attached between the application and a datagram flow)
186///
187/// A datagram filter transforms whole NetMessages at message boundaries -- DTLS is one datagram
188/// per record. Each stage owns a pair of boundary queues, `encOut`/`decOut`, playing the same role
189/// `NetStreamFilter`'s boundary rings play for a byte stream: a stage need not produce exactly one
190/// output per input. Consuming a handshake record and producing nothing is how a stage swallows
191/// it; producing more messages than consumed is how a stage fragments an oversized message for the
192/// wire's MTU.
193abstract class NetDatagramFilter extends NetFlowFilter
194{
195 NetMsgQueue encOut; ///< This stage's output toward the wire (a chain boundary queue)
196 NetMsgQueue decOut; ///< This stage's output toward the app (a chain boundary queue)
197
198 /// @brief The pool this stage allocates its output from, held strongly
199 ///
200 /// Set by the framework when the stage is added to a flow's chain; a concrete filter neither
201 /// sets nor releases it, and simply uses it to allocate messages for `encOut`/`decOut`.
202 /// See @ref net_pool.
203 object[NetPool] pool;
204
205 destroy(); // releases any messages still queued in encOut / decOut back to their pool
206
207 /// Consume from `src` and append transformed output toward the wire into `encOut`.
208 ///
209 /// This is also where a filter drives any handshake it needs: it may append to `encOut`
210 /// regardless of what (if anything) it consumes from `src`, which is how a client emits its
211 /// first handshake flight -- the driver calls this once right after the flow opens even with
212 /// `src` empty, purely to give the filter the opportunity. Fragmenting one oversized message
213 /// into several wire-sized ones is just appending more than one message to `encOut` for the
214 /// one consumed from `src`. A filter still negotiating declines to consume application
215 /// messages from `src` until its own logic decides the channel is ready.
216 ///
217 /// Allocate any message appended to `encOut` (a handshake flight, or a fragment) through
218 /// netpoolAllocMsg() on this stage's own `pool`.
219 ///
220 /// @param src Input queue to consume from (the app-submitted queue, or the previous stage's
221 /// encOut)
222 /// @return Messages produced into encOut, or negative on a fatal transform error.
223 [abstract] intptr encodeMsg(NetMsgQueue *src);
224
225 /// Consume from `src` and append transformed output toward the app into `decOut`.
226 ///
227 /// A filter still negotiating consumes its handshake records from `src` here (a reply may be
228 /// allocated from `pool` and pushed onto this stage's own `encOut` as a side effect) while
229 /// producing nothing into `decOut` until it decides the channel is ready -- typically the same
230 /// point it calls notify(NFN_Secured). A stage that reassembles several wire fragments into one
231 /// application message consumes several from `src` and produces one into `decOut`; the reverse
232 /// is equally valid.
233 ///
234 /// @param src Input queue to consume from (the receive queue, or the next stage's decOut)
235 /// @return Messages produced into decOut, or negative on a fatal transform error.
236 [abstract] intptr decodeMsg(NetMsgQueue *src);
237}
238
239/// @}