Skip to content

Architecture

Overview

tylomon is a Tokio application with a single event loop. Everything that produces data runs in its own task or thread and sends Action values to the loop over an mpsc channel. The loop updates the App state and redraws the terminal at 30fps.

┌────────────┐  ┌─────────────────────────┐  ┌───────────────┐  ┌────────────┐
│ Input task │  │ Network watcher         │  │ Resolver task │  │  Replay    │
│ (crossterm)│  │  └ capture thread / bridge│  │  (bollard)    │  │  thread    │
└─────┬──────┘  │    (pcap)               │  └───────┬───────┘  │ (--read)   │
      │         └────────────┬────────────┘          │          └─────┬──────┘
      │ KeyPress, Mouse*     │ NewExchange,          │ ContainerUpdate │ NewExchange
      │                      │ CaptureStarted/Error  │                 │
      └──────────────┬───────┴───────────────────────┴─────────────────┘
                     ▼
             ┌──────────────┐        ┌────────────────────────────┐
             │  Event loop  │ ─────▶ │ --no-tui: JSON Lines, HAR   │
             │  (App state) │        └────────────────────────────┘
             └──────┬───────┘
                    │ 30fps
                    ▼
             ┌──────────────┐
             │   Ratatui    │
             └──────────────┘

With --read there is no watcher, resolver or Docker connection: only the replay thread feeds the loop. With --no-tui the input task is not started and the loop prints each exchange instead of drawing.

Source tree

src/
  main.rs              # Runtime, target discovery, event loop, headless JSONL mode
  lib.rs               # Public module re-exports
  app.rs               # App state, Action enum, keyboard and mouse handling
  cli.rs               # Clap CLI argument definitions
  error.rs             # Custom Error enum
  export.rs            # JSONL, HAR and curl serialisation
  filter.rs            # Filter query parser and matcher
  stats.rs             # Per-edge aggregation for the service map
  state.rs             # Remembered picker choice (XDG state directory)
  capture/
    bridge.rs          # Docker network ID → bridge interface name (br-<12chars>)
    packet.rs          # Raw bytes → ParsedPacket via etherparse
    stream.rs          # TCP stream reassembly with FlowKey tracking
    pipeline.rs        # Capture/replay threads, HTTP extraction, pcap recording
    http2.rs           # Cleartext HTTP/2 + gRPC decoder (H2Tracker)
    protobuf.rs        # Schema-less protobuf rendering
    postgres.rs        # PostgreSQL wire protocol decoder (PgTracker)
  docker/
    compose.rs         # Compose file and COMPOSE_FILE project detection
    discovery.rs       # Network discovery, Target resolution (bollard)
    resolver.rs        # Container IP-to-name resolution
    task.rs            # Async resolver polling task (5s interval)
    watch.rs           # NetworkWatcher: one capture per network, follows changes
  http/
    parser.rs          # HTTP request/response parsing via httparse
    encoding.rs        # gzip / deflate / brotli decoding
    types.rs           # HttpExchange, HttpRequest, HttpResponse structs
  tui/
    mod.rs             # Terminal init/restore (alternate screen, mouse capture)
    event.rs           # Input task — crossterm events → Action channel
    picker.rs          # Interactive target selection
    rows.rs            # Detail rows, text selection, highlighting
    theme.rs           # Colour scheme (static Style methods)
    views/
      mod.rs           # Layout — title, error banner, panes, status bar, help
      exchange_list.rs # Left pane — filtered, sorted exchange list
      detail.rs        # Right pane — pinned summary, headers, bodies
      body.rs          # Body rendering: coloured JSON, text, hex dump
      service_map.rs   # Per-edge table
      help.rs          # Key reference popup
      status_bar.rs    # Bottom bar — mode indicators, hints, counts
tests/
  http_parser_tests.rs # HTTP parsing
  replay_tests.rs      # Pipeline tests over generated pcap files
scripts/
  e2e.sh               # End-to-end check against examples/

Data pipeline

1. Target resolution

A Target (a Compose project or a single network) is resolved to Docker networks. It comes from --project, --network, or the picker, which lists the Compose project found in the working directory plus bridge networks that have containers. Each network maps to a Linux bridge interface (br-<first 12 chars of the ID>, or the name in com.docker.network.bridge.name, such as docker0).

2. Capture

The NetworkWatcher starts one capture thread per network. Each thread opens the bridge with libpcap in non-blocking mode, filters TCP, and polls every 10 ms so it can always be stopped. Every 5 seconds the watcher re-resolves the target: networks that were recreated get a new capture on their new bridge, removed networks are dropped, and new networks of a project are added. Captures that ended on their own are restarted, except those that failed for lack of privileges.

With --write, every thread also writes its packets to one shared pcap file. With --read, a replay thread reads a pcap file through the same pipeline instead.

3. Packet parsing and reassembly

etherparse extracts IP addresses, TCP ports, flags, sequence numbers and payload. The StreamReassembler accumulates payload bytes per flow direction and handles the SYN/FIN/RST lifecycle.

4. HTTP extraction

The pipeline finds request/response pairs in the reassembled data:

  • Parses headers with httparse
  • Reads bodies by Content-Length or Transfer-Encoding: chunked
  • Decodes Content-Encoding gzip, deflate and brotli (output capped at 16 MiB)
  • Supports keep-alive connections (several exchanges per stream)
  • Measures response time from packet timestamps, so live capture and replay agree
  • Flushes requests with no response after 30 seconds (or at the end of a replayed file) as unanswered

5. Other protocols

Before trying HTTP/1.x, the pipeline asks the H2Tracker and then the PgTracker whether a stream belongs to them.

HTTP/2 and gRPC. A client connection preface identifies cleartext HTTP/2. The tracker parses frames in both directions, decodes header blocks with one HPACK context per direction (every block must be decoded, even for streams it ignores, or the dynamic table drifts) and keeps per-stream state. A stream ends at the server's END_STREAM or a RST_STREAM. Streams with an application/grpc request become Protocol::Grpc exchanges: the status comes from the grpc-status trailer (200 for OK, else 500 with the status name), and each length-prefixed message is decompressed if needed and rendered by protobuf.rs without a schema. Other streams become HTTP exchanges with version HTTP/2. Streams that have started a response (such as long-lived streaming calls) stay open until they end; at the end of a replayed file everything still open is reported.

PostgreSQL. A flow is recognised by its first bytes (a protocol 3.0 startup, SSL, GSSAPI or cancel request) and then followed in both directions as a connection: the tracker parses typed messages, remembers prepared statements and portals, and pairs each Query or Execute with the server messages that answer it (RowDescription, DataRow, CommandComplete, ErrorResponse, ReadyForQuery). Finished queries are emitted as exchanges with Protocol::Postgres, reusing the HTTP structures: the verb is the method, the SQL is the path and body, and the status is 200 or 500. Connections that switch to TLS are reported once and not decoded. Unanswered queries are flushed after 30 seconds like HTTP requests.

6. Container resolution

A separate async task re-resolves the target and polls the Docker API every 5 seconds to maintain an IP-to-container-name mapping, so the capture threads are never blocked by Docker calls.

7. State and rendering

App owns all state: exchanges (each with a unique id), the selected id, filter, sort, view mode, split width, zoom, text selection, and the cached detail rows for the selected exchange. Selection is tracked by exchange id, so it survives new traffic, sorting and filtering.

Views render from App at 30fps. The few things they write back are layout facts needed for mouse hit-testing (pane areas, scroll offsets) and the detail cache.

Capture failures are kept per interface in App and shown as a banner until that capture runs again or its network goes away.

8. Output

In the TUI, e writes the listed exchanges to a HAR file and 3 copies a curl command. In headless mode each completed exchange is written to stdout as one JSON object, and --har writes everything captured on exit.

Key dependencies

Crate Purpose
ratatui Terminal UI framework
crossterm Terminal input/output and mouse events
tokio Async runtime
pcap Packet capture, recording and replay (libpcap bindings)
etherparse Ethernet/IP/TCP packet parsing
httparse HTTP request/response parsing
flate2, brotli Body decompression
bollard Docker Engine API client
clap CLI argument parsing
serde, serde_json, serde_yaml_ng Serialisation, JSON bodies, Compose name:
base64 Binary bodies in JSONL and HAR
arboard Clipboard (X11 and Wayland)
anyhow Error handling
tracing Structured logging to a file