Skip to main content

krabka_kafka_tap/
lib.rs

1//! Test-only Kafka wire tap.
2//!
3//! The tap is a TCP relay. It tees complete frames to a `Recorder`, and at the
4//! same time it forwards the bytes byte-for-byte to a real broker.
5pub mod frame;
6
7use std::{
8    io::{self, Read, Write},
9    net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs},
10    sync::{Arc, Mutex, PoisonError},
11    thread,
12};
13
14use frame::{CapturedFrame, Pending, parse_request_prefix, read_correlation_id};
15
16/// Callback invoked once per fully-read frame in either direction.
17pub type Recorder = Arc<dyn Fn(CapturedFrame) + Send + Sync>;
18
19/// Bind a listener, accept connections, relay each one to `upstream`, and
20/// record the frames.
21///
22/// The function returns the bound local address, which is useful when the
23/// caller passes port 0. The accept loop runs on a background thread for the
24/// lifetime of the process.
25///
26/// # Errors
27///
28/// If `listen` cannot be bound, or the bound listener has no local address.
29pub fn spawn(
30    listen: impl ToSocketAddrs,
31    upstream: &str,
32    recorder: Recorder,
33) -> io::Result<SocketAddr> {
34    let listener = TcpListener::bind(listen)?;
35    let addr = listener.local_addr()?;
36    let upstream = upstream.to_owned();
37    thread::spawn(move || {
38        for client in listener.incoming() {
39            let Ok(client) = client else { continue };
40            let upstream = upstream.clone();
41            let recorder = Arc::clone(&recorder);
42            thread::spawn(move || {
43                if let Err(e) = handle_conn(client, &upstream, &recorder) {
44                    eprintln!("tap conn error: {e}");
45                }
46            });
47        }
48    });
49    Ok(addr)
50}
51
52fn handle_conn(client: TcpStream, upstream: &str, recorder: &Recorder) -> io::Result<()> {
53    let server = TcpStream::connect(upstream)?;
54    let pending = Arc::new(Mutex::new(Pending::default()));
55
56    let c2s_client = client.try_clone()?;
57    let c2s_server = server.try_clone()?;
58    let pend_req = Arc::clone(&pending);
59    let rec_req = Arc::clone(recorder);
60    let t = thread::spawn(move || {
61        let _ = pump(c2s_client, c2s_server, true, &pend_req, &*rec_req);
62    });
63
64    pump(server, client, false, &pending, &**recorder)?;
65    let _ = t.join();
66    Ok(())
67}
68
69/// Copy length-prefixed frames from `src` to `dst`, and tee each frame to the
70/// recorder. `is_request` selects between header parsing and correlation
71/// lookup.
72///
73/// Each frame reaches the recorder before it is written on. A response cannot
74/// exist until its request has been forwarded, so this is what makes a
75/// request reach the recorder before its own response does: forwarded first,
76/// the response pump could record the reply while the request pump had yet to
77/// record the request, and the standalone tap's spool would list them the
78/// wrong way round.
79fn pump(
80    mut src: TcpStream,
81    mut dst: TcpStream,
82    is_request: bool,
83    pending: &Mutex<Pending>,
84    recorder: &(dyn Fn(CapturedFrame) + Send + Sync),
85) -> io::Result<()> {
86    loop {
87        let mut len_buf = [0u8; 4];
88        if let Err(e) = src.read_exact(&mut len_buf) {
89            if e.kind() == io::ErrorKind::UnexpectedEof {
90                return Ok(());
91            }
92            return Err(e);
93        }
94        // A negative length ends the relay.
95        let Ok(n) = usize::try_from(i32::from_be_bytes(len_buf)) else {
96            return Ok(());
97        };
98        let mut body = vec![0u8; n];
99        src.read_exact(&mut body)?;
100
101        if is_request {
102            if let Some(p) = parse_request_prefix(&body) {
103                pending
104                    .lock()
105                    .unwrap_or_else(PoisonError::into_inner)
106                    .record(p.correlation_id, p.api_key, p.api_version);
107                recorder(CapturedFrame {
108                    api_key: p.api_key,
109                    version: p.api_version,
110                    is_request: true,
111                    body: body.clone(),
112                });
113            }
114        } else if let Some(corr) = read_correlation_id(&body) {
115            let request = pending
116                .lock()
117                .unwrap_or_else(PoisonError::into_inner)
118                .take(corr);
119            if let Some((api_key, version)) = request {
120                recorder(CapturedFrame {
121                    api_key,
122                    version,
123                    is_request: false,
124                    body: body.clone(),
125                });
126            }
127        }
128
129        dst.write_all(&len_buf)?;
130        dst.write_all(&body)?;
131        dst.flush()?;
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use std::time::{Duration, Instant};
138
139    use krabka_ids::{ApiKey, ApiVersion};
140
141    use super::*;
142
143    fn framed(body: &[u8]) -> Vec<u8> {
144        let mut out = i32::try_from(body.len()).unwrap().to_be_bytes().to_vec();
145        out.extend_from_slice(body);
146        out
147    }
148
149    /// A request frame: `api_key`, `api_version`, `correlation_id`, then payload.
150    fn request(api_key: i16, version: i16, corr: i32) -> Vec<u8> {
151        let mut b = api_key.to_be_bytes().to_vec();
152        b.extend_from_slice(&version.to_be_bytes());
153        b.extend_from_slice(&corr.to_be_bytes());
154        b
155    }
156
157    /// An upstream that reads one frame and answers with `response`.
158    fn upstream_echoing(response: Vec<u8>) -> io::Result<SocketAddr> {
159        let listener = TcpListener::bind("127.0.0.1:0")?;
160        let addr = listener.local_addr()?;
161        thread::spawn(move || {
162            let Ok((mut server, _)) = listener.accept() else {
163                return;
164            };
165            let mut len = [0u8; 4];
166            if server.read_exact(&mut len).is_err() {
167                return;
168            }
169            // The client in these tests only sends non-negative lengths.
170            let mut body = vec![0u8; usize::try_from(i32::from_be_bytes(len)).unwrap()];
171            if server.read_exact(&mut body).is_err() {
172                return;
173            }
174            let _ = server.write_all(&framed(&response));
175            let _ = server.flush();
176        });
177        Ok(addr)
178    }
179
180    /// The relay is the whole point of the crate and nothing exercised it: a
181    /// `handle_conn` or `pump` replaced by `Ok(())` forwards nothing and
182    /// records nothing, while still returning success. Driving one request and
183    /// one response through real sockets is what tells those apart.
184    ///
185    /// It also pins the frame-length guard. `n < 0` read as `n > 0` returns
186    /// before relaying anything at all. And it pins the recording order: a
187    /// `pump` that forwards before it records can let the response reach the
188    /// recorder ahead of its request.
189    #[test]
190    fn relays_frames_both_ways_and_records_them() {
191        let response = 7i32.to_be_bytes().to_vec();
192        let upstream = upstream_echoing(response.clone()).unwrap();
193
194        let seen = Arc::new(Mutex::new(Vec::new()));
195        let sink = Arc::clone(&seen);
196        let addr = spawn(
197            "127.0.0.1:0",
198            &upstream.to_string(),
199            Arc::new(move |f: CapturedFrame| sink.lock().unwrap().push(f)),
200        )
201        .unwrap();
202
203        let mut client = TcpStream::connect(addr).unwrap();
204        client
205            .set_read_timeout(Some(Duration::from_secs(10)))
206            .unwrap();
207        client.write_all(&framed(&request(18, 3, 7))).unwrap();
208        client.flush().unwrap();
209
210        let mut len = [0u8; 4];
211        client.read_exact(&mut len).unwrap();
212        let mut body = vec![0u8; usize::try_from(i32::from_be_bytes(len)).unwrap()];
213        client.read_exact(&mut body).unwrap();
214
215        // Forwarded byte-for-byte.
216        assert2::check!(body == response);
217
218        // `pump` records a frame before it writes it on, so by the time the
219        // response has arrived here both frames have been recorded, request
220        // first. No waiting, and the order below is the one the recorder saw.
221        // Both directions reached the recorder, and the response was correlated
222        // back to the api key and version the request carried.
223        let kinds: Vec<_> = seen
224            .lock()
225            .unwrap()
226            .iter()
227            .map(|f| (f.api_key, f.version, f.is_request))
228            .collect();
229        assert2::check!(
230            kinds
231                == vec![
232                    (ApiKey(18), ApiVersion(3), true),
233                    (ApiKey(18), ApiVersion(3), false)
234                ]
235        );
236    }
237
238    /// The frame-length guard stops the relay only on a *negative* length. Zero
239    /// is a legal empty frame and must be forwarded like any other: read as
240    /// `n <= 0` or `n == 0`, the first empty frame ends the connection and
241    /// everything after it is silently dropped.
242    #[test]
243    fn a_zero_length_frame_is_forwarded_rather_than_ending_the_relay() {
244        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
245        let upstream = listener.local_addr().unwrap();
246
247        // Collects the frame lengths the upstream actually receives.
248        let got = Arc::new(Mutex::new(Vec::new()));
249        let sink = Arc::clone(&got);
250        thread::spawn(move || {
251            let Ok((mut server, _)) = listener.accept() else {
252                return;
253            };
254            for _ in 0..2 {
255                let mut len = [0u8; 4];
256                if server.read_exact(&mut len).is_err() {
257                    return;
258                }
259                let n = i32::from_be_bytes(len);
260                let mut body = vec![0u8; usize::try_from(n).unwrap()];
261                if server.read_exact(&mut body).is_err() {
262                    return;
263                }
264                sink.lock().unwrap().push(n);
265            }
266        });
267
268        let addr = spawn("127.0.0.1:0", &upstream.to_string(), Arc::new(|_| {})).unwrap();
269        let mut client = TcpStream::connect(addr).unwrap();
270        client.write_all(&framed(&[])).unwrap();
271        client.write_all(&framed(&request(18, 3, 7))).unwrap();
272        client.flush().unwrap();
273
274        let deadline = Instant::now() + Duration::from_secs(10);
275        while got.lock().unwrap().len() < 2 && Instant::now() < deadline {
276            thread::sleep(Duration::from_millis(5));
277        }
278        assert2::check!(*got.lock().unwrap() == vec![0, 8]);
279    }
280
281    /// A client that closes without sending is an ordinary end of stream, not a
282    /// failure. The guard reads the error kind to decide; inverted, a clean
283    /// close is reported as an error instead.
284    #[test]
285    fn clean_close_ends_the_pump_without_an_error() {
286        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
287        let addr = listener.local_addr().unwrap();
288        let client = TcpStream::connect(addr).unwrap();
289        let (server, _) = listener.accept().unwrap();
290
291        // Close the writing end, so the pump's first read hits end of stream.
292        drop(client);
293
294        let result = pump(
295            server.try_clone().unwrap(),
296            server,
297            true,
298            &Mutex::new(Pending::default()),
299            &drop::<CapturedFrame>,
300        );
301        assert2::check!(result.is_ok());
302    }
303}