Source code: github.com/yuval1024/ebpf-https-capture-with-mcp

Agents make network calls. What if we want to record the traffic?

Two common approaches, and the friction each one carries:

  1. Inject JavaScript into the browser to record traffic. Application-layer change, required per browser; also harder with Chrome Manifest V3.
  2. MITM (mitmproxy is the standard one). Terminates the connection and opens a new one — changes the TLS fingerprint. Also requires installing a custom CA in the system.

Solution — eBPF (and wrap with MCP)

eBPF is passive. It listens to traffic by attaching to OpenSSL functions before encryption and after decryption — OpenSSL’s SSL_write (just before encrypt) and SSL_read (just after decrypt). Nothing changes on the wire.

This is what gojue/ecapture does:

  • Passive — the real client does not change TLS fingerprint.
  • System-wide — captures all processes on the host that use libssl (with caveats below).
  • No CA install required.

Adding MCP for agent-native communication

We wrap ecapture in an MCP server so agents can drive capture as tool calls instead of CLI invocations. Code: github.com/yuval1024/ebpf-https-capture-with-mcp.

Agent → MCP → listener → process — the architecture

The agent gets tools like:

  • capture_start / capture_stop / capture_status
  • list_domains, list_requests, get_request, get_pairs, search
  • watch_domain — subscribe to a domain so its traffic is persisted

Capture is host-wide, but subscriptions decide what gets saved, which reduces load on the ring buffer. We don’t want to bloat the buffer — only specific events get persisted.

Problems with eBPF

  • Doesn’t always work.
  • Captures only HTTPs. Plain HTTP is invisible because it doesn’t go through SSL libs.
  • Requires root permissions / a running daemon.

Extending eBPF for stripped binaries — using function signatures (theory only)

We can try to extend eBPF coverage to other TLS implementations. For example:

  • Go crypto/tls — an entirely separate TLS implementation in the Go runtime.
  • BoringSSL (Chrome, Node.js) — an OpenSSL fork, frequently statically linked and stripped.
  • Stripped and static binaries in general.

The method is creating “fingerprints” for the relevant functions. Borrowing from reverse engineering (IDA FLIRT, YARA rules, AV signatures): a function’s machine code is fairly stable across builds — except for the bytes that vary (relative addresses, relocations, immediates). So you build a masked byte signature: the function’s prologue as a byte pattern with the volatile bytes wildcarded.

This is relevant only if usage volume is high enough to justify the effort. Sample scenario:

  1. Harness for compiling many solutions × SSL libs × build flags, with symbols on, to learn the variant space.
  2. Write a fingerprint for the relevant functions. Example:

    # `??` masks the bytes that change build-to-build (call targets, offsets).
    signature = [55, 48, 89, e5, e8, ??, ??, ??, ??, 48, 8b, ??, ??, ??, ??, ??, c3]
    
  3. Data structure for searching signatures, e.g. MinHash (Mining of Massive Datasets, Ch. 3).
  4. Scan running targets for matching offsets and attach uprobes; cache sha256(file) → offsets so we don’t re-scan.

Other alternatives

  • “Smart” MITM that preserves the SSL fingerprint. Current MITM solutions replace the SSL stack. We should preserve it — bundle OpenSSL, BoringSSL, and maybe even wolfSSL 😅 — and have the client pick the matching SSL version.
  • Signature-based scanning for stripped binaries, as above.