Skip to main content

net_io/
gate.rs

1//! The gate seam: admission before any byte is read, a check before every write.
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::error::{CloseReason, KillReason};
7use crate::id::ConnectionId;
8use crate::info::{ConnectionInfo, GateTag, Lane};
9use crate::sys::KillSlot;
10
11/// Decides who may connect and whether bytes may still leave.
12///
13/// net-io calls `admit` after accept and before any read, `readmit` after a
14/// trusted PROXY header, `check` before every write system call, and `closed`
15/// exactly once when an admitted connection closes. A method that panics counts
16/// as a refusal or a denial (fail closed).
17///
18/// # Examples
19///
20/// ```
21/// use net_io::{Admitted, AdmitStage, CloseReason, ConnectionInfo, Denial, Gate, GateTag, Lane, Refusal, WriteCheck};
22/// use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
23///
24/// /// One global kill switch and a count of open connections.
25/// struct Switch { revoked: AtomicBool, open: AtomicU64 }
26///
27/// impl Gate for Switch {
28///     type Ticket = ();
29///     fn admit(&self, info: &ConnectionInfo, _stage: AdmitStage) -> Result<Admitted<()>, Refusal> {
30///         if self.revoked.load(Ordering::Acquire) {
31///             return Err(Refusal::Reset { code: 1 });
32///         }
33///         let lane = if info.client_addr().is_some_and(|a| a.ip().is_loopback()) { Lane(0) } else { Lane(3) };
34///         self.open.fetch_add(1, Ordering::Relaxed);
35///         Ok(Admitted { lane, ticket: (), tag: GateTag(0) })
36///     }
37///     fn readmit(&self, _ticket: &mut (), _info: &ConnectionInfo) -> Result<(), Refusal> {
38///         Ok(())
39///     }
40///     fn check(&self, _ticket: &mut (), _write: &WriteCheck) -> Result<(), Denial> {
41///         if self.revoked.load(Ordering::Acquire) { Err(Denial { code: 503 }) } else { Ok(()) }
42///     }
43///     fn closed(&self, _ticket: (), _info: &ConnectionInfo, _reason: CloseReason) {
44///         self.open.fetch_sub(1, Ordering::Relaxed);
45///     }
46/// }
47///
48/// let gate = Switch { revoked: AtomicBool::new(false), open: AtomicU64::new(0) };
49/// let info = ConnectionInfo::for_testing("127.0.0.1:9".parse().unwrap());
50/// let admitted = gate.admit(&info, AdmitStage::Accept).unwrap();
51/// assert_eq!(admitted.lane, Lane(0));
52/// ```
53pub trait Gate: Send + Sync + 'static {
54    /// Per-connection state the gate keeps; net-io stores it beside the connection.
55    type Ticket: Send + 'static;
56
57    /// After accept and before any read (`Accept`), on `Server::adopt` (`Adopt`),
58    /// or when a client connection gets a gate (`Dialed`).
59    ///
60    /// # Errors
61    ///
62    /// A [`Refusal`] closes the connection abortively without reading.
63    fn admit(
64        &self,
65        info: &ConnectionInfo,
66        stage: AdmitStage,
67    ) -> Result<Admitted<Self::Ticket>, Refusal>;
68
69    /// After a trusted PROXY header revealed the real client (`info.real_peer` is set).
70    ///
71    /// There is no default: a gate that does not care about proxies writes `Ok(())`
72    /// on purpose (decision D-060).
73    ///
74    /// # Errors
75    ///
76    /// A [`Refusal`] closes the connection abortively.
77    fn readmit(&self, ticket: &mut Self::Ticket, info: &ConnectionInfo) -> Result<(), Refusal>;
78
79    /// Before every write system call. The path where nothing is revoked should be one atomic load.
80    ///
81    /// # Errors
82    ///
83    /// A [`Denial`] wipes pending output, writes the session's denial if no byte of
84    /// the current response has left, and resets the connection.
85    fn check(&self, ticket: &mut Self::Ticket, write: &WriteCheck) -> Result<(), Denial>;
86
87    /// Exactly once per admitted connection, after it closed, with its one close reason.
88    fn closed(&self, ticket: Self::Ticket, info: &ConnectionInfo, reason: CloseReason) {
89        let _ = (ticket, info, reason);
90    }
91}
92
93/// When `Gate::admit` runs.
94///
95/// ```
96/// assert_ne!(net_io::AdmitStage::Accept, net_io::AdmitStage::Dialed);
97/// ```
98#[non_exhaustive]
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100pub enum AdmitStage {
101    /// After accept, before any read.
102    Accept,
103    /// A dialed connection adopted into a server.
104    Adopt,
105    /// A client connection that got a gate.
106    Dialed,
107}
108
109/// What admission grants.
110///
111/// ```
112/// let a = net_io::Admitted { lane: net_io::Lane(1), ticket: (), tag: net_io::GateTag(5) };
113/// assert_eq!(a.tag.0, 5);
114/// ```
115#[derive(Debug)]
116pub struct Admitted<T> {
117    /// The connection's lane.
118    pub lane: Lane,
119    /// The gate's per-connection state.
120    pub ticket: T,
121    /// The gate's opaque name for the connection, stored in `ConnectionInfo::gate`.
122    pub tag: GateTag,
123}
124
125/// How a connection is refused.
126///
127/// ```
128/// let r = net_io::Refusal::WithBytes { code: 3, bytes: b"HTTP/1.1 503 Service Unavailable\r\nRetry-After: 5\r\n\r\n" };
129/// assert_eq!(r.code(), 3);
130/// ```
131#[non_exhaustive]
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum Refusal {
134    /// Close with RST.
135    Reset {
136        /// The gate's code.
137        code: u16,
138    },
139    /// Write these bytes once (non-blocking, at most 512), then RST.
140    WithBytes {
141        /// The gate's code.
142        code: u16,
143        /// The reply.
144        bytes: &'static [u8],
145    },
146}
147
148impl Refusal {
149    /// The gate's code.
150    #[must_use]
151    pub fn code(&self) -> u16 {
152        match self {
153            Refusal::Reset { code } | Refusal::WithBytes { code, .. } => *code,
154        }
155    }
156}
157
158/// The write the gate is asked about.
159///
160/// ```
161/// use net_io::{ConnectionId, Lane, WriteCheck};
162/// let w = WriteCheck::new(ConnectionId::new(1).unwrap(), Lane(0), 512, false, None);
163/// assert_eq!(w.bytes, 512);
164/// ```
165#[non_exhaustive]
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub struct WriteCheck {
168    /// The connection.
169    pub id: ConnectionId,
170    /// Its lane.
171    pub lane: Lane,
172    /// The size of the chunk about to be written.
173    pub bytes: u32,
174    /// Whether a byte of the current response already left.
175    pub response_started: bool,
176    /// The tag the session gave `Output::begin_tagged_response`.
177    pub response: Option<GateTag>,
178}
179
180impl WriteCheck {
181    /// Builds a check (for tests of gates).
182    #[must_use]
183    pub fn new(
184        id: ConnectionId,
185        lane: Lane,
186        bytes: u32,
187        response_started: bool,
188        response: Option<GateTag>,
189    ) -> WriteCheck {
190        WriteCheck {
191            id,
192            lane,
193            bytes,
194            response_started,
195            response,
196        }
197    }
198}
199
200/// A gate's refusal of a write.
201///
202/// ```
203/// let d = net_io::Denial { code: 403 };
204/// assert_eq!(d.code, 403);
205/// ```
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub struct Denial {
208    /// The gate's code, given to `Session::on_denied`.
209    pub code: u16,
210}
211
212/// The explicit "no gate": admits every connection in `Lane(0)` with `GateTag(0)` and never denies.
213///
214/// ```
215/// use net_io::{AdmitStage, AllowAll, ConnectionInfo, Gate};
216/// let info = ConnectionInfo::for_testing("127.0.0.1:1".parse().unwrap());
217/// assert!(AllowAll.admit(&info, AdmitStage::Accept).is_ok());
218/// ```
219#[derive(Clone, Copy, Debug, Default)]
220pub struct AllowAll;
221
222impl Gate for AllowAll {
223    type Ticket = ();
224    #[inline(always)]
225    fn admit(&self, _info: &ConnectionInfo, _stage: AdmitStage) -> Result<Admitted<()>, Refusal> {
226        Ok(Admitted {
227            lane: Lane::MOST_IMPORTANT,
228            ticket: (),
229            tag: GateTag(0),
230        })
231    }
232    #[inline(always)]
233    fn readmit(&self, _ticket: &mut (), _info: &ConnectionInfo) -> Result<(), Refusal> {
234        Ok(())
235    }
236    #[inline(always)]
237    fn check(&self, _ticket: &mut (), _write: &WriteCheck) -> Result<(), Denial> {
238        Ok(())
239    }
240}
241
242/// Calls `f`, turning a panic into `None` (fail closed for gate and session hooks).
243pub(crate) fn guarded<R>(f: impl FnOnce() -> R) -> Option<R> {
244    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).ok()
245}
246
247/// A gate and one connection's ticket, type-erased for `TcpConnection`.
248pub(crate) trait ErasedGate: Send {
249    fn check(&mut self, write: &WriteCheck) -> Result<(), Denial>;
250    fn close(&mut self, info: &ConnectionInfo, reason: CloseReason);
251    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any + Send>;
252}
253
254/// The erased form of a gate plus its current ticket.
255pub(crate) struct GateCell<G: Gate> {
256    pub(crate) gate: Arc<G>,
257    pub(crate) ticket: Option<G::Ticket>,
258}
259
260impl<G: Gate> ErasedGate for GateCell<G> {
261    fn check(&mut self, write: &WriteCheck) -> Result<(), Denial> {
262        let gate = &self.gate;
263        match self.ticket.as_mut() {
264            Some(ticket) => {
265                guarded(|| gate.check(ticket, write)).unwrap_or(Err(Denial { code: 0 }))
266            }
267            None => Err(Denial { code: 0 }),
268        }
269    }
270
271    fn close(&mut self, info: &ConnectionInfo, reason: CloseReason) {
272        if let Some(ticket) = self.ticket.take() {
273            let gate = &self.gate;
274            let _ = guarded(|| gate.closed(ticket, info, reason));
275        }
276    }
277
278    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any + Send> {
279        self
280    }
281}
282
283/// Closes one connection abortively from any thread.
284///
285/// The owning thread does the close; the killer only sets a flag and wakes it,
286/// so it can never close a reused descriptor number.
287///
288/// # Examples
289///
290/// ```no_run
291/// use net_io::KillReason;
292/// let mut conn = net_io::connect("127.0.0.1:7000")?;
293/// let kill = conn.kill_handle();
294/// std::thread::spawn(move || {
295///     std::thread::sleep(std::time::Duration::from_secs(1));
296///     kill.kill(KillReason(1));
297/// });
298/// let mut buf = [0u8; 16];
299/// assert!(matches!(conn.recv(&mut buf), Err(net_io::NetError::Killed { .. })));
300/// # Ok::<(), net_io::NetError>(())
301/// ```
302#[derive(Clone)]
303pub struct KillHandle {
304    pub(crate) slot: Arc<KillSlot>,
305    pub(crate) generation: u32,
306    pub(crate) id: ConnectionId,
307}
308
309impl KillHandle {
310    /// Kills the connection; `false` if it already closed (or was already killed).
311    pub fn kill(&self, reason: KillReason) -> bool {
312        self.slot.kill(self.generation, reason)
313    }
314
315    /// The connection's id.
316    #[must_use]
317    pub fn id(&self) -> ConnectionId {
318        self.id
319    }
320
321    /// Whether the connection is still open: a snapshot, for diagnostics only.
322    #[must_use]
323    pub fn is_open(&self) -> bool {
324        self.slot.is_open(self.generation)
325    }
326}
327
328impl fmt::Debug for KillHandle {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        f.debug_struct("KillHandle")
331            .field("id", &self.id)
332            .field("open", &self.is_open())
333            .finish()
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    struct Panics;
342    impl Gate for Panics {
343        type Ticket = ();
344        fn admit(&self, _: &ConnectionInfo, _: AdmitStage) -> Result<Admitted<()>, Refusal> {
345            Err(Refusal::Reset { code: 1 })
346        }
347        fn readmit(&self, _: &mut (), _: &ConnectionInfo) -> Result<(), Refusal> {
348            Ok(())
349        }
350        #[allow(clippy::panic)]
351        fn check(&self, _: &mut (), _: &WriteCheck) -> Result<(), Denial> {
352            panic!("gate bug")
353        }
354    }
355
356    #[test]
357    fn test_gate_panic_in_check_denies() {
358        let mut cell = GateCell {
359            gate: Arc::new(Panics),
360            ticket: Some(()),
361        };
362        let w = WriteCheck::new(ConnectionId::new(1).unwrap(), Lane(0), 1, false, None);
363        let prev = std::panic::take_hook();
364        std::panic::set_hook(Box::new(|_| {}));
365        let r = cell.check(&w);
366        std::panic::set_hook(prev);
367        assert_eq!(r, Err(Denial { code: 0 }));
368    }
369
370    #[test]
371    fn test_allow_all_admits_and_allows() {
372        let info = ConnectionInfo::for_testing("127.0.0.1:1".parse().unwrap());
373        assert!(AllowAll.admit(&info, AdmitStage::Accept).is_ok());
374        let mut cell = GateCell {
375            gate: Arc::new(AllowAll),
376            ticket: Some(()),
377        };
378        let w = WriteCheck::new(info.id, Lane(0), 10, false, None);
379        assert!(cell.check(&w).is_ok());
380        cell.close(&info, CloseReason::SessionClosed);
381        assert!(cell.check(&w).is_err());
382    }
383
384    #[test]
385    fn test_refusal_code() {
386        assert_eq!(Refusal::Reset { code: 9 }.code(), 9);
387    }
388}