tinytap

module
v0.6.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: MIT

README

tinytap

Test codecov Go Report Card Go Reference OpenSSF Scorecard OpenSSF Best Practices

A tiny eBPF-based HTTP traffic capture tool for local development.

Left: an ordinary app calling an API over HTTPS. Right: tinytap's TUI (output = "tui" in the config file — see Configuration) showing the exact bytes that app sent — the request line, every header including Authorization: Bearer, and the decoded JSON body — in plaintext, with no proxy and no CA certificate installed. In the request table j/k scroll and Enter opens the detail panel (b toggles the hex body view). See scripts/demo/ for how the gif is recorded.

Quick start

Install:

curl -fsSL https://raw.githubusercontent.com/shinagawa-web/tinytap/main/scripts/install.sh | sh

Grant it the capabilities it needs, then run it — no full root required:

sudo setcap cap_dac_read_search,cap_perfmon,cap_bpf=eip $(command -v tinytap)
tinytap

With no config file, that opens the TUI shown at the top of this README — j/k to scroll, Enter for the detail panel, q or Ctrl-C to quit — as long as your terminal is at least 120x24. In a smaller or non-interactive terminal it prints guidance and exits instead of silently streaming; see Configuration to switch to the line-oriented stdout mode.

Diagnosing startup problems

If it didn't work, run tinytap doctor first — read-only preflight checks (kernel version, BTF availability, the capabilities in docs/capabilities.md, syscall tracepoint availability, a dry-run BPF load, and the host's libssl execute bit), printed as a copy-paste-friendly report, without needing root or capabilities itself:

tinytap doctor

Each result is classified by what it actually costs: a blocking result means tinytap can't run at all (e.g. a kernel below the 5.8 floor); a degraded result means tinytap runs but one specific capability is lost (e.g. no TLS capture without cap_sys_admin) — it's never printed as if something were broken. doctor exits non-zero only when a blocking result is present, so tinytap doctor && tinytap is a reasonable way to run it. A normal startup failure also names the specific blocking cause instead of only a raw error, pointing at tinytap doctor for the full picture.

Linux amd64/arm64 only — on macOS/Windows, see Where tinytap Runs. Want HTTPS capture too, a specific version, or to build from source instead? See Running without full root, Installing a specific version or location, or Building from source.

Where tinytap Runs

tinytap requires a Linux kernel. It cannot run natively on macOS or Windows, because eBPF is a Linux kernel technology. But that's less restrictive than it sounds, because Linux kernels are everywhere:

Where the user works How tinytap runs there
Linux desktop / laptop / workstation Native. Just run the binary.
Linux server (cloud VM, on-prem, dev box) Native. SSH in, run it.
Mac (Intel or Apple Silicon) Inside a Linux VM — Lima, Multipass, OrbStack, UTM, Docker Desktop's VM, etc.
Windows Inside WSL2 (which is a real Linux kernel).

This pattern — "Mac/Win developers run this through a Linux VM" — is the standard for eBPF tooling in general (bpftrace, Cilium, etc.). tinytap is not unusual here.

Containers are friends, not enemies

A common question: "if my dev stack runs in Docker on my Mac, can tinytap see inside the containers?"

Yes. A Docker container is just a process (or process tree) running on the host's Linux kernel, isolated by namespaces and cgroups. eBPF programs attach to kernel events — syscalls, kprobes, tracepoints — which fire for all processes, container or not. So:

Mac
└── Lima VM (Ubuntu)        ← tinytap runs here
    ├── tinytap (Go binary, sudo)
    └── Docker daemon
        ├── container: api-service
        ├── container: db
        └── container: cache

...tinytap, running in the VM as root, observes syscalls from the containerized processes too — the same way it would for a process running directly on the VM. This is the same reason htop on the host shows container processes: they're all just kernel processes.

For the user, this means tinytap doesn't need to be installed inside containers, doesn't need a sidecar, and doesn't require the application to be rebuilt with anything. One install on the host is enough.

(Container-aware attribution — turning a PID into "this is the api-service container" — is a planned feature, not yet built. The kernel sees the PIDs; mapping them back to container names requires reading from Docker/containerd. For now tinytap shows raw PIDs.)

Requirements
  • Linux kernel 5.8+ (tinytap's event transport is BPF_MAP_TYPE_RINGBUF, added in 5.8 — see Toolchain)
  • macOS/Windows users run tinytap inside a Linux VM (Lima, WSL2, etc.) — there is no native macOS/Windows build and none is planned, since eBPF is Linux-only
Running without full root

sudo ./tinytap is the simplest path, but tinytap doesn't need full root. Plaintext HTTP capture needs three Linux capabilities:

sudo setcap cap_dac_read_search,cap_perfmon,cap_bpf=eip ./tinytap
./tinytap

TLS capture (the libssl uprobes) needs one more, cap_sys_admin:

sudo setcap cap_dac_read_search,cap_perfmon,cap_bpf,cap_sys_admin=eip ./tinytap
./tinytap

See docs/capabilities.md for what each capability is for, why TLS needs the broader cap_sys_admin, how this was verified, and known gaps (older kernels, x86_64).

Installing a specific version or location

Two env vars change the install script's behavior — set them on the sh side of the pipe, not before curl, since a VAR=val curl ... | sh prefix only reaches curl, not the piped-in script:

curl -fsSL https://raw.githubusercontent.com/shinagawa-web/tinytap/main/scripts/install.sh | TINYTAP_VERSION=v0.6.1 sh   # pin a release instead of the latest
curl -fsSL https://raw.githubusercontent.com/shinagawa-web/tinytap/main/scripts/install.sh | INSTALL_DIR=~/bin sh       # install somewhere other than /usr/local/bin

Verifying a release download

The install script already verifies the downloaded archive's SHA-256 checksum automatically — this section is for downloading a release archive by hand instead (from the releases page or in a script that intentionally avoids curl | sh) and confirming its full chain of trust, including the cosign signature the install script doesn't check. Every tagged release publishes, alongside the linux_amd64/linux_arm64 archives:

  • checksums.txt — SHA-256 of every archive and SBOM in the release
  • checksums.txt.sigstore.json — a keyless cosign signature over checksums.txt, minted from the release workflow's own GitHub Actions OIDC identity (no private key is stored anywhere)
  • <archive>.sbom.json — an SBOM for each archive (syft, SPDX format)

To verify the full chain of trust manually instead of trusting the script:

sha256sum --check --ignore-missing checksums.txt

Verify checksums.txt itself was produced by tinytap's release workflow (requires cosign v3+):

cosign verify-blob \
  --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp "^https://github.com/shinagawa-web/tinytap/\.github/workflows/release\.yml@refs/tags/v.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

Since every archive and SBOM is listed by digest inside checksums.txt, a passing cosign verify-blob on checksums.txt plus a passing sha256sum --check on the archive establishes the whole chain: this exact archive came from this exact release workflow run.

What it does today

tinytap attaches eBPF probes to a process's socket syscalls (accept4/read/write/close/recvfrom/sendto/recvmsg/sendmsg), parses the payload bytes as HTTP/1.1, pairs each request with its response, and renders the exchange live — either in the terminal TUI above or as a line-oriented stream:

12:47:57.005  python3[27122]  GET   /                        200    1304B     0.3ms
12:47:57.005  curl[1234]       GET   /api                     ABANDONED     12.3ms  (peer closed)

output = "auto" (the default) picks the TUI when stdout/stdin are an interactive terminal of at least 120x24; otherwise it prints guidance and exits rather than silently streaming — the line stream is opt-in via output = "stdout". output = "tui" forces the TUI (and exits the same way if the terminal can't host it); verbose = true hangs the full request/response headers under each stdout line. --version prints the build's version, commit, and date, and exits without needing root.

Configuration

Session settings (output, verbose, and process filters) live in a TOML config file, not CLI flags. tinytap config init writes one, fully populated with defaults, so tinytap config init && tinytap just works:

tinytap config init          # writes ./tinytap.toml
tinytap config init path/to/config.toml   # or a specific path
tinytap config init --force  # overwrite an existing file

Search order when --config <path> isn't given: ./tinytap.toml, then $XDG_CONFIG_HOME/tinytap/config.toml (falling back to ~/.config/tinytap/config.toml) — finding neither is not an error, the defaults below apply.

output = "auto"   # auto | stdout | tui
verbose = false

[filter]
pid  = []         # []uint32 — schema only, not yet enforced by the BPF program (#211)
comm = []         # []string — schema only, not yet enforced by the BPF program (#211)

The only CLI surface is one-shot actions, not session settings: --config <path> (point at an alternate config file), --version (build metadata, exits before any eBPF load), config init (above), and doctor (see Diagnosing startup problems).

Current limitations

  • HTTP/1.1 only — no HTTP/2, gRPC, or other protocols yet
  • TLS capture needs a dynamically linked libssl.so, so statically linked TLS stacks are invisible — that includes Go's crypto/tls and therefore Go-based proxies like Traefik and Caddy. Clients that hand OpenSSL a custom BIO instead of calling SSL_set_fd (e.g. curl) are captured and paired, but keyed on the SSL* pointer rather than a socket fd, so their exchanges are marked [ssl-keyed, fd unverified] — see docs/tls-compat.md
  • Debian/Ubuntu package libssl.so.3 without the execute bit (mode 0644), which the TLS uprobe attach requires — until fixed, TLS capture silently finds nothing to hook. One-time fix per host: find the path with ldconfig -p | grep libssl, then sudo chmod +x <path> (tinytap deliberately never does this itself; making the failure discoverable at runtime instead of only here is tracked in #216)
  • Single host — no cross-container attribution or cross-service correlation yet
  • Response bodies are sampled up to a fixed per-syscall cap, not captured in full (see docs/server-compat.md for exactly how each server's syscall pattern affects this)
  • sendfile-based transfers only carry payload bytes on amd64/arm64 — other architectures see the exchange but not the sampled body

See docs/server-compat.md for a server-by-server breakdown of what's currently visible.

Status & Roadmap

Released so far: v0.1.0 (HTTP request/response visible), v0.2.0 (Bubble Tea TUI), v0.3.0 (filtering + test foundation), v0.4.0 (server capture & compatibility — see docs/server-compat.md), v0.5.0 (HTTPS support via libssl uprobes — see docs/tls-compat.md), v0.6.0 (production readiness). v0.7.0 (real-hardware bring-up) is in progress — see #198. (v0.4.0 has no corresponding git tag — git tag jumps from v0.3.0 to v0.5.0 — left alone rather than backfilled; see #206.)

Full roadmap (near-term steps and longer-term vision) lives in #19, kept out of the README so this stays focused on what tinytap does today.

Toolchain

Component Choice Why
eBPF lib github.com/cilium/ebpf Pure Go, modern, standard for new projects
Build bpf2go (part of cilium/ebpf) Generates Go bindings from C code
Compiler clang 17+ Standard for eBPF, supports BTF. clang-14 compiles cleanly but the emitted bpf_probe_read_user call fails the kernel verifier (R2 unbounded memory access) — CI pins 17 (#207); 15/16 untested
Go 1.24+
Kernel Linux 5.8+ Required for BPF_MAP_TYPE_RINGBUF, tinytap's event transport
Architecture amd64 + arm64 Need arm64 for Apple Silicon Lima VM
Release builds GoReleaser v2 Cross-compiles linux/amd64 + linux/arm64 on tag push (.goreleaser.yml) — the release workflow regenerates the bpf2go artifacts from source first (#260), then it's a plain go build; GoReleaser itself needs no clang/libbpf step
Dev environment

Mac (Apple Silicon) + Lima with Ubuntu 24.04. Build and run inside the Lima VM. Edit code on Mac via VS Code's remote SSH or the auto-mounted filesystem. This is private to the maintainer's workflow — it does not constrain users; see Where tinytap Runs for how tinytap runs on a user's machine.

Setup commands:

# Mac side
brew install lima
limactl start --name=tinytap template://ubuntu
limactl shell tinytap

# Inside the VM
sudo apt update
sudo apt install -y clang llvm libbpf-dev linux-headers-$(uname -r) \
  build-essential git pkg-config

# Go (apt version is old)
GO_VERSION=1.24.0
ARCH=$(dpkg --print-architecture)  # arm64 on Apple Silicon
wget https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz
sudo tar -C /usr/local -xzf go${GO_VERSION}.linux-${ARCH}.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc

Building from source

Build and run inside the Lima VM (see Toolchain above for setup):

# Regenerate Go bindings from C (only needed after editing bpf/*.c)
cd ~/tinytap/internal/loader/bpf && go generate

# Build
cd ~/tinytap && go build ./...

# Run (requires root — eBPF needs CAP_BPF/CAP_PERFMON or root)
sudo ./tinytap

Root isn't actually required — see Running without full root for the minimal setcap invocation.

Or via make:

make run       # orchestrated smoke test: starts a demo HTTP server, fires a request, shows the capture
make run-raw   # build + run with output = "stdout" against whatever's already running

Run make install once per checkout (or worktree) to install the pre-push hook that runs lint, tests, and coverage checks before every push.

License

MIT — see LICENSE. Exception: bpf/vmlinux.h is generated from the Linux kernel's BTF info and is distributed under the kernel's GPL-2.0 license instead.

References

Directories

Path Synopsis
cmd
tinytap command
Command tinytap attaches the BPF program, drains its ringbuf, parses the per-syscall events into HTTP messages, pairs requests with responses, and feeds the result to an output sink.
Command tinytap attaches the BPF program, drains its ringbuf, parses the per-syscall events into HTTP messages, pairs requests with responses, and feeds the result to an output sink.
internal
config
Package config loads tinytap's TOML session settings — the file-based replacement for the --output/-v/--verbose flags (#217).
Package config loads tinytap's TOML session settings — the file-based replacement for the --output/-v/--verbose flags (#217).
doctor
Package doctor runs read-only preflight checks that classify why tinytap might not start, or might start with reduced capability, on a machine that isn't the primary dev VM (#209).
Package doctor runs read-only preflight checks that classify why tinytap might not start, or might start with reduced capability, on a machine that isn't the primary dev VM (#209).
events
Package events defines the shape of a single observation from BPF and the protocol-agnostic decoder that turns a ringbuf record into an Event.
Package events defines the shape of a single observation from BPF and the protocol-agnostic decoder that turns a ringbuf record into an Event.
loader
Package loader owns the BPF program lifecycle: lock memory, load the generated bindings, attach every tracepoint the program declares, and expose a ringbuf.Reader for userspace consumption.
Package loader owns the BPF program lifecycle: lock memory, load the generated bindings, attach every tracepoint the program declares, and expose a ringbuf.Reader for userspace consumption.
loader/bpf
Package bpf holds the bpf2go-generated bindings for the kernel-side program in bpf/tinytap.bpf.c.
Package bpf holds the bpf2go-generated bindings for the kernel-side program in bpf/tinytap.bpf.c.
loader/bpf/fixture
Package fixture holds the bpf2go-generated bindings for the integration-test fixture program in fixture.bpf.c.
Package fixture holds the bpf2go-generated bindings for the integration-test fixture program in fixture.bpf.c.
output
Package output defines the seam between tinytap's capture pipeline and how observations are rendered.
Package output defines the seam between tinytap's capture pipeline and how observations are rendered.
output/stdout
Package stdout renders the capture pipeline's output as a line-oriented HTTP exchange log (#63): one self-contained summary line per paired request/response, written to stdout.
Package stdout renders the capture pipeline's output as a line-oriented HTTP exchange log (#63): one self-contained summary line per paired request/response, written to stdout.
output/tui
Package tui renders the capture pipeline as a live Bubble Tea table: one row per paired HTTP exchange, scrolling in as exchanges are captured.
Package tui renders the capture pipeline as a live Bubble Tea table: one row per paired HTTP exchange, scrolling in as exchanges are captured.
proc
Package proc looks up process metadata from a /proc-style filesystem.
Package proc looks up process metadata from a /proc-style filesystem.
protocols/http
Package http parses HTTP/1.x messages from BPF observation streams, pairs requests with responses on the same (pid, fd), and renders the one-line summary the demo emits.
Package http parses HTTP/1.x messages from BPF observation streams, pairs requests with responses on the same (pid, fd), and renders the one-line summary the demo emits.
tls
Package tls locates the OpenSSL/BoringSSL shared library loaded by a traced process and confirms it exports the symbols tinytap needs to hook (SSL_read, SSL_write, SSL_set_fd) in order to capture TLS plaintext without reading OpenSSL's internal struct layout.
Package tls locates the OpenSSL/BoringSSL shared library loaded by a traced process and confirms it exports the symbols tinytap needs to hook (SSL_read, SSL_write, SSL_set_fd) in order to capture TLS plaintext without reading OpenSSL's internal struct layout.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL