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    /// `Gate::readmit` with the current ticket (a panic refuses).
251    #[cfg_attr(not(feature = "proxy"), allow(dead_code))]
252    fn readmit(&mut self, info: &ConnectionInfo) -> Result<(), Refusal>;
253    fn close(&mut self, info: &ConnectionInfo, reason: CloseReason);
254    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any + Send>;
255}
256
257/// The erased form of a gate plus its current ticket.
258pub(crate) struct GateCell<G: Gate> {
259    pub(crate) gate: Arc<G>,
260    pub(crate) ticket: Option<G::Ticket>,
261}
262
263impl<G: Gate> ErasedGate for GateCell<G> {
264    fn check(&mut self, write: &WriteCheck) -> Result<(), Denial> {
265        let gate = &self.gate;
266        match self.ticket.as_mut() {
267            Some(ticket) => {
268                guarded(|| gate.check(ticket, write)).unwrap_or(Err(Denial { code: 0 }))
269            }
270            None => Err(Denial { code: 0 }),
271        }
272    }
273
274    fn readmit(&mut self, info: &ConnectionInfo) -> Result<(), Refusal> {
275        let gate = &self.gate;
276        match self.ticket.as_mut() {
277            Some(ticket) => {
278                guarded(|| gate.readmit(ticket, info)).unwrap_or(Err(Refusal::Reset { code: 0 }))
279            }
280            None => Err(Refusal::Reset { code: 0 }),
281        }
282    }
283
284    fn close(&mut self, info: &ConnectionInfo, reason: CloseReason) {
285        if let Some(ticket) = self.ticket.take() {
286            let gate = &self.gate;
287            let _ = guarded(|| gate.closed(ticket, info, reason));
288        }
289    }
290
291    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any + Send> {
292        self
293    }
294}
295
296/// Closes one connection abortively from any thread.
297///
298/// The owning thread does the close; the killer only sets a flag and wakes it,
299/// so it can never close a reused descriptor number.
300///
301/// # Examples
302///
303/// ```no_run
304/// use net_io::KillReason;
305/// let mut conn = net_io::connect("127.0.0.1:7000")?;
306/// let kill = conn.kill_handle();
307/// std::thread::spawn(move || {
308///     std::thread::sleep(std::time::Duration::from_secs(1));
309///     kill.kill(KillReason(1));
310/// });
311/// let mut buf = [0u8; 16];
312/// assert!(matches!(conn.recv(&mut buf), Err(net_io::NetError::Killed { .. })));
313/// # Ok::<(), net_io::NetError>(())
314/// ```
315#[derive(Clone)]
316pub struct KillHandle {
317    pub(crate) slot: Arc<KillSlot>,
318    pub(crate) generation: u32,
319    pub(crate) id: ConnectionId,
320}
321
322impl KillHandle {
323    /// Kills the connection; `false` if it already closed (or was already killed).
324    pub fn kill(&self, reason: KillReason) -> bool {
325        self.slot.kill(self.generation, reason)
326    }
327
328    /// The connection's id.
329    #[must_use]
330    pub fn id(&self) -> ConnectionId {
331        self.id
332    }
333
334    /// Whether the connection is still open: a snapshot, for diagnostics only.
335    #[must_use]
336    pub fn is_open(&self) -> bool {
337        self.slot.is_open(self.generation)
338    }
339}
340
341impl fmt::Debug for KillHandle {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        f.debug_struct("KillHandle")
344            .field("id", &self.id)
345            .field("open", &self.is_open())
346            .finish()
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    struct Panics;
355    impl Gate for Panics {
356        type Ticket = ();
357        fn admit(&self, _: &ConnectionInfo, _: AdmitStage) -> Result<Admitted<()>, Refusal> {
358            Err(Refusal::Reset { code: 1 })
359        }
360        fn readmit(&self, _: &mut (), _: &ConnectionInfo) -> Result<(), Refusal> {
361            Ok(())
362        }
363        #[allow(clippy::panic)]
364        fn check(&self, _: &mut (), _: &WriteCheck) -> Result<(), Denial> {
365            panic!("gate bug")
366        }
367    }
368
369    #[test]
370    fn test_gate_panic_in_check_denies() {
371        let mut cell = GateCell {
372            gate: Arc::new(Panics),
373            ticket: Some(()),
374        };
375        let w = WriteCheck::new(ConnectionId::new(1).unwrap(), Lane(0), 1, false, None);
376        let prev = std::panic::take_hook();
377        std::panic::set_hook(Box::new(|_| {}));
378        let r = cell.check(&w);
379        std::panic::set_hook(prev);
380        assert_eq!(r, Err(Denial { code: 0 }));
381    }
382
383    #[test]
384    fn test_allow_all_admits_and_allows() {
385        let info = ConnectionInfo::for_testing("127.0.0.1:1".parse().unwrap());
386        assert!(AllowAll.admit(&info, AdmitStage::Accept).is_ok());
387        let mut cell = GateCell {
388            gate: Arc::new(AllowAll),
389            ticket: Some(()),
390        };
391        let w = WriteCheck::new(info.id, Lane(0), 10, false, None);
392        assert!(cell.check(&w).is_ok());
393        cell.close(&info, CloseReason::SessionClosed);
394        assert!(cell.check(&w).is_err());
395    }
396
397    #[test]
398    fn test_refusal_code() {
399        assert_eq!(Refusal::Reset { code: 9 }.code(), 9);
400    }
401}