feat: l7 proxy dispatches mtls upstreams to client_dial - #444
Merged
Conversation
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.
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.
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
PR-5 final wiring (after #440, #441, #442, #443). teaches the L7 reverse
proxy's outbound
forwardSingleAttemptto dispatch onupstream.peer_modeand useclient_dial.dial(#442) when mTLS is inplay. with this PR, the data-plane code path is complete — once a
service's
tls.peerreaches the service-registry state (the oneremaining cluster-DB schema migration), inbound and outbound legs
both run mTLS end-to-end.
what's in here
split into two commits for review:
1. carry
peer_modethrough the spec-side typesUpstreamgains apeer_modefield (default.off)ServiceDefinition,ServiceState,ServiceSnapshotgainpeer_modeand the apply/snapshot paths propagate itresolveUpstreamWithPolicycopiespeer_modefrom the servicesnapshot onto every candidate
Upstreamand onto the returned one2. dispatch + mTLS attempt in the reverse proxy
forwardSingleAttemptearly-branches onupstream.peer_mode != .offinto a new
forwardSingleAttemptMtls:store.getClusterCa)client_dial.dial(ca_cert_pem=...)state — sharing one across requests would corrupt the stream)
.require⇒ returnserror.ClusterCaMissing.warn⇒ logs and falls back to plaintext via the newforwardPlainAttempthelper (an exact copy of the legacy legof
forwardSingleAttempt, so service traffic keeps flowingwhile the bootstrap thread catches up)
readResponseFromSession: session-aware drain that readsuntil
error.PeerClosed(the session's orderly EOF) ormax_bytesdesign notes
(endpoint_id, address, port)and stores raw fds. mixing fds and TLS sessionsunder one key would yield catastrophic checkouts (a session's
encryption counters can't be picked up by a fresh handshake at
the same address). a session pool would need a separate per-target
map and a "is this session still healthy" check; left as a follow-up
once we see real throughput numbers.
.warnfalling back to plaintext is thespec's intent — it's the migration mode that proves the rest of the
stack works before flipping a service to
.require. when the CA ispresent, both
.warnand.requirerun the same handshake.socket_helpers.connectToUpstreamdirectly. those paths get thesame treatment in a follow-up once we have HTTP/1.1 mTLS in
production.
tests
three new tests on
readResponseFromSessionagainst a duck-typedFakeSession(same shapeclient_session.ClientSessionexposes):ResponseTooLargewhenmax_bytesis exceededthe live mTLS handshake itself is already exhaustively covered by:
client_session.zig(feat: tls 1.3 client session driver #438): client happy + 2rejects
session_runtime.zig(feat: server-side mtls — certificaterequest + client-cert verify #439): server happy +empty-cert reject + warn-mode accept
client_dial.zig(feat: client_dial helper — open a tcp socket and run mtls handshake #442): bare + mTLS round-tripwhat's deferred (and why)
peer_mode— the bridge from themanifest loader → service store row → service registry. without
it,
Upstream.peer_modestays.offin production. small dedicatedmigration: add a column, update
services_types.zig+ the raftINSERT/UPDATE SQL builders, copy through in
serviceDefinitionFromRecord. one-shot schema PR; out of scope forthis one.
mTLS has burn-in.
verification
tools/panic_audit.shcleanzig fmt --checkcleanstill no auto-merge — security-sensitive data-plane.