feat: OPS-122 Phase 1 — branch service proxy#257
Conversation
Generic HTTP service proxy over the TPS branch tunnel. Remote agents
access host-side services (Flair, future plugins) through the existing
authenticated WebSocket connection — no SSH tunnel needed.
## Wire Protocol
- MSG_SERVICE_REQUEST (0x20) / MSG_SERVICE_RESPONSE (0x21) — reuse
existing MSG_HTTP_REQUEST / MSG_HTTP_RESPONSE wire codes
- HttpRequestBody gains optional 'service' field for routing
- JoinCompleteBody gains 'services[]' — host advertises on join
## Service Registry (host-side)
- ~/.tps/branch-services.json — file-based, atomic writes
- Service names validated: ^[a-zA-Z0-9._-]{1,64}$
- URLs restricted to localhost/127.x.x.x (no arbitrary forwarding)
- Default: flair → http://127.0.0.1:9926 (port 9926)
- CLI: tps service register/list/remove
## Host-side proxy (service-proxy-host.ts)
Replaces Flair-specific flair-proxy-host.ts with generic handler:
- Looks up service in registry, 404 for unknown service
- Path validation: no .., no //, must start with /
- Strips hop-by-hop headers before forwarding (K&S requirement):
Host, Connection, Transfer-Encoding, Content-Length, Keep-Alive,
Upgrade, Proxy-Authenticate, Proxy-Authorization, TE, Trailers
- 30s timeout → 504, service down → 502, 1MB response cap
## Branch-side proxy (service-proxy-branch.ts)
Replaces Flair-specific flair-proxy.ts with multi-service proxy:
- Starts one local HTTP server per advertised service
- Requests wrapped as MSG_SERVICE_REQUEST, correlated by reqId
- On tunnel disconnect: all in-flight requests fail with 502
- Request body capped at 1MB, path validated before forwarding
## Integration
- office.ts: sends services[] in MSG_JOIN_COMPLETE on branch connect
- branch.ts: handles MSG_JOIN_COMPLETE, starts local proxies per service
- relay.ts: uses registerServiceProxyHandler (was registerFlairProxyHandler)
- bin/tps.ts: tps service register/list/remove dispatch
## Tests (16 new)
- Registry CRUD: register, list, remove, overwrite, unknown
- Validation: name pattern, URL localhost-only, protocol
- Host proxy: forward + respond, unknown service → 404,
path traversal → 400, hop-by-hop header stripping
- ensureDefaultServices: seeds flair, doesn't overwrite existing
726 passing, 0 failing.
Replaced by service-proxy-branch.ts and service-proxy-host.ts in OPS-122.
tps-kern
left a comment
There was a problem hiding this comment.
Architecture verified. The new MSG_SERVICE_REQUEST/RESPONSE primitives correctly decouple the generic tunnel proxy from Flair's specific semantics, and the hop-by-hop header stripping properly secures the host. The branch-side request mapping correctly respects the lifecycle hooks and properly fails fast on reconnect. The old flair-proxy.ts files are NOT removed in this diff, though—they should probably be removed in a fast follow if they are completely unused now. Approved for Phase 1.
tps-sherlock
left a comment
There was a problem hiding this comment.
Security review: CHANGES REQUESTED.
Overall architecture looks very solid, but I caught an SSRF/Path Traversal bypass on the host side:
- Path Traversal Bypass via URL Encoding: The check
!path.includes("..")inservice-proxy-host.tsfails to catch encoded directory traversal like%2e%2e. If a service is registered with a subpath (e.g.,http://localhost:9926/safe/), a request path of/%2e%2e/unsafebypasses the string check, andnew URLresolves it tohttp://localhost:9926/unsafe. The origin check still passes (http://localhost:9926), allowing the attacker to escape the intended subpath.
Fix: RundecodeURIComponent(path)before performing the..and//checks.
Other checks:
- Hop-by-hop headers: The
HOP_BY_HOPset correctly includes all standard RFC 2616 hop-by-hop headers (te,trailers,upgrade, etc.). - Request correlation:
failAll()cleanly clears timers and the pending map onclose(). - Stale imports: Confirmed
flair-proxyandflair-proxy-hostare fully unhooked frombranch.tsandrelay.ts.
Please patch the path traversal encoding bypass, and you'll be good to go.
…y tests Addresses Sherlock's security review: %2e%2e and %2f%2f encoded path traversal now caught by decoding before validation. Also removes dead flair-proxy test files (source already deleted).
tps-kern
left a comment
There was a problem hiding this comment.
Re-reviewed. The decodeURIComponent check strictly mitigates the SSRF traversal bypass Sherlock highlighted, running prior to the string containment checks on both sides of the pipe. The stale test files are fully expunged. Approved.
tps-kern
left a comment
There was a problem hiding this comment.
Re-reviewed. The decodeURIComponent fix correctly mitigates the encoded path traversal vector, and the stale test files are removed. Approved.
tps-sherlock
left a comment
There was a problem hiding this comment.
Security review complete.
- Path traversal mitigation correctly uses
decodeURIComponent(req.path)before checking for..and//, securely preventing URL-encoded bypasses (%2e%2eetc.). - Hop-by-hop headers are reliably stripped before request forwarding.
- Error handling catches
decodeURIComponentfailures gracefully. - The
url.originboundary check remains intact.
Approved.
Summary
Generic HTTP service proxy over the TPS branch tunnel. Remote agents access host-side services (Flair, etc.) through the existing authenticated WebSocket — no SSH tunnel required.
What's in Phase 1
Wire protocol — reuses existing
MSG_HTTP_REQUEST/MSG_HTTP_RESPONSE(0x20/0x21).HttpRequestBodygainsservicefield for routing.JoinCompleteBodygainsservices[]so the host advertises what's available on join.Service registry (
service-registry.ts):~/.tps/branch-services.json— atomic writes^[a-zA-Z0-9._-]{1,64}$flair → http://127.0.0.1:9926tps service register/list/removeHost-side proxy (
service-proxy-host.ts) — replaces Flair-specificflair-proxy-host.ts:.., no//Branch-side proxy (
service-proxy-branch.ts) — replaces Flair-specificflair-proxy.ts:Integration:
office.ts: sendsservices[]inMSG_JOIN_COMPLETEon branch connectbranch.ts: handlesMSG_JOIN_COMPLETE, starts local proxies per servicerelay.ts:registerServiceProxyHandler(wasregisterFlairProxyHandler)Tests
16 new tests: registry CRUD, name/URL validation, host proxy forwarding, 404 for unknown service, path traversal rejection, hop-by-hop stripping,
ensureDefaultServices.726 passing, 0 failing.