Sign inSign up

iksnerd/diffwire

By iksnerd

Updated 3 months ago

Image
0

5.9K

iksnerd/diffwire repository overview

Diffwire

A headless, content-addressable local network proxy for mobile and agent workflows.

Diffwire intercepts live HTTPS traffic via MITM TLS decryption, lets you branch API payloads like Git commits, and exposes everything to AI agents via MCP. Built for complex multi-step flows — KYC, ledger mutations, embedded banking — where staging environments are too slow and static mocks are too dumb.

Source, docs, and issues: https://github.com/iksnerd/diffwire


Quick start

1. Run the engine
docker run -d --name diffwire \
  -p 18081:18081 -p 13001:13001 -p 13002:13002 \
  -v diffwire-data:/app/.diffwire \
  iksnerd/diffwire:latest -upstream https://your-staging-api.com

Open the dashboard at http://localhost:13001. You should see the Routes/Traffic workspace with an empty traffic feed and the engine status row showing all three ports green.

2. Trust the root CA

Diffwire performs MITM TLS decryption with a per-workspace root CA generated on first run. Clients reject HTTPS traffic until they trust this CA. Grab the PEM first:

curl -O http://localhost:13001/api/cert/ca.pem

Then trust it per platform:

# macOS — system keychain (sudo prompts)
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain ca.pem

# Linux (Debian/Ubuntu)
sudo cp ca.pem /usr/local/share/ca-certificates/diffwire.crt
sudo update-ca-certificates

# iOS Simulator (booted simulators)
xcrun simctl keychain booted add-root-cert ca.pem

# Android emulator / device (rooted or via Network Security Config)
adb push ca.pem /sdcard/  # then: Settings → Security → Install from storage

iOS physical devices: use the dashboard's Setup workspace (sidebar) — it generates a .mobileconfig profile and shows a QR code. Linux/macOS hosts can also click "Set as system proxy" from the same Setup workspace; the dw CLI's dw setup mirrors this on the command line but is not bundled in the Docker image.

3. Point traffic at the proxy

Set both HTTP_PROXY and HTTPS_PROXY — many clients use one but not the other.

# Host process:
export HTTP_PROXY=http://localhost:18081
export HTTPS_PROXY=http://localhost:18081

# A different Docker container on the same host:
docker run --rm \
  --add-host host.docker.internal:host-gateway \
  -e HTTP_PROXY=http://host.docker.internal:18081 \
  -e HTTPS_PROXY=http://host.docker.internal:18081 \
  your-app-image

# iOS Simulator / Android emulator: configure the proxy through the
# system network settings, pointing at your Mac's LAN IP and port 18081.
4. Verify it works
curl --proxy http://localhost:18081 https://httpbin.org/get

Refresh the dashboard. A new row appears in the Traffic workspace — click it to see decoded request + response headers, body, and timing in the inspector. If the row doesn't appear, the proxy isn't being used; if it appears but shows a TLS error, the CA isn't trusted by the client running curl.

The root CA, routes, branches, and history persist in the diffwire-data named volume across restarts. Deleting the volume regenerates a new CA — clients will need to re-trust the new one.


Ports

PortPurpose
18081MITM proxy — point your client traffic here
13001Control Plane REST API + static web dashboard
13002MCP server (Streamable HTTP, stateless) for agents

Configuration

Flag / EnvDefaultWhat it does
-upstream <url> / DIFFWIRE_UPSTREAM(required — engine warns on startup)Default upstream for unmatched routes
--project <name>defaultIsolate workspace under /app/.diffwire/<name>/
-insecure-upstreamfalseTrust self-signed upstream certs (test fixtures)

Example with env vars and an isolated project:

docker run -d --name diffwire-staging \
  -p 18081:18081 -p 13001:13001 -p 13002:13002 \
  -v diffwire-data:/app/.diffwire \
  -e DIFFWIRE_UPSTREAM=https://api.staging.example.com \
  iksnerd/diffwire:latest --project staging

What's inside

  • HTTP/1.1, HTTP/2, WebSocket, and gRPC traffic decoded inline (no .proto schema needed for gRPC v1)
  • Branch-style mock payloads with Go template helpers ({{uuid}}, {{now}}, {{randomInt min max}}, …)
  • AST-aware JSON chaos fuzzer per route
  • Breakpoints (request and response phase, edit-and-release in the UI)
  • Pact-style contract inference + schema drift detection from live traffic
  • MCP server with 5 tools so agents can author fixtures and replay sessions
  • HAR + OpenAPI import, named session record/replay, per-route JS transform hooks

Image details

  • Architectures: linux/amd64, linux/arm64
  • Base: gcr.io/distroless/static:nonroot — no shell, no package manager
  • User: nonroot (UID 65532)
  • Size: ~25 MB
  • Workspace volume: /app/.diffwire (declared VOLUME)
  • Entrypoint: /app/diffwire — extra args are forwarded as flags

Tags

TagMeaning
latestMost recent release (does not move on plain main pushes)
X.Y.ZPinned semver release — also rolled up as X.Y and X (no v prefix)
mainTip of the main branch — rebuilt on every push
sha-<short>Build pinned to a specific commit (both main pushes and tag pushes)

For reproducible deployments, pin to X.Y.Z or sha-<short>. main is a moving target; latest only changes when a new release tag is cut. Note the published tags use bare semver (e.g. 0.1.2), matching the convention of Docker Hub libraries like postgres and node — the v prefix only lives on the git tag.

GHCR mirror

The same image is mirrored at ghcr.io/iksnerd/diffwire with the same tag scheme — useful if your org standardizes on GitHub Container Registry. Both registries are updated by the same CI workflow on every push, so digests match.


Troubleshooting

  • TLS / certificate errors on the client — the CA isn't trusted. Re-run step 2 above for the specific platform you're testing from. On macOS, double-click the PEM in Finder and set it to "Always Trust" in Keychain Access if the security command was skipped.
  • Dashboard loads but no traffic appears — the client isn't actually going through :18081. Confirm with curl --proxy http://localhost:18081 https://httpbin.org/get (it should appear in the feed). If that works but your app doesn't, your app is ignoring HTTP_PROXY env vars (common with Node's fetch, Go's http.DefaultClient without ProxyFromEnvironment, etc.).
  • port already in use on docker run — another process is bound to :18081, :13001, or :13002. Either kill it or remap with -p 28081:18081 etc.
  • No -upstream configured — the engine logs a 3-line warning at boot. Explicitly matched routes still work, but unmatched HTTPS traffic has nowhere to forward to.
  • Cross-container access — the container's localhost is not the host's localhost. For another container on the same host, use --add-host host.docker.internal:host-gateway and target http://host.docker.internal:18081 (shown in step 3).
  • dw CLI not in the container — the Docker image ships only the engine binary. dw (setup helpers, named sessions, etc.) is for host-side use; install it via make build-dw from source or grab the matching tarball from the GitHub Release.

License

MIT — see LICENSE.

Tag summary

Content type

Image

Digest

sha256:661c20259

Size

14.6 MB

Last updated

3 months ago

docker pull iksnerd/diffwire