feat: client_dial helper — open a tcp socket and run mtls handshake - #442
Merged
Conversation
small glue between the existing connectToUpstream-style raw TCP dial and the client_session.doHandshake driver from #438. callers pass an address + optional CA + optional client cert/key and get back a tagged union: .bare (plain fd, when no CA was supplied) or .session (an mTLS ClientSession wrapping the dialed fd). the union shape keeps the consumer (forwardSingleAttempt and friends) agnostic to the per-target mTLS decision — they just match on the outcome and use one of two well-defined apis. two tests on a real local TCP listener: 1. dial with no CA → .bare fd, accept on the listener, close. 2. dial with full mTLS opts → handshake completes against session_runtime.acceptServerHandshake on the listener side. production wiring (plumbing peer_mode through the L7 service registry and switching reverse_proxy reads/writes onto the union) is the next chunk — this PR just lands the helper so that chunk has a clean target.
This was referenced Jun 22, 2026
kacy
added a commit
that referenced
this pull request
Jun 23, 2026
* feat: carry peer_mode through upstream + service registry types adds tls.peer to the spec-side types so the L7 proxy can read the service's mtls posture at dial time: - Upstream gains peer_mode (defaults .off) - ServiceDefinition and ServiceState gain peer_mode (defaults .off) - ServiceSnapshot exposes peer_mode read-only - assignCompatProxyFields copies peer_mode from def to state - snapshotService propagates peer_mode - resolveUpstreamWithPolicy copies peer_mode onto each Upstream candidate and onto the selected one returned to the caller the cluster-state-DB column to persist peer_mode end-to-end (so the manifest's tls.peer reaches resolveUpstream in production) is a small follow-up schema migration; this PR's purpose is the in-memory carry so forwardSingleAttempt can dispatch on it. * feat: forwardSingleAttempt dispatches mtls upstreams to client_dial upstream.peer_mode != .off now branches into a new forwardSingleAttemptMtls: - loads the cluster CA via store.getClusterCa - calls client_dial.dial with the CA pem + sni - writes the request and drains the response through the session, no pooling (mtls sessions hold encryption state and can't share a bare-fd pool key) policy: - .require + missing cluster CA → ClusterCaMissing - .warn + missing cluster CA → log + downgrade to the plaintext dial+pool path (kept as forwardPlainAttempt, exact copy of the legacy leg) so service-to-service traffic keeps flowing while the ca_bootstrap thread catches up readResponseFromSession is the session-aware equivalent of the existing bare-fd readResponse: drains chunks until PeerClosed (the session's orderly EOF) or max_bytes, returns the bytes. three tests on a duck-typed FakeSession cover the read loop's happy path, max-bytes rejection, and immediate-close fallthrough. the live tls handshake itself is already covered end-to-end by the socketpair tests in #438/#439/#442. * feat: persist tls.peer in the services table closes the manifest → registry → upstream loop. previously peer_mode was added to all the in-memory types but production code paths could not set it because the cluster state DB had no column. with this commit: - services.peer_mode TEXT column (default 'off'); ALTER TABLE migration for existing databases (no NOT NULL so the migration is forward-safe). - ServiceRecord.peer_mode (?[]const u8 — null means 'off', keeps every existing literal compiling). - services_core.createInDb inserts peer_mode; syncConfig takes a peer_mode arg and updates the column alongside lb_policy. - syncServiceDefinitions in the manifest apply path now passes svc.tls.peer.label() through. - serviceDefinitionFromRecord parses the textual peer_mode back into the enum so registry → snapshot → Upstream carries it cleanly. with this in place: a manifest with tls.peer = 'require' on a service flows end-to-end — orchestrator writes 'require' to the services row, the registry snapshot reads it back as .require, resolveUpstream copies it onto Upstream, and forwardSingleAttempt dispatches mtls upstreams to client_dial (the changes from the previous commits). one new round-trip test (syncConfig persists peer_mode) confirms the column behaves and updates apply.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
summary
third PR-5 chunk (after #440, #441). adds the small glue that turns
"connect to an upstream over TCP" into "connect and optionally run an
mTLS handshake, returning a session". the L7 proxy wiring (chunk 4)
calls this from
forwardSingleAttempt.self-contained; no proxy behavior change.
what's in here
new
src/tls/client_dial.zigexposing:Optionscarries upstream addr/port, optional CA pem (null ⇒ stayplaintext), optional client cert/key, optional SNI / expected SAN, and
the current unix time. the result is a tagged union so the caller
matches once and uses one of two clean apis — no fd-vs-session
conditionals threaded through every read/write call.
also adds a small
parseIpv4helper (nostd.net.Addressdependencyto keep the dial path lean) with positive + negative tests.
tests
real TCP listener on
127.0.0.1:<ephemeral>for both cases:.bare, listener accepts.acceptServerHandshakeon the listener side;dialreturns.session; both sides complete cleanly.plus two
parseIpv4truth-table tests.not in this PR
peer_modethroughservice_registry_runtimesoresolveUpstreamWithPolicyknows which upstreams are mTLSreverse_proxy.forwardSingleAttempt's read/write loop tomatch on the dial outcome (the bigger change — touches the
upstream-pool path too)
yoq cert service <name>CLIstill no auto-merge — security-sensitive wiring.