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 ¶
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.
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 ¶
FuncResolver lets callers pass a closure where an UpstreamResolver is required.
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 ¶
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 (*Router) Apply ¶
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.
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 ¶
New constructs a Subsystem. Required Config fields are validated up front so misconfiguration fails fast at runed startup.
func (*Subsystem) HTTPAddr ¶
HTTPAddr returns the bound HTTP listener address. Useful for tests using port :0.
func (*Subsystem) Ready ¶
func (s *Subsystem) Ready() <-chan struct{}
Ready reports when both listeners are bound and serving.