Skip to content

fix: support SDK TLS edges and client-facing node origins - #194

Merged
CMGS merged 5 commits into
mainfrom
fix/sdk-tls-edge
Sep 16, 2026
Merged

CMGS merged 5 commits into
mainfrom
fix/sdk-tls-edge

Conversation

@CMGS

@CMGS CMGS commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #192.

  • Both SDKs accept HTTP(S) origins and use verified TLS for control requests and HTTP/1.1 Upgrade: silkd relays. Go accepts WithTLSConfig; Python accepts a standard ssl_context. Bare owner/peer addresses inherit the entry scheme; explicit schemes win.
  • Add server-side client_advertise, gossiped separately from internal advertise_addr. Owners, all claim/template/checkpoint redirects, and peer discovery return client origins. Preview forwarding and checkpoint peer traffic retain internal HTTP addresses.
  • Preserve port half-close using the existing data_end frame, without shutting down the Python TLS socket underneath the response stream.
  • Document per-node Caddy routing, CA trust, Upgrade passthrough and timeout requirements. Add a checksum-pinned Caddy integration to CI for both SDKs, plus endpoint/certificate/cancellation/address-projection tests.
  • Enforce strict mypy for the Python SDK. Declaration-only Go client rearrangement is a separate commit.

Verification

  • Five Go modules: race tests pass; Linux and macOS lint/format pass (10 0 issues results). ASL hard gates pass on both targets.
  • Python: 186 tests pass, ruff clean, strict mypy clean across all 9 package files.
  • Real Caddy 2.11.4 integration: Go and Python claim through node A, follow a public HTTPS owner on B, lookup, exec, relay ports, read after half-close, proxy locally and release. The fake cluster's private owner cannot resolve from the client. Go additionally verifies fork origins; certificate failures and canceled/stalled handshakes have regression coverage.
  • Real Linux/x86_64 KVM acceptance: two isolated sandboxd processes with real mesh membership and Caddy; a separate client machine forwards only the two HTTPS edge ports, not either private API port. Both SDKs pass A-to-B claim, true, lookup, real guest HTTP via port relay and local proxy, half-close and release; Go fork passes. Repeated both SDK sequences directly over plaintext HTTP on the node with client_advertise unset.
  • Hardware image: ghcr.io/cocoonstack/sandbox/rt@sha256:72eac39c87af2060ba82ac0eb15e1165ac54d513f197cf6611037324379dd456; tested sandboxd binary SHA-256: 6b141f4e0211191754259aaea214b9dec9bf9d91c286f9183a6b7484178cad67. Isolated VMs, daemons and scratch data removed after verification.

Local review is closed: no unresolved correctness findings. Existing ASL advisory predicate helpers are retained. Relative to main: production source -30 lines, tests/fixtures +583 lines, docs/config +159 lines (physical lines, including comment normalization).

Deployment contract

Each node needs its own stable client-facing origin; one randomly balanced owner hostname is insufficient. All external-client nodes must publish these origins. client_advertise is not an SDK argument, and sandboxd itself remains a plain-HTTP upstream.

The TLS edge support parsed and validated every address on every
control request and every relay dial: sdk/go went from 28 ns/1 alloc to
246 ns/4 allocs per request, and Python built a default SSL context per
Client (+5.5 ms) and a fresh urllib opener per request (+0.26 ms).
Addresses are now validated once at Connect; requests concatenate the
scheme, relay dials split the authority without url.Parse, and Python
builds its TLS context and HTTPS opener lazily on the first https use.
Measured on loopback: Go 39 ns/1 alloc per request and 19 ns/0 allocs
per dial; Python Client() 5.5 ms -> 3.5 us, info() 540 -> 279 us.

Connect keeps a bare entry address bare, http being the default scheme,
so Owner() and the origin fallback read as before on plain HTTP.

Both Go validators now reject a bare trailing '#' like the Python one
(sandboxd accepted https://node#, which every Python client refused).
Relay TLS failures raise ProtocolError like the plaintext dial. push()'s
docstring states the real contract: a truncated stream leaves dest
untouched. The Caddy test holds both edge ports until Caddy binds them.
silkdtest's duplicate PortForward case and endpointURL's unreachable
control-character clause are gone.
Connect's own http.Client used http.DefaultTransport, which keeps two
idle connections per host: a client issuing 100 concurrent claims
re-dialed about 98 of them on every burst (24.5 ms and 98 TCP dials per
burst on loopback, and the ephemeral port range ran out under sustained
load). Connect now clones the default transport with 100 idle
connections per host: 1.4 ms and one dial per burst. A caller's own
transport from WithHTTPClient keeps its settings.

The relay dial cloned a tls.Config with no ClientSessionCache, so every
RPC behind an HTTPS edge paid a full handshake. The client now owns one
LRU session cache shared by the control-plane transport and the relay
dials: 1.6 ms -> 0.6 ms per relay dial on loopback, 99% resumed. The
config is materialized once in configureTLS, so dialAgent no longer
special-cases a missing one.
@CMGS
CMGS merged commit 9423b1d into main Sep 16, 2026
2 checks passed
@CMGS
CMGS deleted the fix/sdk-tls-edge branch September 16, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sdk: clients cannot reach sandboxd through a TLS-terminating edge

1 participant