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}