ingress

package
v0.0.1-dev.137 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package ingress implements the edge-node ingress controller for RUNE-066. It owns :80 and :443 on edge nodes and:

  • serves HTTP-01 ACME challenges so the orchestrator can complete certificate issuance against Let's Encrypt;
  • routes plain HTTP and TLS-terminated HTTPS traffic to backend services by Host header;
  • hot-reloads TLS certificates without restart via tls.Config.GetCertificate.

The package is split into:

  • router.go — host -> Route table; thread-safe Apply / Match
  • challenge.go — in-memory ChallengeStore for ACME HTTP-01
  • cert_loader.go — CertStore-backed dynamic TLS GetCertificate
  • server.go — HTTP + HTTPS net.Listener Subsystem

Single-node v1 deliberately defers nftables / IPVS path; the listener handles requests directly. Performance is acceptable because TLS termination is local CPU-bound work, and the only upstream hop is a localhost dial to the data-plane proxy.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func JoinHostPort

func JoinHostPort(host string, port int) string

JoinHostPort is a tiny convenience for callers building dial targets without importing net.

Types

type CertLoader

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

CertLoader fronts an acme.CertStore with an in-process LRU-style cache so TLS handshakes do not hit Badger on every connection.

Reload(ctx, host) refreshes the cached entry; the orchestrator calls Reload after a successful Set on the underlying CertStore. Hot-reload is therefore push-based (orchestrator -> loader) with a pull-based fallback if the cache misses.

func NewCertLoader

func NewCertLoader(store acme.CertStore) *CertLoader

NewCertLoader builds a loader backed by store.

func (*CertLoader) Forget

func (l *CertLoader) Forget(host string)

Forget removes the cached cert for host.

func (*CertLoader) GetCertificate

func (l *CertLoader) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate satisfies tls.Config.GetCertificate. It is called by the TLS stack on every handshake.

func (*CertLoader) Reload

func (l *CertLoader) Reload(ctx context.Context, host string) error

Reload fetches the latest cert for host from the store and updates the cache. Errors include "no cert exists yet" (ok = nil cert, nil err — Reload then leaves the cache empty so handshakes fail with a clear error rather than silently serving a stale cert).

type ClientCARegistry

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

ClientCARegistry holds the per-host client-CA pools that drive inbound mTLS. The ingress controller writes entries (resolved from each exposed service's clientCert.caSecret); the TLS listener reads them at handshake time via ConfigFor. Concurrency-safe; reads are the hot path (every handshake to an mTLS host), writes happen on reconcile.

A host with an entry requires + verifies a client cert against its pool. A host with no entry uses the default config (no client auth) — so removing clientCert from a service disables mTLS on the next reconcile.

func NewClientCARegistry

func NewClientCARegistry() *ClientCARegistry

NewClientCARegistry returns an empty registry.

func (*ClientCARegistry) ConfigFor

func (r *ClientCARegistry) ConfigFor(hello *tls.ClientHelloInfo, base *tls.Config) *tls.Config

ConfigFor implements the per-SNI branch of tls.Config.GetConfigForClient. Client-cert verification is negotiated during the handshake — before the HTTP Host header exists — so the only routing signal is SNI. When the SNI host has a registered CA pool, return a clone of base that requires and verifies a client cert against it; otherwise return nil so the TLS stack keeps using base (no client auth). base must carry GetCertificate so the clone still serves the server cert.

func (*ClientCARegistry) Forget

func (r *ClientCARegistry) Forget(host string)

Forget removes any client-CA pool for host (disabling mTLS for it).

func (*ClientCARegistry) HasPool

func (r *ClientCARegistry) HasPool(host string) bool

HasPool reports whether a client-CA pool is registered for host. The listener uses this to fail closed: a route that requires mTLS but has no pool loaded (e.g. a misconfigured caSecret) must be refused, not served unauthenticated. Nil-safe.

func (*ClientCARegistry) Set

func (r *ClientCARegistry) Set(host string, pool *x509.CertPool)

Set installs (or replaces) the client-CA pool for host. host is normalized to match handshake SNI lookups.

type Config

type Config struct {
	// Router is the live host -> service table. Required.
	Router *Router

	// Challenges serves HTTP-01 ACME challenge tokens. Required.
	// Reads come on the HTTP listener; writes are made by the
	// ACME orchestrator.
	Challenges *MemChallengeStore

	// Certs is the TLS certificate loader. Required.
	Certs *CertLoader

	// ClientCAs holds per-host client-CA pools for inbound mTLS
	// (origin hardening). Optional; nil disables client-cert
	// verification entirely. When set, hosts with a registered pool
	// require + verify a client cert at the handshake.
	ClientCAs *ClientCARegistry

	// HTTPAddr is the bind address for the plain HTTP listener.
	// Default ":80". Use ":8080" or similar in dev mode to avoid
	// requiring CAP_NET_BIND_SERVICE.
	HTTPAddr string

	// HTTPSAddr is the bind address for the HTTPS listener.
	// Default ":443". Use ":8443" in dev mode. Empty disables TLS.
	HTTPSAddr string

	// UpstreamResolver maps (namespace, service, port) to a dial
	// target ("ip:port"). Returning an empty string means the
	// service is not currently reachable; the listener will return
	// 503. The data plane normally satisfies this via the local
	// VIP (RUNE-041); a stub returning vip:port is fine.
	UpstreamResolver UpstreamResolver

	// Logger is the structured logger. Defaults to "ingress".
	Logger log.Logger

	// ReadyTimeout caps how long Start blocks waiting for the
	// listeners to bind. Defaults to 5s.
	ReadyTimeout time.Duration
}

Config bundles the dependencies the ingress Subsystem needs.

type FuncResolver

type FuncResolver func(namespace, service string, port int) (string, bool)

FuncResolver lets callers pass a closure where an UpstreamResolver is required.

func (FuncResolver) Resolve

func (f FuncResolver) Resolve(ns, svc string, port int) (string, bool)

Resolve calls f.

type MemChallengeStore

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

MemChallengeStore is a goroutine-safe in-memory ChallengeStore suitable for single-node deployments. Multi-node will replace this with a SecretRepo-backed store so every edge node serves the same token (RUNE-066b).

func NewMemChallengeStore

func NewMemChallengeStore() *MemChallengeStore

NewMemChallengeStore returns an empty MemChallengeStore.

func (*MemChallengeStore) Delete

func (s *MemChallengeStore) Delete(token string)

Delete removes the token. Safe to call when token is not present.

func (*MemChallengeStore) Get

func (s *MemChallengeStore) Get(token string) (string, bool)

Get returns the keyAuth for token. ok=false when absent.

func (*MemChallengeStore) Put

func (s *MemChallengeStore) Put(token, keyAuth string)

Put stores keyAuth under token.

type Route

type Route struct {
	// Host is the lowercased exact-match hostname.
	Host string
	// Namespace + Service identify the upstream service.
	Namespace string
	Service   string
	// Port is the service-level port to dial.
	Port int
	// Path is an optional path prefix; empty matches all.
	Path string
	// AllowCIDRs, when non-empty, restricts inbound connections to these
	// parsed source networks (matched against the real TCP peer). Empty =
	// no restriction. Parsed once at Apply time so the request path does no
	// parsing. See RUNE-0XX-Expose-Origin-Hardening-Design.md.
	AllowCIDRs []*net.IPNet
	// RequireClientCert is true when the service declares expose.clientCert
	// (inbound mTLS). Carried on the route — independent of whether the CA
	// pool has loaded — so the listener can fail CLOSED: refuse plaintext
	// (:80) and refuse :443 when the per-host CA pool isn't registered yet,
	// instead of serving the origin unauthenticated.
	RequireClientCert bool
}

Route is the resolved upstream for a Host header at request time. The router does not own selection of a specific endpoint — that is the data plane's job. The router only resolves Host -> service.

func (Route) PeerAllowed

func (rt Route) PeerAllowed(peerIP net.IP) bool

PeerAllowed reports whether a connection from peerIP may proceed under this route's source-IP allowlist. An empty allowlist means "no restriction" (allow all) — never deny-all. A non-nil allowlist with no match denies. A nil/unparseable peerIP denies when an allowlist is set (fail closed: we can't prove the source is trusted).

type Router

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

Router is a host-keyed lookup for resolved upstream routes. Apply replaces the entire route table atomically. Match is wait-free for readers (RWMutex).

func NewRouter

func NewRouter() *Router

NewRouter returns an empty Router.

func (*Router) Apply

func (r *Router) Apply(routes []Route)

Apply replaces the route table with routes. The input is copied; the caller may reuse the slice. Hosts are normalized to lowercase and stripped of leading dots.

func (*Router) Hosts

func (r *Router) Hosts() []string

Hosts returns a sorted snapshot of all hostnames in the table. Useful for the certificate loader to know what to pre-fetch.

func (*Router) Match

func (r *Router) Match(host, path string) (Route, bool)

Match returns the first route whose host (and optional path prefix) match. ok=false when no route matches.

type Subsystem

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

Subsystem implements the agent.Subsystem contract for the ingress controller. Lifecycle:

  • Start binds both listeners and reports ready once both bind successfully (or, if HTTPSAddr is empty, only HTTP).
  • Stop closes the listeners and waits for in-flight handlers to drain (capped at 30s).

func New

func New(cfg Config) (*Subsystem, error)

New constructs a Subsystem. Required Config fields are validated up front so misconfiguration fails fast at runed startup.

func (*Subsystem) HTTPAddr

func (s *Subsystem) HTTPAddr() string

HTTPAddr returns the bound HTTP listener address. Useful for tests using port :0.

func (*Subsystem) HTTPSAddr

func (s *Subsystem) HTTPSAddr() string

HTTPSAddr returns the bound HTTPS listener address.

func (*Subsystem) Name

func (s *Subsystem) Name() string

Name returns the subsystem identifier used in logs and metrics.

func (*Subsystem) Ready

func (s *Subsystem) Ready() <-chan struct{}

Ready reports when both listeners are bound and serving.

func (*Subsystem) Start

func (s *Subsystem) Start(ctx context.Context) error

Start binds both listeners and runs them in background goroutines. Returns the first bind error.

func (*Subsystem) Stop

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

Stop closes the listeners and waits for handlers to drain. Always honors a 30s drain cap regardless of the supplied ctx so a panicked handler does not wedge runed shutdown forever.

type UpstreamResolver

type UpstreamResolver interface {
	Resolve(namespace, service string, port int) (target string, ok bool)
}

UpstreamResolver returns the dial target for a service+port.

Jump to

Keyboard shortcuts

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