proxy

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

Package proxy serves the Temporal WorkflowService on a local unix socket, forwarding every request to an upstream Temporal frontend over gRPC. The socket path is derived from the upstream host:port, so local workers connect without TLS while the upstream hop stays secured.

Index

Constants

This section is empty.

Variables

View Source
var Module = fx.Options(fx.Invoke(func(p ProxyParams) error {

	if p.Config.Encryption.Enabled && p.Vault == nil {
		return fmt.Errorf("encryption is enabled but no vault was provided")
	}

	// Built once (not per upstream) so its collectors register with Prometheus
	// exactly once; a per-upstream build would panic on duplicate registration.
	var encReporter *Reporter
	if p.Vault != nil {
		encReporter = NewReporter(p.Factory.ForSubsystem("encryption"))
	}

	conns := make([]*connect.Conn, 0, len(p.Config.Upstreams))

	for i := range p.Config.Upstreams {
		up := &p.Config.Upstreams[i]
		if err := up.Validate(); err != nil {
			return fmt.Errorf("invalid upstream configuration: %w", err)
		}

		// Request-independent dial options: namespace translation and outbound
		// credentials. Per-request credentials are added by the resolver.
		var dialOpts []grpc.DialOption
		rules := &up.Namespaces.Rules
		if rules.Configured() {
			dialOpts = append(dialOpts, translationDialOptions(p.Translator, rules.Remote, rules.Local)...)
		}

		cp, err := outbound.CredentialProviderFor(up.Credentials)
		if err != nil {
			return fmt.Errorf("invalid credentials for upstream %q: %w", up.Name, err)
		}
		if cp != nil {
			dialOpts = append(dialOpts, outbound.DialOptions(cp)...)
		}

		if p.Vault != nil {
			enc, err := EncryptionInterceptor(p.Config.Encryption.Enabled, p.Vault, encReporter)
			if err != nil {
				return fmt.Errorf("failed to build encryption interceptor for upstream %q: %w", up.Name, err)
			}

			dialOpts = append(dialOpts, grpc.WithChainUnaryInterceptor(enc))
		}

		res, err := upstreamResolver(up, dialOpts)
		if err != nil {
			return err
		}

		conn, err := connect.NewConn(p.Pool.ConnOrCreate, res)
		if err != nil {
			return err
		}

		conns = append(conns, conn)

		var opts []Option
		if p.Logger != nil {
			opts = append(opts, WithLogger(p.Logger))
		}

		svr, err := New(up.Listen.HostPort, conn, opts...)
		if err != nil {
			return fmt.Errorf("failed to create proxy for upstream %q: %w", up.Name, err)
		}

		p.Lifecycle.Append(fx.Hook{
			OnStart: func(context.Context) error {

				lis, err := svr.Listen(p.Context)
				if err != nil {
					return fmt.Errorf("failed to start proxy for upstream %q: %w", up.Name, err)
				}

				go func() {
					defer func() { _ = lis.Close() }()

					if err := svr.Start(p.Context, lis); err != nil {

						_ = p.Shutdowner.Shutdown(fx.ExitCode(1))
					}
				}()

				return nil
			},
			OnStop: svr.Stop,
		})
	}

	p.Lifecycle.Append(fx.StartHook(func(ctx context.Context) error {
		if err := connect.WaitReady(ctx, conns...); err != nil {
			return fmt.Errorf("upstream connection not ready: %w", err)
		}

		return nil
	}))

	return nil
}))

Module is the fx module that constructs the proxy Server from ProxyParams and binds its lifecycle to the application.

Functions

func EncryptionInterceptor

func EncryptionInterceptor(enabled bool, v Vault, r *Reporter) (grpc.UnaryClientInterceptor, error)

EncryptionInterceptor returns a unary client interceptor that opens inbound response payloads using v and, when enabled is true, seals outbound request payloads as well. Sealing is gated so encryption can be turned off for new traffic while still opening data sealed earlier: inbound decryption always runs. Each payload is sealed under the DEK for the request's namespace, read from the outgoing gRPC metadata via meta.NamespaceFrom, so the upstream never sees plaintext while local workers still exchange cleartext. On the way back only payloads this interceptor sealed (identified by the encryptionEncoding marker) are opened; anything else passes through untouched. Search attributes are skipped so they stay queryable upstream. r records the duration and result of every seal/open through VaultOp. It returns an error only if the underlying visitor interceptor cannot be constructed.

Types

type DynamicResolver

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

DynamicResolver is a connect.Resolver that renders an upstream's dial target (and optional TLS server name) per request from the local namespace and request metadata. It always reports IsStatic as false, so a connect.Conn built from it resolves lazily on every call. A non-templated hostPort renders to itself, so a DynamicResolver also serves upstreams with a fixed address. Construct one with NewDynamicResolver.

func NewDynamicResolver

func NewDynamicResolver(up *config.Upstream, opts ...ResolverOption) (*DynamicResolver, error)

NewDynamicResolver builds a DynamicResolver for up. It compiles the hostPort and TLS server-name templates (failing if either is malformed) and applies opts. By default the remote namespace equals the local one and no dial options are added; use WithRemoteNamespacer and WithOptionsFactory to change that.

func (*DynamicResolver) IsStatic

func (r *DynamicResolver) IsStatic() bool

IsStatic reports that a DynamicResolver always resolves per request.

func (*DynamicResolver) Resolve

Resolve renders the dial target and server name from ctx and returns the pool cache key, the dial target, and the dial options. The cache key combines the target and rendered server name so that two requests to the same address with different server names get distinct pooled connections. It fails with codes.Internal (naming the upstream and template) when a template fails to render, the rendered address is empty or malformed, or the options factory errors; nothing is dialed in those cases.

type Option

type Option func(*Options)

Option configures a Server via New.

func WithLogger

func WithLogger(log logger.Logger) Option

WithLogger sets the logger used by the proxy.

type Options

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

Options configures a Server at construction time.

type ProxyParams

type ProxyParams struct {
	fx.In
	Lifecycle  fx.Lifecycle
	Shutdowner fx.Shutdowner

	// Required values
	Context    context.Context
	Config     *config.Config
	Translator *protoutil.Translator
	Pool       *connect.Pool
	Vault      *crypto.Vault
	Factory    *metrics.Factory

	// Optional values
	Logger logger.Logger `optional:"true"`
}

ProxyParams collects the fx-provided dependencies needed to construct and run the proxy Server. Context, Config, Translator, and Pool are required; Logger is optional and falls back to the default used by New when not supplied. protoutil.Module provides the Translator and connect.Module provides the Pool in the assembled application.

type Reporter

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

Reporter records envelope-operation telemetry to Prometheus: each seal (encrypt) and open (decrypt) the encryption interceptor performs, timed end to end, including any KEK wrap or unwrap and any DEK cache lookup along the way. The AES-step duration alone is owned by internal/kms. The namespace label is unbounded, so handles are resolved per call via WithLabelValues rather than pre-computed. A Reporter is safe for concurrent use.

func NewReporter

func NewReporter(f *metrics.Factory) *Reporter

NewReporter builds the Prometheus-backed vault-operation Reporter. f must already be scoped to the "encryption" subsystem by the caller.

func (*Reporter) VaultOp added in v0.3.0

func (r *Reporter) VaultOp(operation, result, namespace string, seconds float64)

VaultOp records a single envelope operation and its duration.

type ResolverOption

type ResolverOption func(*DynamicResolver)

ResolverOption configures a DynamicResolver at construction.

func WithOptionsFactory

func WithOptionsFactory(f func(RouteData) ([]grpc.DialOption, error)) ResolverOption

WithOptionsFactory sets the function that produces the dial options for a resolved request. It receives the rendered host and server name via RouteData.

func WithRemoteNamespacer

func WithRemoteNamespacer(f func(string) string) ResolverOption

WithRemoteNamespacer sets the function that maps the local namespace to the remote one, making RemoteNamespace available to the templates.

type RouteData

type RouteData struct {
	template.UpstreamContext
	ResolvedServerName string
}

RouteData is passed to the options factory once a request has been resolved. It carries the template context used for rendering plus the resolved TLS server name, so the factory can build dial options (e.g. credentials whose SNI depends on the rendered server name).

type Server

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

Server proxies the Temporal WorkflowService. It re-serves an upstream frontend on a local unix socket, letting local workers connect without TLS while the upstream hop stays secured. The upstream connection(s) it forwards to are owned by the shared connect.Pool, not by this Server.

func New

func New(hostPort string, cc grpc.ClientConnInterface, opts ...Option) (*Server, error)

New constructs a Server that forwards WorkflowService traffic to the upstream reachable through cc. The local listener is a unix socket whose path is derived from hostPort. cc is typically a resolvingConn; the connection(s) it uses are owned by the shared pool, not by this Server.

func (*Server) Listen

func (s *Server) Listen(ctx context.Context) (net.Listener, error)

Listen removes any socket left behind by a prior run and binds the proxy's local unix socket, returning the listener. Binding is separate from Start so callers can bind synchronously during startup (the socket is then listening, and the OS backlogs connections) before serving in the background, ensuring no request is routed to an unbound socket.

func (*Server) Start

func (s *Server) Start(ctx context.Context, lis net.Listener) error

Start serves on lis until the proxy is stopped or ctx is cancelled. It blocks, so callers typically run it in its own goroutine after binding the listener with Listen.

func (*Server) Stop

func (s *Server) Stop(ctx context.Context) error

Stop gracefully shuts the proxy down, waiting for in-flight RPCs to complete.

type Vault

type Vault interface {
	Seal(context.Context, string, []byte) (*crypto.Message, error)
	Open(context.Context, *crypto.Message) ([]byte, error)
}

Vault seals and opens payloads using envelope encryption scoped by namespace. It is the subset of crypto.Vault the interceptor depends on.

Jump to

Keyboard shortcuts

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