toolbox-sidescale

module
v0.0.0-...-853a302 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: MIT

README

toolbox-sidescale

license Tests - Main Push

A Tailscale control-channel and DERP interception sidecar for go-appsec/toolbox.

Background

go-appsec/toolbox provides sectool, an MCP-driven proxy for security testing. It sits in front of an application, captures each request and response as a flow, and exposes tools (proxy_poll, flow_get, replay_send, proxy_rule_add, diff_flow, and more) that an agent uses to inspect, mutate, and replay that traffic. Out of the box it understands HTTP.

A sidecar extends sectool to a protocol it does not natively speak. The sidecar terminates the protocol, turns its messages into the same HTTP-shaped flows sectool already works with, and forwards them upstream.

Tailscale wraps its control traffic in an end-to-end encrypted Noise tunnel, so an ordinary proxy sees only opaque bytes. sidescale sits between a Tailscale client and the control server and terminates the Noise tunnel on both sides: to the client it acts as the control server, to the server it acts as a client. That puts the decrypted inner traffic in its hands, and each inner request and response lands in sectool as a normal flow you can capture, mutate, and replay.

One sidescale process covers two Tailscale surfaces:

  • Control channel (POST /ts2021), always active.
  • DERP relay (GET /derp), opt-in, enabled by adding a derp: section to the config.

It works against production Tailscale (controlplane.tailscale.com), a self-hosted coordinator such as Headscale, a Tailscale-deployed DERP server, or a local test setup. The binary is Linux-only because it links Tailscale's upstream packages.

What it captures

Control channel (Noise)

sidescale decrypts the inner HTTP/2 inside the Noise tunnel, so every /machine/* endpoint is captured as an HTTP-shaped flow. That includes POST /machine/register and the long-lived streaming POST /machine/map (captured chunk by chunk).

  • Mutate. Match/replace rules rewrite either direction on the hot path and emit a paired captured/mutated flow.
  • Replay. Re-send any captured flow. sidescale rebinds register signatures, hardware attestation, and map sessions when it holds the key material, and otherwise strips and annotates them.
  • Inject. The tailscale_inject tool originates fresh inner messages into an active or new tunnel.

Flows are tagged tailscale.tunnel, tailscale.control, or tailscale.control.map.stream.

DERP

With a derp: section present, the same process intercepts the DERP relay. Relayed packets are end-to-end encrypted and stay opaque, so what is exposed is the frame-level metadata and the decrypted login handshake: ClientInfo / ServerInfo, the control frames (peer-gone, peer-present, note-preferred, health, restarting, ping/pong), and the packet frames with their source and destination node keys, sizes, and disco classification.

  • relay mode bridges a real DERP server transparently.
  • terminate mode runs a synthetic relay with no upstream, registering each client by node key and forwarding packets between them.
  • Inject. The derp_inject tool originates frames, most usefully the server-to-client frames a hostile relay could send.

Flows are tagged tailscale.derp.tunnel or tailscale.derp.frame.

Because the flows are HTTP-shaped, agents use sectool's existing tools unchanged.

Getting started

1. Build
make build
2. Enable sidecars in sectool

Sidecars are off by default. Enable them in ~/.sectool/config.json, then start sectool:

tmp=$(mktemp); jq '.sidecars.enabled = true' ~/.sectool/config.json > "$tmp" && mv "$tmp" ~/.sectool/config.json
sectool mcp --workflow none
3. Run sidescale
bin/sidescale -config /path/to/sidescale.json
4. Start order

Start sectool, then sidescale, then the client. The Tailscale client caches the server's Noise key on its first /key fetch and never re-fetches on the hot path, so both must be up before the client makes first contact. If the client connected first, restart it (tailscale down / up).

Configuration

A JSON file with a control section (always active) and an optional derp section:

{
  "name": "sidescale",
  "sectool": { "socket": "/home/you/.sectool/sidecar.sock" },
  "control": { "...": "..." },
  "derp":    { "...": "..." }
}
Control section
Field Description
control_hosts Host patterns to claim for POST /ts2021 (default controlplane.tailscale.com). Add a :port when the substitute /key responder runs on a non-443 port
key_strategy substitute (default) serves the client a fresh responder key; borrow serves the real upstream key and requires noise_keypair_path
key_substitution Under substitute: responder (default) registers a canned /key response, or sidecar_tls terminates the /key TLS in the sidecar
noise_keypair_path Responder Noise private key. Required for borrow; optional for a persistent substitute key
upstream_overrides Map of host to host:port redirecting the upstream dial (for example a local coordinator)
machine_identity Upstream initiator identity: per_client (default, one stable key per client), shared (one key for all clients), path:<file>, or pool:<dir>
device_cert_path, device_key_path, hw_key_path Optional key material to rebind register signatures and hardware attestation on replay instead of stripping them
upstream_scheme auto (default, HTTPS/443), https, or http for a plaintext control server
upstream_pool_mode shared (default, one upstream session per machine identity) or per_client (one session per client-facing tunnel)
early_noise forward (default), suppress, or replace the upstream EarlyNoise frame

Choosing a key strategy. substitute is the only strategy that works against an upstream whose key you do not hold, including production controlplane.tailscale.com. borrow is for an operator-controlled coordinator whose Noise private key you can supply. It trades stealth for exact real-key fidelity. A minimal borrow config against a local coordinator:

"control": {
  "control_hosts": ["ts.test"],
  "key_strategy": "borrow",
  "noise_keypair_path": "/home/you/noise_private.key",
  "upstream_overrides": { "ts.test": "127.0.0.1:8443" },
  "machine_identity": "per_client"
}
DERP section

Present this section to enable the DERP surface alongside control.

Field Description
derp_hosts Host patterns to claim for GET /derp, as host or host:port (default port 443). Required
relay_mode relay (default) bridges a real DERP server; terminate runs a synthetic relay with no upstream
server_key Client-facing server key: substitute (default, fresh key) or borrow (requires server_keypair_path)
server_keypair_path DERP server node private key. Required for borrow
node_identity Upstream client identity (relay only): per_client (default), shared, path:<file> for a transparent relay, or pool:<dir>
upstream_overrides Map of host to host:port for the upstream DERP dial
cert_name_sans Additive leaf SANs, for a terminate host whose DERP map pins a plain CertName
dup_policy Duplicate-key policy in terminate mode: last_writer (default) or disable_fighters
mesh_key Mesh key for mesh impersonation (injecting mesh frames or presenting a mesh identity the client did not)

relay mode bridges a real DERP server. With node_identity: per_client the sidecar presents a fresh stable key per client, which captures the client-to-sidecar leg, handshake, and metadata cleanly but is not a fully transparent relay: peer return paths keyed to the client's real node key do not survive. Use path:<file> with the client's node private key to preserve addressing end to end.

"derp": {
  "derp_hosts": ["ts.test"],
  "relay_mode": "relay",
  "server_key": "substitute",
  "node_identity": "per_client",
  "upstream_overrides": { "ts.test": "127.0.0.1:8443" }
}

terminate mode needs no upstream, so node_identity and upstream_overrides are ignored and derp_hosts names the synthetic host every client dials:

"derp": {
  "derp_hosts": ["derp.test"],
  "relay_mode": "terminate",
  "server_key": "substitute"
}

Reading the flows

sidescale flows nest. The /ts2021 (or /derp) upgrade is a top-level flow, the tunnel envelope and upstream dial audit are its children, and the inner control requests or DERP frames are children of the tunnel envelope. proxy list is oldest-first and shows only top-level flows, so drill down by parent:

sectool proxy list --limit 3000 | grep /ts2021 | tail        # newest /ts2021 upgrade
sectool proxy list --parent-flow-id <ts2021_flow_id>         # dial audit + tunnel envelope
sectool proxy list --parent-flow-id <tunnel_flow_id>         # register + map children
sectool proxy get  <tunnel_flow_id>                          # decrypted handshake headers
sectool proxy get  <register_flow_id>                        # decrypted RegisterRequest body

Directories

Path Synopsis
Command sidescale is the Tailscale control-channel + DERP MITM sidecar: it registers with sectool as a protocol adapter and mediates the ts2021/DERP surfaces.
Command sidescale is the Tailscale control-channel + DERP MITM sidecar: it registers with sectool as a protocol adapter and mediates the ts2021/DERP surfaces.

Jump to

Keyboard shortcuts

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