proxy

package
v0.0.0-...-3aa2c68 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package proxy is ensemble's interceptor: one process, N listeners, each fronting a real service — capturing hops, propagating trace context, and (in later tasks) injecting latency and partitioning recording sessions.

Index

Constants

View Source
const CallerHeader = "X-Ensemble-Caller"

CallerHeader is the default request header letting a caller ensemble doesn't manage (a dev-only client, another team's tool, a bare curl) self-declare its name when it has no real trace context and no config-declared CalledBy hint to fall back on. Honored only when SpanOwner has no real trace-derived answer; the resulting hop is marked Hop.Attribution = "declared" — a caller-asserted name, not ground truth, but more specific than a static config guess. Used only when Proxy.SourceHeaders is unset — an org with its own existing header convention (e.g. "X-Source-Client") sets SourceHeaders instead.

View Source
const CaptureLimit = 256 * 1024

CaptureLimit caps how much request/response body is *captured* (never how much is forwarded). Exported so other capture paths — notably core/stub, which has no upstream to stream through and so can't reuse cappedBuffer directly — cap at the same size instead of duplicating the number.

View Source
const DefaultRingBytes = 256 << 20

DefaultRingBytes is the default byte budget for the in-memory hop ring. The count cap alone is not a memory bound: 1024 hops each carrying two 256 KB bodies is half a gigabyte resident, so eviction enforces bytes AND count together — see Record.

View Source
const FallbackClient = "client"

FallbackClient is what a hop records when a client-identity header was present but its value failed validation.

It is deliberately a value, not "": an empty Client means "no client header arrived", and a malformed one is a different fact — someone IS declaring a client identity and getting it wrong. Collapsing the two would hide a misconfigured app inside the much larger population of requests that simply carry no header, which is where it would never be found.

It is equally deliberately not the offending string. Whatever a client puts in that header would otherwise reach hops.jsonl, the traffic UI, and any group-by built on Client — so the raw value is dropped at the door rather than sanitized downstream by every consumer in turn.

Variables

View Source
var DefaultClientHeaders = []string{"x-source-client", "x-local-client"}

DefaultClientHeaders are the request headers checked, in order, for the name of the client application that sent a request, when Proxy.ClientHeaders is unset. Both are conventions already common in multi-front-end stacks; a project with its own spelling sets ClientHeaders instead.

Unlike SourceHeaders there is no "off" state, and none is needed: reading two headers that are not present costs a map lookup and records nothing. A stack that does not use them simply never has a Client on its hops.

View Source
var ErrSessionActive = errors.New("session already active")

ErrSessionActive marks an id that is already registered. Wrapped, not returned bare, so a caller identifies it with errors.Is rather than string-matching — ensemble/server's handleSessionStart maps this specifically to 409 Conflict, and every OTHER Start failure (a bad proxy.host, a bind failure) is a different kind of caller mistake and maps to 400 instead.

View Source
var ServerReadHeaderTimeout = 10 * time.Second

ServerReadHeaderTimeout bounds how long a connected client may dawdle before sending its request headers — without it an idle socket holds a server goroutine forever (slowloris). Generous, because everything here is loopback: the point is reclaiming abandoned sockets, not policing latency. Exported so core/stub's server sets the same bound. A var, not a const, only so tests can shorten it — production never writes it.

View Source
var ValidClient = regexp.MustCompile(`^[a-z0-9][a-z0-9:-]{0,31}$`)

ValidClient is the shape a client identity must have: lower-case alphanumeric, then up to 31 more of the same plus ':' and '-'.

Tight on purpose. Client is an IDENTIFIER — it is grouped by, filtered on, and rendered as a label — so the charset is the set of things that are safe to do all three with. The leading character excludes '-' and ':' so a value cannot be mistaken for a flag or an empty namespace, and the 32-byte cap keeps a header nobody validates upstream from becoming an unbounded field on every hop.

Functions

func BindLoopbackCompanion

func BindLoopbackCompanion(primary net.Listener) (net.Listener, error)

BindLoopbackCompanion best-effort binds the OTHER loopback address family on primary's own port — 127.0.0.1 gets a companion ::1, and vice versa — so a client that resolves "localhost" (or is otherwise pointed at this host) via either family still reaches a working listener. This is what fixes the common default case: a proxy or replay listener configured with a bare literal IP (no explicit hostname) has, until now, only ever answered the one family it happened to bind.

Returns (nil, nil) — not an error — when primary's host is not one of the two well-known loopback literals: a hostname target already binds both families via ServeStoppable's own hostAddrs path, and this must not double-bind on top of that. A failed companion bind (no IPv6 stack, a same-port race) is also silent and non-fatal — exactly as hostAddrs' own best-effort ln6 already behaves — since the primary listener working is what every existing caller and advertised address already depends on.

Exported for retrace/cmd/retrace's replay listener, which binds its own net.Listener directly rather than going through ServeStoppable/Target.

func Listen

func Listen(listen string) (lns []net.Listener, advertise string, err error)

Listen binds the listener(s) for one capture-plane listen address, with this package's loopback enforcement: a hostname (anything net.ParseIP does not recognize) is resolved, MUST be loopback-only (see hostAddrs), and binds both loopback families where available; a literal IP binds exactly as given, with a best-effort loopback companion (see BindLoopbackCompanion). advertise is the address a client should be told — built from the CONFIGURED hostname string for a hostname, never the resolved address, so it reads back exactly as configured.

Exported so core/stub binds through the same enforcement instead of a bare net.Listen — the stub records hops into the same Recorder, so a stub reachable from a LAN interface would be the same forged-capture hazard hostAddrs documents for the proxy.

Types

type CORSPolicy

type CORSPolicy struct {
	// AllowOrigins lists the origins allowed to read a response. A single
	// "*" entry allows any origin, but only when AllowCredentials is
	// false — the Fetch spec forbids the combination, and ensemble's
	// config validation rejects it before it ever reaches here.
	AllowOrigins     []string
	AllowCredentials bool
	AllowMethods     []string
	AllowHeaders     []string
	// MaxAgeSeconds, when > 0, sets Access-Control-Max-Age.
	MaxAgeSeconds int
}

CORSPolicy is a gateway Target's cross-origin resource sharing configuration. A nil *CORSPolicy (the default) disables CORS entirely: no response headers are added and OPTIONS requests are forwarded like any other request.

type HopEvent

type HopEvent struct {
	Hop     trace.Hop
	Updated bool
}

HopEvent is what a subscription delivers: a hop, plus whether this is a re-delivery of a Seq the subscriber may already hold. Updated is true exactly once per streaming hop — the finalization Update publishes when the stream closes — and consumers upsert by Seq rather than append.

type HopRedactor

type HopRedactor interface {
	Hop(trace.Hop) (trace.Hop, error)
}

HopRedactor scrubs one hop on its way into the recorder. *trace.Redactor is the production implementation; a non-nil error means some value could not be redacted as configured (the returned hop is already fail-closed — see trace.Redactor.Hop) and Record degrades the hop rather than dropping it or killing the request.

type LatencyRule

type LatencyRule struct {
	Target  string  `json:"target"` // service name; "*" matches any
	Path    string  `json:"path"`   // prefix; longest prefix wins
	FixedMs float64 `json:"fixedMs,omitempty"`
	P50     float64 `json:"p50,omitempty"`
	P95     float64 `json:"p95,omitempty"`
	P99     float64 `json:"p99,omitempty"`
	Enabled bool    `json:"enabled"`
	// Source describes where this rule's values came from: empty for a
	// manually `set` rule, otherwise a human-readable Datadog query +
	// window (e.g. "datadog:p{P}:trace...{...} (last 60m)"). Purely
	// informational — DelayFor ignores it.
	Source string `json:"source,omitempty"`
}

LatencyRule injects artificial delay for one target+path prefix. Either FixedMs or the P50/P95/P99 distribution is used; a rule only fires when Enabled ("armed") — pulled APM profiles are stored disarmed by design.

type LatencyStore

type LatencyStore struct {
	// contains filtered or unexported fields
}

LatencyStore holds live-editable delay rules. All methods are safe for concurrent use from the proxy hot path and the control API.

func NewLatencyStore

func NewLatencyStore(uniform func() float64) *LatencyStore

func (*LatencyStore) ArmAll

func (s *LatencyStore) ArmAll(enabled bool)

ArmAll toggles every rule's Enabled flag.

func (*LatencyStore) DelayFor

func (s *LatencyStore) DelayFor(target, path string) time.Duration

DelayFor picks the winning rule for a request: an exact-target rule beats a wildcard, and within a target the longest matching path prefix wins.

func (*LatencyStore) DelayForExact

func (s *LatencyStore) DelayForExact(target, path string) time.Duration

DelayForExact is DelayFor without the "*" wildcard fallback — only a rule naming target exactly can inject delay. Used for a passthrough target: a stack-wide latency rule must not silently reach a real remote environment just because it happened to match everything else.

func (*LatencyStore) OnChange

func (s *LatencyStore) OnChange(fn func(rules []LatencyRule))

OnChange registers fn to be called, with a snapshot equivalent to Rules(), after every mutation (Set/Remove/Reset/ArmAll) — the hook a caller uses to persist rules to disk without core/proxy itself knowing anything about files. fn runs outside the store's lock, after the mutation is already visible to DelayFor/Rules; pass nil to disable. Not called for read-only methods.

func (*LatencyStore) Remove

func (s *LatencyStore) Remove(target, path string)

func (*LatencyStore) Reset

func (s *LatencyStore) Reset()

Reset removes every rule.

func (*LatencyStore) Rules

func (s *LatencyStore) Rules() []LatencyRule

Rules returns a copy, sorted for stable display.

func (*LatencyStore) Set

func (s *LatencyStore) Set(rule LatencyRule)

Set upserts a rule keyed by (target, path).

type Proxy

type Proxy struct {

	// Latency, when set, injects artificial delay per its rules. The sleep
	// happens before the upstream clock starts so recorded upstream timings
	// stay honest; the injected amount lands in Hop.InjectedDelayMs.
	Latency *LatencyStore

	// TraceHeader names a stack's own correlation header (e.g.
	// "x-local-trace-id"), read as a fallback trace id whenever a request
	// carries no real traceparent — see trace.ResolveInbound. Stack-wide,
	// not per-Target, since it's a convention the whole company's services
	// already share. Empty (the default) disables the fallback entirely,
	// preserving the exact prior behavior of always minting a fresh trace.
	TraceHeader string

	// SourceHeaders names request headers (checked in order, case-insensitive
	// per the HTTP header convention — see net/http.Header.Get) that let a
	// caller ensemble doesn't manage self-declare its name, replacing the
	// CallerHeader default entirely. The first header present on the request
	// wins, regardless of how many later entries are also present. Empty
	// (the default) falls back to checking only CallerHeader, preserving
	// prior behavior for a stack with no such convention of its own.
	SourceHeaders []string

	// ClientHeaders names request headers (checked in order,
	// case-insensitive) carrying the name of the CLIENT APPLICATION that
	// sent a request — "web", "ios", "admin". The first header PRESENT wins,
	// even if its value is malformed; see clientIdentity. Empty (the
	// default) checks DefaultClientHeaders.
	//
	// Not the same setting as SourceHeaders even though both name headers
	// that identify an origin, and they must not be merged: SourceHeaders
	// answers "who called this hop" in the service graph, is consulted only
	// when trace context has no answer, and accepts free text;
	// ClientHeaders answers "which of our front-ends started this", is read
	// unconditionally, and is validated to an identifier (see
	// trace.Hop.Client). A stack may reasonably set one and not the other.
	ClientHeaders []string

	// OnWarn receives non-fatal diagnostics from live traffic — today, a
	// malformed client identity. Nil (the default) discards them, which is
	// what keeps a library with no logger from having to grow one.
	//
	// Invoked serially, never concurrently, so a sink needs no lock of its
	// own. That is load-bearing: the natural sink is stderr.
	OnWarn func(string)
	// contains filtered or unexported fields
}

Proxy runs any number of intercept listeners inside one process. Each listener is a goroutine and a socket — per-service cost is kilobytes.

func New

func New(rec *Recorder) *Proxy

func (*Proxy) Close

func (p *Proxy) Close()

Close shuts every listener down.

func (*Proxy) Serve

func (p *Proxy) Serve(t Target) (string, error)

Serve opens the target's listener and starts intercepting immediately. It returns the bound address (useful with ":0").

func (*Proxy) ServeStoppable

func (p *Proxy) ServeStoppable(t Target) (string, func(), error)

ServeStoppable is Serve with a per-listener stop, used for ephemeral listeners like session client-edge ports.

t.Listen's host may be a literal IP (unchanged behavior: one listener, bound and advertised exactly as given — net.ParseIP recognizes it, so no resolution happens at all) or a hostname such as "localhost". A hostname is resolved and MUST be loopback-only (see hostAddrs); it binds on both loopback families where available, and the returned address is built from the CONFIGURED hostname string, never the listener's resolved address — so it reads back exactly as configured (e.g. "localhost:53221") regardless of which family a given client happens to dial. This exists for URL-bound auth schemes (DPoP/RFC 9449 and similar) whose validation compares hostnames; see design.md §6.1.2. It does not touch port selection — ":0" still picks an ephemeral port — so it does not reintroduce the shared-fixed-port design that was rejected there.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder is the single sink for captured hops: an in-memory replay ring, live fan-out to subscribers, and span-ownership tracking that lets the next hop name its caller.

func NewRecorder

func NewRecorder(opts RecorderOpts) *Recorder

func (*Recorder) ClaimSpan

func (r *Recorder) ClaimSpan(spanID, service string)

ClaimSpan registers which service opened a span, so the hop that carries it as parent can name its caller.

func (*Recorder) ClaimTrace

func (r *Recorder) ClaimTrace(traceID, session string)

ClaimTrace records which session a trace belongs to, at request start.

func (*Recorder) Close

func (r *Recorder) Close()

Close flushes the write queue and stops the writer goroutine. Call it before closing the Writer's underlying file — every shutdown path does (ensemble down, retrace capture close) — so nothing enqueued is lost. Recording after Close still works in memory; only persistence stops.

func (*Recorder) DroppedWrites

func (r *Recorder) DroppedWrites() uint64

DroppedWrites reports how many hop persists were dropped because the write queue was full (a stalled or slow disk). Surfaced in GET /api/status — a non-zero value means hops.jsonl is incomplete.

func (*Recorder) Record

func (r *Recorder) Record(h trace.Hop) trace.Hop

Record assigns the next sequence number, scrubs, retains, and fans out. Schema is stamped here — the single place, so the ring, every subscriber (API/SSE), and the NDJSON file all agree; Writer.Write stamps it too but only as a no-op safety net for callers that bypass Record.

A hop with Streaming set is retained and fanned out but NOT persisted yet: it is an open stream recorded at response-headers time, and the line on disk should be the finalized one Update writes when the stream closes — two NDJSON lines for one Seq would break every reader that treats the file as one record per hop.

func (*Recorder) SetRedactor

func (r *Recorder) SetRedactor(redactor HopRedactor)

SetRedactor swaps the redaction rules applied to every hop from this call onward — e.g. after `ensemble up` reconciles a config whose redact list changed. Already-recorded hops are not retroactively scrubbed.

func (*Recorder) Snapshot

func (r *Recorder) Snapshot() []trace.Hop

Snapshot returns the retained hops, oldest first.

func (*Recorder) SpanOwner

func (r *Recorder) SpanOwner(spanID string) string

SpanOwner resolves a span id to the service that opened it ("" if unknown — a client or an unproxied caller).

func (*Recorder) Subscribe

func (r *Recorder) Subscribe(cursor uint64) (ch <-chan HopEvent, dropped func() uint64, cancel func())

Subscribe replays retained hops with Seq > cursor, then delivers live. The returned dropped func reports how many events Record/Update have had to drop for this subscription because its buffer was full (a slow consumer) — callers that need to know their capture may be incomplete (e.g. SessionManager, for verdict purposes) should poll it. The returned cancel must be called to release the subscription.

func (*Recorder) TraceSession

func (r *Recorder) TraceSession(traceID string) string

TraceSession resolves a trace id to the session that owns it ("" if none).

func (*Recorder) Update

func (r *Recorder) Update(h trace.Hop) trace.Hop

Update finalizes a hop in place: same Seq, new content — how a streaming hop recorded at response-headers time gets its duration, final body and truncation when the stream closes. The ring slot is replaced (so Snapshot and late subscribers see the finalized hop), the finalized line is persisted, and every subscriber receives an Updated event to upsert by Seq. h.Seq must be the value Record assigned; a Seq the ring already evicted still persists and fans out — the update is real even if the in-memory slot is gone.

func (*Recorder) WriteErrors

func (r *Recorder) WriteErrors() uint64

WriteErrors reports how many hop persists failed at the io.Writer (disk full, file gone). Surfaced in GET /api/status alongside DroppedWrites.

type RecorderOpts

type RecorderOpts struct {
	// InitialSeq is the last sequence already used by a retained log. The
	// first new hop receives InitialSeq+1. Zero starts a fresh stream at 1.
	InitialSeq uint64
	// Ring is how many hops are kept in memory for replay to late
	// subscribers. Zero means a default of 1024.
	Ring int
	// RingBytes is the ring's byte budget: eviction removes oldest hops
	// until the ring is under BOTH the count cap and this. Zero means
	// DefaultRingBytes.
	RingBytes int64
	// Redactor scrubs hops on the way in. Nil records hops verbatim —
	// only acceptable in tests.
	Redactor HopRedactor
	// Writer, when set, also appends every hop as NDJSON — asynchronously,
	// through a bounded ordered queue drained by one writer goroutine, so
	// disk latency never serializes requests. Call Close before closing the
	// underlying file, or queued hops are lost.
	Writer *trace.Writer
}

RecorderOpts configures hop retention and scrubbing.

type Route

type Route struct {
	Prefix      string
	Regex       *regexp.Regexp
	Upstream    string
	StripPrefix bool
	Rewrite     string
	// CORSPassthrough, when true, makes handler skip the gateway's own CORS
	// header injection and preflight short-circuit for a request matching
	// this route: OPTIONS forwards upstream like any other method, and
	// whatever the backend returns (its own CORS headers, or none) passes
	// through unmodified. For a backend that already owns its CORS (e.g. a
	// framework with CORS middleware built in) sitting behind the same
	// gateway as one that doesn't — the common "mixed backends" case a
	// single Target.CORS policy can't express. Inert (no-op, no error) on a
	// route whose gateway has no CORS policy at all.
	CORSPassthrough bool
}

Route is one rule of a gateway Target: requests matching it are forwarded to Upstream. A route matches either by Prefix (path-segment rooted, longest-prefix-wins) or by Regex (matched against the full path, no implicit anchoring — write ^ / $ yourself), never both; see resolve for how the two kinds combine. Prefix is "/"-rooted; a trailing slash is ignored (so "/cart" and "/cart/" are the same rule) except for the bare "/" catch-all, which matches every path.

StripPrefix forwards the path with the matched prefix removed (an empty remainder becomes "/"); the query string is always preserved. StripPrefix only applies to Prefix routes — a Regex route always forwards the path unmodified unless Rewrite is set.

Rewrite replaces the matched portion of the path instead of just stripping it — the piece strip_prefix can't express (e.g. /v1 forwarded as /internal/v1, not just /). On a Prefix route it replaces the matched prefix, remainder appended (mutually exclusive with StripPrefix — set by at most one). On a Regex route it's a regexp.ReplaceAllString template ($1, $2, ...) applied to the whole path, so only the matched substring changes and the rest of the path is untouched; empty Rewrite leaves a Regex route's path unmodified, as before.

type Session

type Session struct {
	ID       string
	EdgeAddr string
	// contains filtered or unexported fields
}

Session is one recording run: an ephemeral client-edge listener that stamps every entering request with retrace-run baggage, plus the hops partitioned to it and a capture-trust verdict.

func (*Session) DroppedHops

func (s *Session) DroppedHops() uint64

DroppedHops returns how many of this session's hops were discarded rather than kept. See the field comment for what counts.

func (*Session) Hops

func (s *Session) Hops() []trace.Hop

Hops returns the hops attributed to this session so far, oldest first.

func (*Session) Verdict

func (s *Session) Verdict() (trace.Verdict, []string)

Verdict returns the session's capture-trust rating and its reasons.

type SessionManager

type SessionManager struct {
	// contains filtered or unexported fields
}

SessionManager partitions the recorder's hop stream into sessions and detects propagation gaps. One subscription goroutine sees every hop in sequence order:

  • a hop carrying retrace-run baggage goes to its session (and the session learns the hop's trace id);
  • a hop WITHOUT session baggage whose trace id belongs to a session is a proven gap — some service forwarded traceparent but dropped baggage — and degrades that session, naming the offender;
  • a hop with no inherited context arriving at a non-entry target while sessions are active is unattributable under parallelism and marks every active session suspect (it may equally be ambient traffic — which is why this is "suspect", not "degraded").

func NewSessionManager

func NewSessionManager(p *Proxy, rec *Recorder, entryTargets []string) *SessionManager

NewSessionManager starts the partitioning loop. entryTargets names the services clients call directly (context-less arrivals there are normal).

func (*SessionManager) Close

func (m *SessionManager) Close()

Close ends every session and stops the partitioning loop.

func (*SessionManager) End

func (m *SessionManager) End(id string) *Session

End closes the session's edge listener and detaches it from routing. Returns the finalized session, or nil if unknown. The session is retained in the bounded ended map so hops still in flight when the map entry vanished land on droppedHops instead of nowhere — see the ended field's comment.

func (*SessionManager) Get

func (m *SessionManager) Get(id string) *Session

Get returns id's active session, or nil if no session is active under that id (never started, or already ended). Used by read-only consumers (e.g. the control API's hop-tail endpoint) that must not end the session as a side effect of looking it up.

func (*SessionManager) Start

func (m *SessionManager) Start(id, entryName, entryUpstream, host string, port int) (*Session, error)

Start registers a session and opens its client-edge listener: an extra intercept port fronting entryUpstream that stamps retrace-run baggage on everything entering it. host is the hostname the listener binds on AND is advertised as; empty means the default, "127.0.0.1" — unchanged behavior. A non-default host must resolve loopback-only, enforced by ServeStoppable (see design.md §6.1.2's proxy.host addendum), not re-validated here. port is the fixed TCP port to bind; zero means the default, an OS-chosen ephemeral port — unchanged behavior. A non-zero port already in use surfaces ServeStoppable's own bind error unchanged; this never silently falls back to a different port (design.md §6.1.2's proxy.port addendum).

The duplicate-id check runs BEFORE the edge listener binds. Binding first meant a colliding Start briefly held a fixed proxy.port the live session was about to need back, and with an ephemeral port it opened a listener whose only future was being torn down — a caller retrying a 409 in a loop could starve the port it was colliding over. The check is repeated under the lock after the bind: two concurrent Starts with the same never-before-seen id both pass the pre-check, and the second one must still lose.

type Target

type Target struct {
	Name     string // logical service name, becomes Hop.To
	Listen   string // e.g. "127.0.0.1:7003"; ":0" picks an ephemeral port
	Upstream string // base URL of the real service, e.g. "http://localhost:8003"
	// InjectBaggage entries are forced into the trace context of every
	// request through this listener — how a session's client-edge port
	// stamps retrace-run without the client knowing anything about it.
	InjectBaggage map[string]string
	// Routes, when non-empty, makes this listener a gateway: the upstream
	// is chosen per request by longest path-prefix match (see Route) and
	// Upstream is unused. A request matching no route is answered 404 and
	// still recorded as a hop, so a mis-routed client shows up in the
	// traffic stream rather than vanishing.
	Routes []Route
	// CORS, when set, adds cross-origin response headers to every request
	// through this listener and answers a preflight OPTIONS request
	// directly (204, no upstream call, not recorded as a hop — it's
	// synthetic browser traffic, not application traffic). nil disables
	// CORS entirely; see CORSPolicy.
	CORS *CORSPolicy
	// CalledBy is a config-declared fallback caller hint, used only when
	// SpanOwner has no real trace-derived answer (the inbound request
	// carried no traceparent this proxy claimed, or none at all — typical
	// of an off-the-shelf backend with no trace-context propagation). One
	// entry attributes the hop directly; more than one means the hint is
	// ambiguous, so every candidate is surfaced jointly. Either way the hop
	// is marked Hop.Attribution = "inferred" so the UI never presents a
	// guess as ground truth.
	CalledBy []string
	// Passthrough marks Upstream as a real remote environment rather than a
	// local process — it arms the read-only-by-default safety rail (see
	// AllowWrites) below, and rewrites the outbound Host header to match
	// Upstream (see handler) so a passthrough target behaves exactly like a
	// client pointed at that host directly, without the caller having to
	// know or send it. TLS (below) is what handles a passthrough target
	// that happens to need mTLS.
	Passthrough bool
	// AllowWrites opts a Passthrough target out of the read-only default.
	// Ignored when Passthrough is false.
	AllowWrites bool
	// TLS, when set, is the client certificate presented while dialing
	// Upstream — for a remote edge that requires mTLS. nil (the default)
	// uses Proxy's shared transport, which presents no client certificate
	// and otherwise behaves exactly like ordinary Go stdlib TLS (default
	// verification, default SNI from the upstream URL's host).
	TLS *tls.Certificate
}

Target is one intercepted service: a listen address fronting an upstream.

Jump to

Keyboard shortcuts

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