Skip to main content

lacodda_tear/
lib.rs

1//! A TCP proxy for tests that tears connections where you say.
2//!
3//! Code that talks over a network has to survive the network breaking: a
4//! request torn halfway up, an answer lost on its way back, a connection that
5//! goes silent, a phone that drops off the network and comes back. A mock of
6//! the other side cannot show any of that - it never breaks the way a socket
7//! does. [`Tear`] stands between the code under test and the real server, on
8//! a real socket, passes every byte through, and breaks the connection on
9//! cue:
10//!
11//! - [`Cut::Up`] - after so many more bytes towards the server: a request
12//!   torn partway;
13//! - [`Cut::Down`] - after so many more bytes back from it: an answer torn
14//!   partway, or - with zero - lost before its first byte, after the server
15//!   has done the work;
16//! - [`Cut::After`] - whatever is open once a while has passed;
17//! - [`Cut::Now`] - whatever is open now;
18//! - [`Tear::offline`] and [`Tear::online`] - the network gone: open
19//!   connections torn, new ones reset at once, until it is back;
20//! - [`Tear::stall`] and [`Tear::flow`] - bytes held, the connection open and
21//!   silent, as on a network that drops packets without saying so.
22//!
23//! Byte counts run across every connection from the moment a cut is armed,
24//! not per connection: an HTTP client that reuses a pooled connection for
25//! the next request is cut just the same. A tear is a reset (`RST`), the way
26//! a dropped mobile connection looks to both ends - not a polite end of the
27//! stream.
28//!
29//! The proxy knows nothing of what it carries: HTTP, a database's protocol,
30//! anything over TCP.
31//!
32//! ```no_run
33//! use lacodda_tear::{Cut, Tear};
34//!
35//! # async fn run(server: std::net::SocketAddr) -> std::io::Result<()> {
36//! let tear = Tear::start(server).await?;
37//! // Point the client at `tear.addr()` instead of the server.
38//!
39//! // The next answer is lost: the server does the work, the client hears
40//! // nothing back.
41//! tear.cut(Cut::Down(0));
42//! // ... make the request, see it fail, make it again ...
43//! assert_eq!(tear.torn(), 1);
44//! # Ok(())
45//! # }
46//! ```
47
48use std::collections::HashMap;
49use std::io;
50use std::net::{Ipv4Addr, SocketAddr};
51use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
52use std::sync::{Arc, Mutex, MutexGuard};
53use std::time::Duration;
54
55use tokio::io::{AsyncReadExt, AsyncWriteExt};
56use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
57use tokio::net::{TcpListener, TcpStream};
58use tokio::sync::watch;
59use tokio::task::{JoinHandle, JoinSet};
60
61/// Where a connection breaks.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum Cut {
65    /// Let this many more bytes through towards the server, then tear the
66    /// connection that carries the next one.
67    Up(usize),
68    /// Let this many more bytes through back from the server, then tear the
69    /// connection that carries the next one. `Down(0)` loses an answer
70    /// before its first byte - after the server has sent it.
71    Down(usize),
72    /// Tear every connection open once this long has passed.
73    After(Duration),
74    /// Tear every connection open now.
75    Now,
76}
77
78/// Which way bytes go.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum Direction {
81    /// From the client towards the server.
82    Up,
83    /// From the server back to the client.
84    Down,
85}
86
87/// A proxy on a port of its own, in front of one server.
88///
89/// Dropping it stops the proxy and closes every connection through it.
90pub struct Tear {
91    addr: SocketAddr,
92    shared: Arc<Shared>,
93    accept: JoinHandle<()>,
94}
95
96struct Shared {
97    upstream: SocketAddr,
98    rules: Mutex<Rules>,
99    /// Whether bytes move: false while stalled.
100    flowing: watch::Sender<bool>,
101    /// The connections open now, each with the switch that tears it.
102    open: Mutex<HashMap<u64, watch::Sender<bool>>>,
103    next: AtomicU64,
104    accepted: AtomicUsize,
105    torn: AtomicUsize,
106    passed_up: AtomicUsize,
107    passed_down: AtomicUsize,
108}
109
110#[derive(Default)]
111struct Rules {
112    /// Bytes left before the tear, towards the server; `None` when unarmed.
113    up: Option<usize>,
114    /// Bytes left before the tear, back from the server.
115    down: Option<usize>,
116    offline: bool,
117}
118
119impl Tear {
120    /// A proxy in front of `upstream`, listening on a free port of the
121    /// loopback interface.
122    ///
123    /// # Errors
124    ///
125    /// When no port can be bound.
126    pub async fn start(upstream: SocketAddr) -> io::Result<Self> {
127        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
128        let addr = listener.local_addr()?;
129        let shared = Arc::new(Shared {
130            upstream,
131            rules: Mutex::new(Rules::default()),
132            flowing: watch::Sender::new(true),
133            open: Mutex::new(HashMap::new()),
134            next: AtomicU64::new(0),
135            accepted: AtomicUsize::new(0),
136            torn: AtomicUsize::new(0),
137            passed_up: AtomicUsize::new(0),
138            passed_down: AtomicUsize::new(0),
139        });
140        let accept = tokio::spawn(accept(listener, shared.clone()));
141        Ok(Self { addr, shared, accept })
142    }
143
144    /// Where the client connects instead of the server.
145    pub fn addr(&self) -> SocketAddr {
146        self.addr
147    }
148
149    /// The server this proxy stands in front of.
150    pub fn upstream(&self) -> SocketAddr {
151        self.shared.upstream
152    }
153
154    /// Arms a cut. A byte cut replaces an earlier one of the same direction
155    /// that has not fired yet; once fired, it is gone.
156    pub fn cut(&self, cut: Cut) {
157        match cut {
158            Cut::Up(bytes) => self.shared.rules().up = Some(bytes),
159            Cut::Down(bytes) => self.shared.rules().down = Some(bytes),
160            Cut::Now => self.shared.tear_open(),
161            Cut::After(delay) => {
162                let shared = Arc::downgrade(&self.shared);
163                tokio::spawn(async move {
164                    tokio::time::sleep(delay).await;
165                    if let Some(shared) = shared.upgrade() {
166                        shared.tear_open();
167                    }
168                });
169            }
170        }
171    }
172
173    /// Disarms the byte cuts that have not fired yet.
174    pub fn disarm(&self) {
175        let mut rules = self.shared.rules();
176        rules.up = None;
177        rules.down = None;
178    }
179
180    /// The network is gone: every open connection is torn, and every new one
181    /// is reset as soon as it is accepted, until [`Tear::online`].
182    pub fn offline(&self) {
183        self.shared.rules().offline = true;
184        self.shared.tear_open();
185    }
186
187    /// The network is back.
188    pub fn online(&self) {
189        self.shared.rules().offline = false;
190    }
191
192    /// Holds every byte where it is, in both directions, with the
193    /// connections left open - until [`Tear::flow`].
194    pub fn stall(&self) {
195        self.shared.flowing.send_replace(false);
196    }
197
198    /// Lets bytes move again after [`Tear::stall`].
199    pub fn flow(&self) {
200        self.shared.flowing.send_replace(true);
201    }
202
203    /// How many connections the proxy has accepted.
204    pub fn connections(&self) -> usize {
205        self.shared.accepted.load(Ordering::SeqCst)
206    }
207
208    /// How many connections the proxy has torn - by a cut, or by going
209    /// offline - counting those reset as they were accepted.
210    pub fn torn(&self) -> usize {
211        self.shared.torn.load(Ordering::SeqCst)
212    }
213
214    /// How many bytes the proxy has passed on in `direction`, over every
215    /// connection.
216    pub fn passed(&self, direction: Direction) -> usize {
217        match direction {
218            Direction::Up => self.shared.passed_up.load(Ordering::SeqCst),
219            Direction::Down => self.shared.passed_down.load(Ordering::SeqCst),
220        }
221    }
222}
223
224impl Drop for Tear {
225    fn drop(&mut self) {
226        // The accept loop owns every connection's task: aborting it drops
227        // them, and their sockets with them.
228        self.accept.abort();
229    }
230}
231
232impl std::fmt::Debug for Tear {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        f.debug_struct("Tear").field("addr", &self.addr).field("upstream", &self.shared.upstream).finish()
235    }
236}
237
238impl Shared {
239    fn rules(&self) -> MutexGuard<'_, Rules> {
240        lock(&self.rules)
241    }
242
243    fn tear_open(&self) {
244        for switch in lock(&self.open).values() {
245            switch.send_replace(true);
246        }
247    }
248
249    /// Takes `len` bytes from the budget of `direction`: how many of them
250    /// pass, and whether the connection is torn after them.
251    fn take(&self, direction: Direction, len: usize) -> (usize, bool) {
252        let mut rules = self.rules();
253        let budget = match direction {
254            Direction::Up => &mut rules.up,
255            Direction::Down => &mut rules.down,
256        };
257        match budget {
258            None => (len, false),
259            Some(left) if len <= *left => {
260                *left -= len;
261                (len, false)
262            }
263            Some(left) => {
264                let pass = *left;
265                *budget = None;
266                (pass, true)
267            }
268        }
269    }
270
271    fn passed(&self, direction: Direction, len: usize) {
272        let counter = match direction {
273            Direction::Up => &self.passed_up,
274            Direction::Down => &self.passed_down,
275        };
276        counter.fetch_add(len, Ordering::SeqCst);
277    }
278}
279
280async fn accept(listener: TcpListener, shared: Arc<Shared>) {
281    let mut connections = JoinSet::new();
282    loop {
283        // Finished connections are reaped as the loop goes, so the set does
284        // not grow with every connection a long test makes.
285        while connections.try_join_next().is_some() {}
286        let Ok((client, _)) = listener.accept().await else {
287            continue;
288        };
289        shared.accepted.fetch_add(1, Ordering::SeqCst);
290        connections.spawn(serve(client, shared.clone()));
291    }
292}
293
294/// How one direction of a connection ended.
295enum Flow {
296    /// Its sender finished; the end was passed on.
297    Ended,
298    /// A cut fired.
299    Cut,
300    /// A socket failed - the peer reset it, say.
301    Failed,
302}
303
304async fn serve(client: TcpStream, shared: Arc<Shared>) {
305    if shared.rules().offline {
306        shared.torn.fetch_add(1, Ordering::SeqCst);
307        reset(&client);
308        return;
309    }
310    let Ok(server) = TcpStream::connect(shared.upstream).await else {
311        reset(&client);
312        return;
313    };
314    // Each byte goes on as it comes: a cut lands where it was asked to, not
315    // where a send buffer happened to flush.
316    let _ = client.set_nodelay(true);
317    let _ = server.set_nodelay(true);
318
319    let id = shared.next.fetch_add(1, Ordering::SeqCst);
320    let (switch, mut torn) = watch::channel(false);
321    lock(&shared.open).insert(id, switch);
322    let (mut client_read, mut client_write) = client.into_split();
323    let (mut server_read, mut server_write) = server.into_split();
324
325    let end = {
326        let up = pump(&mut client_read, &mut server_write, Direction::Up, &shared);
327        let down = pump(&mut server_read, &mut client_write, Direction::Down, &shared);
328        tokio::pin!(up, down);
329        let (mut up_done, mut down_done) = (false, false);
330        loop {
331            let flow = tokio::select! {
332                flow = &mut up, if !up_done => { up_done = true; flow }
333                flow = &mut down, if !down_done => { down_done = true; flow }
334                _ = torn.wait_for(|torn| *torn) => Flow::Cut,
335            };
336            match flow {
337                Flow::Ended if up_done && down_done => break Flow::Ended,
338                Flow::Ended => {}
339                end => break end,
340            }
341        }
342    };
343    lock(&shared.open).remove(&id);
344    if matches!(end, Flow::Ended) {
345        return;
346    }
347    if matches!(end, Flow::Cut) {
348        shared.torn.fetch_add(1, Ordering::SeqCst);
349    }
350    // A failure on one side is passed on to the other as what it was. The
351    // halves go back together first: a write half dropped on its own ends
352    // the stream politely before the reset could say otherwise.
353    for (read, write) in [(client_read, client_write), (server_read, server_write)] {
354        if let Ok(socket) = read.reunite(write) {
355            reset(&socket);
356        }
357    }
358}
359
360/// Moves bytes one way until the sender ends, a cut fires or a socket fails.
361async fn pump(from: &mut OwnedReadHalf, to: &mut OwnedWriteHalf, direction: Direction, shared: &Shared) -> Flow {
362    let mut flowing = shared.flowing.subscribe();
363    let mut buffer = vec![0u8; 16 * 1024];
364    loop {
365        let len = match from.read(&mut buffer).await {
366            Ok(0) => {
367                return match to.shutdown().await {
368                    Ok(()) => Flow::Ended,
369                    Err(_) => Flow::Failed,
370                };
371            }
372            Ok(len) => len,
373            Err(_) => return Flow::Failed,
374        };
375        if flowing.wait_for(|flowing| *flowing).await.is_err() {
376            return Flow::Failed;
377        }
378        let (pass, cut) = shared.take(direction, len);
379        if pass > 0 {
380            if to.write_all(&buffer[..pass]).await.is_err() {
381                return Flow::Failed;
382            }
383            shared.passed(direction, pass);
384        }
385        if cut {
386            return Flow::Cut;
387        }
388    }
389}
390
391/// Closes a socket with a reset rather than an end of stream: the peer reads
392/// an error, not a short message.
393fn reset(socket: &TcpStream) {
394    // Best effort: a socket the peer has reset already cannot be set, and is
395    // torn anyway.
396    let _ = socket.set_zero_linger();
397}
398
399fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
400    mutex.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
401}