Refactor architecture: extract reconciler, add interfaces, reduce complexity, improve test coverage
Architecture: - Extract reconcileNetworking into internal/reconcile package with 7 consumer-side interfaces - Add locked modifyState helper to fix state.yaml read-modify-write race condition - Extract CrowdSec and VPN handler groups with interfaces documenting dependency surface - Replace raw map[string]any YAML manipulation with typed AddDHCPStaticLease/RemoveDHCPStaticLease - Extract Cloudflare API functions into cfClient struct, eliminating repeated auth boilerplate Complexity reduction: - haproxy.GenerateWithOpts: 50 → 6 (extracted 8 focused helpers) - reconcile.Reconcile: 42 → 11 (extracted buildRoutes, buildDNSEntries, writeHAProxyConfig) - AutheliaUpdateConfig: 25 → 10 (extracted enableAuthelia/disableAuthelia) Test coverage improvements: - reconcile: 4.5% → 56.8% (stub-based tests for route building, DNS entries, orchestration) - dnsfilter: 10.7% → 45.6% (Manager.Compile, AddList, ToggleList, custom entries) - config: 67.3% → 89.8% (DHCP static lease mutation tests)
This commit is contained in:
@@ -34,11 +34,11 @@ type CustomRoute struct {
|
||||
|
||||
// HTTPRouteBackend maps path prefixes to a specific backend with its own L7 options.
|
||||
type HTTPRouteBackend struct {
|
||||
Paths []string // path prefixes (empty = catch-all)
|
||||
Backend string // host:port
|
||||
HealthPath string // optional health check path
|
||||
Headers *domains.HeaderConfig // per-route headers
|
||||
IPAllow []string // per-route CIDR whitelist
|
||||
Paths []string // path prefixes (empty = catch-all)
|
||||
Backend string // host:port
|
||||
HealthPath string // optional health check path
|
||||
Headers *domains.HeaderConfig // per-route headers
|
||||
IPAllow []string // per-route CIDR whitelist
|
||||
AuthEnabled bool // forward-auth protection via Authelia
|
||||
AuthPolicy string // one_factor, two_factor (for access control)
|
||||
}
|
||||
@@ -82,18 +82,43 @@ func (m *Manager) GetConfigPath() string {
|
||||
|
||||
// GenerateOpts holds optional parameters for HAProxy config generation.
|
||||
type GenerateOpts struct {
|
||||
HTTPRoutes []HTTPRoute // L7 HTTP reverse proxy routes (TLS terminated by Central)
|
||||
CertsDir string // directory containing per-domain PEM files for L7 TLS
|
||||
AuthEnabled bool // global Authelia forward-auth toggle
|
||||
AuthBackend string // Authelia backend address (e.g. "127.0.0.1:9091")
|
||||
AuthDomain string // Authelia login portal domain (excluded from protection)
|
||||
AuthSessionDomain string // session cookie scope (e.g. "payne.io") — only subdomains are eligible for forward-auth
|
||||
HTTPRoutes []HTTPRoute // L7 HTTP reverse proxy routes (TLS terminated by Central)
|
||||
CertsDir string // directory containing per-domain PEM files for L7 TLS
|
||||
AuthEnabled bool // global Authelia forward-auth toggle
|
||||
AuthBackend string // Authelia backend address (e.g. "127.0.0.1:9091")
|
||||
AuthDomain string // Authelia login portal domain (excluded from protection)
|
||||
AuthSessionDomain string // session cookie scope (e.g. "payne.io") — only subdomains are eligible for forward-auth
|
||||
}
|
||||
|
||||
// GenerateWithOpts creates a complete HAProxy configuration with full options.
|
||||
func (m *Manager) GenerateWithOpts(instances []L4Route, custom []CustomRoute, opts GenerateOpts) string {
|
||||
var sb strings.Builder
|
||||
|
||||
writeGlobalDefaults(&sb, opts.AuthEnabled)
|
||||
|
||||
if len(instances) > 0 || len(opts.HTTPRoutes) > 0 {
|
||||
writeHTTPSFrontend(&sb, instances, opts)
|
||||
writeL4Backends(&sb, instances)
|
||||
if len(opts.HTTPRoutes) > 0 {
|
||||
writeL7Stack(&sb, opts)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.AuthEnabled && opts.AuthBackend != "" {
|
||||
sb.WriteString("# Authelia forward-auth backend\n")
|
||||
sb.WriteString("backend be_authelia\n")
|
||||
sb.WriteString(" mode http\n")
|
||||
fmt.Fprintf(&sb, " server s0 %s\n", opts.AuthBackend)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
writeCustomRoutes(&sb, custom)
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// writeGlobalDefaults writes the HAProxy global, defaults, stats, and HTTP redirect sections.
|
||||
func writeGlobalDefaults(sb *strings.Builder, authEnabled bool) {
|
||||
sb.WriteString(`# Wild Cloud HAProxy Configuration
|
||||
# Managed by Wild Cloud Central API — do not edit manually
|
||||
|
||||
@@ -104,7 +129,7 @@ global
|
||||
maxconn 50000
|
||||
daemon
|
||||
`)
|
||||
if opts.AuthEnabled {
|
||||
if authEnabled {
|
||||
sb.WriteString(" lua-prepend-path /etc/haproxy/lua/?.lua\n")
|
||||
sb.WriteString(" lua-load /etc/haproxy/lua/haproxy-auth-request.lua\n")
|
||||
}
|
||||
@@ -134,281 +159,278 @@ frontend http_in
|
||||
redirect scheme https code 301
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
hasL4Routes := len(instances) > 0 || len(opts.HTTPRoutes) > 0
|
||||
// writeHTTPSFrontend writes the SNI-based HTTPS frontend with ACL routing.
|
||||
// ACL ordering is critical:
|
||||
// 1. L7 HTTP exact matches → L7 termination
|
||||
// 2. L4 exact matches → instance backends
|
||||
// 3. L4 wildcard matches → instance backends
|
||||
// 4. Default → L7 termination (if any HTTP routes exist)
|
||||
func writeHTTPSFrontend(sb *strings.Builder, instances []L4Route, opts GenerateOpts) {
|
||||
sb.WriteString("# HTTPS: SNI-based L4 routing (TLS passes through to k8s traefik)\n")
|
||||
sb.WriteString("frontend https_in\n")
|
||||
sb.WriteString(" bind *:443\n")
|
||||
sb.WriteString(" mode tcp\n")
|
||||
sb.WriteString(" tcp-request inspect-delay 5s\n")
|
||||
sb.WriteString(" tcp-request content accept if { req_ssl_hello_type 1 }\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
if hasL4Routes {
|
||||
sb.WriteString("# HTTPS: SNI-based L4 routing (TLS passes through to k8s traefik)\n")
|
||||
sb.WriteString("frontend https_in\n")
|
||||
sb.WriteString(" bind *:443\n")
|
||||
sb.WriteString(" mode tcp\n")
|
||||
sb.WriteString(" tcp-request inspect-delay 5s\n")
|
||||
sb.WriteString(" tcp-request content accept if { req_ssl_hello_type 1 }\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
// ACL ordering is critical. Process in this order:
|
||||
// 1. L7 HTTP exact matches (central, wild-cloud app) → L7 termination
|
||||
// 2. L4 exact matches (payne.io, civilsociety.dev) → instance backends
|
||||
// 3. L4 wildcard matches (*.cloud.payne.io) → instance backends
|
||||
// 4. Default → L7 termination (if any HTTP routes exist)
|
||||
|
||||
// 1. L7 HTTP services — route to L7 TLS termination frontend
|
||||
if len(opts.HTTPRoutes) > 0 {
|
||||
sb.WriteString(" # L7 HTTP services (→ TLS termination)\n")
|
||||
// Exact matches first
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
if route.Subdomains {
|
||||
continue // handled below
|
||||
}
|
||||
aclName := "is_l7_" + sanitizeName(route.Name)
|
||||
fmt.Fprintf(&sb, " # service: %s\n", route.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m str %s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(&sb, " use_backend be_l7_termination if %s\n", aclName)
|
||||
// 1. L7 HTTP services — route to L7 TLS termination frontend
|
||||
if len(opts.HTTPRoutes) > 0 {
|
||||
sb.WriteString(" # L7 HTTP services (→ TLS termination)\n")
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
if route.Subdomains {
|
||||
continue
|
||||
}
|
||||
// Wildcard matches after exact
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
if !route.Subdomains {
|
||||
continue
|
||||
}
|
||||
aclName := "is_l7_" + sanitizeName(route.Name)
|
||||
fmt.Fprintf(&sb, " # service: %s\n", route.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m end .%s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m str %s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(&sb, " use_backend be_l7_termination if %s\n", aclName)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
aclName := "is_l7_" + sanitizeName(route.Name)
|
||||
fmt.Fprintf(sb, " # service: %s\n", route.Domain)
|
||||
fmt.Fprintf(sb, " acl %s req_ssl_sni -m str %s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(sb, " use_backend be_l7_termination if %s\n", aclName)
|
||||
}
|
||||
|
||||
// 2. L4 exact matches — instances with subdomains:false
|
||||
hasExact := false
|
||||
for _, inst := range instances {
|
||||
if inst.Subdomains {
|
||||
continue // handled in step 3
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
if !route.Subdomains {
|
||||
continue
|
||||
}
|
||||
hasExact = true
|
||||
aclName := "is_" + sanitizeName(inst.Name)
|
||||
fmt.Fprintf(&sb, " # service: %s\n", inst.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m str %s\n", aclName, inst.Domain)
|
||||
fmt.Fprintf(&sb, " use_backend be_https_%s if %s\n", sanitizeName(inst.Name), aclName)
|
||||
}
|
||||
if hasExact {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// 3. L4 wildcard matches — instances with subdomains:true
|
||||
sb.WriteString(" # L4 instance routes (wildcard subdomain matching)\n")
|
||||
for _, inst := range instances {
|
||||
if !inst.Subdomains {
|
||||
continue // already handled in step 2
|
||||
}
|
||||
aclName := "is_" + sanitizeName(inst.Name)
|
||||
fmt.Fprintf(&sb, " # service: %s\n", inst.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m end .%s\n", aclName, inst.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m str %s\n", aclName, inst.Domain)
|
||||
fmt.Fprintf(&sb, " use_backend be_https_%s if %s\n", sanitizeName(inst.Name), aclName)
|
||||
aclName := "is_l7_" + sanitizeName(route.Name)
|
||||
fmt.Fprintf(sb, " # service: %s\n", route.Domain)
|
||||
fmt.Fprintf(sb, " acl %s req_ssl_sni -m end .%s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(sb, " acl %s req_ssl_sni -m str %s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(sb, " use_backend be_l7_termination if %s\n", aclName)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// 4. Default → L7 termination fallback
|
||||
if len(opts.HTTPRoutes) > 0 {
|
||||
sb.WriteString(" default_backend be_l7_termination\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Instance L4 backends
|
||||
for _, inst := range instances {
|
||||
fmt.Fprintf(&sb, "# service: %s\n", inst.Domain)
|
||||
fmt.Fprintf(&sb, "backend be_https_%s\n", sanitizeName(inst.Name))
|
||||
sb.WriteString(" mode tcp\n")
|
||||
sb.WriteString(" option tcp-check\n")
|
||||
fmt.Fprintf(&sb, " server s0 %s:443 check\n", inst.BackendIP)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// L7 TLS termination backend + frontend (for HTTP routes)
|
||||
if len(opts.HTTPRoutes) > 0 {
|
||||
// Load all PEM files from the certs directory — HAProxy serves
|
||||
// the right cert per-SNI. Each service gets its own <domain>.pem.
|
||||
certPath := opts.CertsDir
|
||||
if certPath == "" {
|
||||
certPath = "/etc/haproxy/certs/"
|
||||
}
|
||||
|
||||
sb.WriteString("backend be_l7_termination\n")
|
||||
sb.WriteString(" mode tcp\n")
|
||||
sb.WriteString(" server s0 127.0.0.1:8443\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString("# L7 HTTP frontend: TLS termination + Host-based routing\n")
|
||||
sb.WriteString("frontend l7_https\n")
|
||||
fmt.Fprintf(&sb, " bind 127.0.0.1:8443 ssl crt %s\n", certPath)
|
||||
sb.WriteString(" mode http\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Host ACLs for all services
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
fmt.Fprintf(&sb, " # service: %s\n", httpRoute.Domain)
|
||||
if httpRoute.Subdomains {
|
||||
fmt.Fprintf(&sb, " acl %s hdr(host) -i -m end .%s\n", hostACL, httpRoute.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s hdr(host) -i -m str %s\n", hostACL, httpRoute.Domain)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, " acl %s hdr(host) -i %s\n", hostACL, httpRoute.Domain)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Per-route ACLs (IP whitelisting, headers) and routing rules
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
|
||||
for i, rb := range httpRoute.Routes {
|
||||
aclSuffix := fmt.Sprintf("%s_%d", routeName, i)
|
||||
|
||||
// IP whitelisting for this route
|
||||
if len(rb.IPAllow) > 0 {
|
||||
ipACL := "ip_" + aclSuffix
|
||||
fmt.Fprintf(&sb, " acl %s src %s\n", ipACL, strings.Join(rb.IPAllow, " "))
|
||||
if len(rb.Paths) > 0 {
|
||||
pathACL := "path_" + aclSuffix
|
||||
fmt.Fprintf(&sb, " acl %s path_beg %s\n", pathACL, strings.Join(rb.Paths, " "))
|
||||
fmt.Fprintf(&sb, " http-request deny if %s %s !%s\n", hostACL, pathACL, ipACL)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, " http-request deny if %s !%s\n", hostACL, ipACL)
|
||||
}
|
||||
}
|
||||
|
||||
// Request headers
|
||||
if rb.Headers != nil {
|
||||
for _, k := range sortedKeys(rb.Headers.Request) {
|
||||
fmt.Fprintf(&sb, " http-request set-header %s %q if %s\n", k, rb.Headers.Request[k], hostACL)
|
||||
}
|
||||
}
|
||||
|
||||
// Response headers
|
||||
if rb.Headers != nil {
|
||||
for _, k := range sortedKeys(rb.Headers.Response) {
|
||||
fmt.Fprintf(&sb, " http-response set-header %s %q if %s\n", k, rb.Headers.Response[k], hostACL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Authelia forward-auth directives for protected routes
|
||||
if opts.AuthEnabled && opts.AuthBackend != "" {
|
||||
authDomainACL := ""
|
||||
if opts.AuthDomain != "" {
|
||||
authDomainACL = "host_" + sanitizeName(opts.AuthDomain)
|
||||
}
|
||||
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
|
||||
// Skip auth portal domain (would cause infinite redirect)
|
||||
if hostACL == authDomainACL {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip domains outside the auth session scope — forward-auth
|
||||
// requires shared cookies, which only work within the same
|
||||
// parent domain. Other domains should use OIDC instead.
|
||||
if opts.AuthSessionDomain != "" &&
|
||||
httpRoute.Domain != opts.AuthSessionDomain &&
|
||||
!strings.HasSuffix(httpRoute.Domain, "."+opts.AuthSessionDomain) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if any route on this domain has auth enabled
|
||||
hasAuth := false
|
||||
for _, rb := range httpRoute.Routes {
|
||||
if rb.AuthEnabled {
|
||||
hasAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAuth {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(&sb, " # auth: %s\n", httpRoute.Domain)
|
||||
fmt.Fprintf(&sb, " http-request lua.auth-request be_authelia /api/authz/forward-auth if %s\n", hostACL)
|
||||
fmt.Fprintf(&sb, " http-request redirect location https://%s/?rd=https://%%[hdr(host)]%%[path] if %s !{ var(txn.auth_response_successful) -m bool }\n", opts.AuthDomain, hostACL)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Routing rules: path-specific first, then catch-all
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
|
||||
// Path-specific routes first (most specific wins)
|
||||
for i, rb := range httpRoute.Routes {
|
||||
if len(rb.Paths) == 0 {
|
||||
continue
|
||||
}
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
pathACL := fmt.Sprintf("path_%s_%d", routeName, i)
|
||||
fmt.Fprintf(&sb, " acl %s path_beg %s\n", pathACL, strings.Join(rb.Paths, " "))
|
||||
fmt.Fprintf(&sb, " use_backend %s if %s %s\n", backendName, hostACL, pathACL)
|
||||
}
|
||||
|
||||
// Catch-all route last
|
||||
for i, rb := range httpRoute.Routes {
|
||||
if len(rb.Paths) > 0 {
|
||||
continue
|
||||
}
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
fmt.Fprintf(&sb, " use_backend %s if %s\n", backendName, hostACL)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// L7 backends — one per route
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
for i, rb := range httpRoute.Routes {
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
fmt.Fprintf(&sb, "# service: %s\n", httpRoute.Domain)
|
||||
fmt.Fprintf(&sb, "backend %s\n", backendName)
|
||||
sb.WriteString(" mode http\n")
|
||||
if rb.HealthPath != "" {
|
||||
fmt.Fprintf(&sb, " option httpchk GET %s\n", rb.HealthPath)
|
||||
fmt.Fprintf(&sb, " server s0 %s check\n", rb.Backend)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, " server s0 %s\n", rb.Backend)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Authelia backend for forward-auth
|
||||
// 2. L4 exact matches
|
||||
hasExact := false
|
||||
for _, inst := range instances {
|
||||
if inst.Subdomains {
|
||||
continue
|
||||
}
|
||||
hasExact = true
|
||||
aclName := "is_" + sanitizeName(inst.Name)
|
||||
fmt.Fprintf(sb, " # service: %s\n", inst.Domain)
|
||||
fmt.Fprintf(sb, " acl %s req_ssl_sni -m str %s\n", aclName, inst.Domain)
|
||||
fmt.Fprintf(sb, " use_backend be_https_%s if %s\n", sanitizeName(inst.Name), aclName)
|
||||
}
|
||||
if hasExact {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// 3. L4 wildcard matches
|
||||
sb.WriteString(" # L4 instance routes (wildcard subdomain matching)\n")
|
||||
for _, inst := range instances {
|
||||
if !inst.Subdomains {
|
||||
continue
|
||||
}
|
||||
aclName := "is_" + sanitizeName(inst.Name)
|
||||
fmt.Fprintf(sb, " # service: %s\n", inst.Domain)
|
||||
fmt.Fprintf(sb, " acl %s req_ssl_sni -m end .%s\n", aclName, inst.Domain)
|
||||
fmt.Fprintf(sb, " acl %s req_ssl_sni -m str %s\n", aclName, inst.Domain)
|
||||
fmt.Fprintf(sb, " use_backend be_https_%s if %s\n", sanitizeName(inst.Name), aclName)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// 4. Default fallback
|
||||
if len(opts.HTTPRoutes) > 0 {
|
||||
sb.WriteString(" default_backend be_l7_termination\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// writeL4Backends writes L4 TCP passthrough backend definitions.
|
||||
func writeL4Backends(sb *strings.Builder, instances []L4Route) {
|
||||
for _, inst := range instances {
|
||||
fmt.Fprintf(sb, "# service: %s\n", inst.Domain)
|
||||
fmt.Fprintf(sb, "backend be_https_%s\n", sanitizeName(inst.Name))
|
||||
sb.WriteString(" mode tcp\n")
|
||||
sb.WriteString(" option tcp-check\n")
|
||||
fmt.Fprintf(sb, " server s0 %s:443 check\n", inst.BackendIP)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// writeL7Stack writes the L7 TLS termination backend, frontend, host ACLs,
|
||||
// per-route rules (IP whitelisting, headers, auth), routing, and backends.
|
||||
func writeL7Stack(sb *strings.Builder, opts GenerateOpts) {
|
||||
certPath := opts.CertsDir
|
||||
if certPath == "" {
|
||||
certPath = "/etc/haproxy/certs/"
|
||||
}
|
||||
|
||||
// TLS termination backend + frontend binding
|
||||
sb.WriteString("backend be_l7_termination\n")
|
||||
sb.WriteString(" mode tcp\n")
|
||||
sb.WriteString(" server s0 127.0.0.1:8443\n")
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("# L7 HTTP frontend: TLS termination + Host-based routing\n")
|
||||
sb.WriteString("frontend l7_https\n")
|
||||
fmt.Fprintf(sb, " bind 127.0.0.1:8443 ssl crt %s\n", certPath)
|
||||
sb.WriteString(" mode http\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Host ACLs
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
fmt.Fprintf(sb, " # service: %s\n", httpRoute.Domain)
|
||||
if httpRoute.Subdomains {
|
||||
fmt.Fprintf(sb, " acl %s hdr(host) -i -m end .%s\n", hostACL, httpRoute.Domain)
|
||||
fmt.Fprintf(sb, " acl %s hdr(host) -i -m str %s\n", hostACL, httpRoute.Domain)
|
||||
} else {
|
||||
fmt.Fprintf(sb, " acl %s hdr(host) -i %s\n", hostACL, httpRoute.Domain)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Per-route ACLs: IP whitelisting and headers
|
||||
writeL7RouteACLs(sb, opts.HTTPRoutes)
|
||||
|
||||
// Authelia forward-auth directives
|
||||
if opts.AuthEnabled && opts.AuthBackend != "" {
|
||||
sb.WriteString("# Authelia forward-auth backend\n")
|
||||
sb.WriteString("backend be_authelia\n")
|
||||
sb.WriteString(" mode http\n")
|
||||
fmt.Fprintf(&sb, " server s0 %s\n", opts.AuthBackend)
|
||||
sb.WriteString("\n")
|
||||
writeL7AuthDirectives(sb, opts)
|
||||
}
|
||||
|
||||
// Routing rules: path-specific first, then catch-all
|
||||
writeL7RoutingRules(sb, opts.HTTPRoutes)
|
||||
|
||||
// L7 backends — one per route
|
||||
writeL7Backends(sb, opts.HTTPRoutes)
|
||||
}
|
||||
|
||||
// writeL7RouteACLs writes per-route IP whitelisting and header directives.
|
||||
func writeL7RouteACLs(sb *strings.Builder, httpRoutes []HTTPRoute) {
|
||||
for _, httpRoute := range httpRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
|
||||
for i, rb := range httpRoute.Routes {
|
||||
aclSuffix := fmt.Sprintf("%s_%d", routeName, i)
|
||||
|
||||
if len(rb.IPAllow) > 0 {
|
||||
ipACL := "ip_" + aclSuffix
|
||||
fmt.Fprintf(sb, " acl %s src %s\n", ipACL, strings.Join(rb.IPAllow, " "))
|
||||
if len(rb.Paths) > 0 {
|
||||
pathACL := "path_" + aclSuffix
|
||||
fmt.Fprintf(sb, " acl %s path_beg %s\n", pathACL, strings.Join(rb.Paths, " "))
|
||||
fmt.Fprintf(sb, " http-request deny if %s %s !%s\n", hostACL, pathACL, ipACL)
|
||||
} else {
|
||||
fmt.Fprintf(sb, " http-request deny if %s !%s\n", hostACL, ipACL)
|
||||
}
|
||||
}
|
||||
|
||||
if rb.Headers != nil {
|
||||
for _, k := range sortedKeys(rb.Headers.Request) {
|
||||
fmt.Fprintf(sb, " http-request set-header %s %q if %s\n", k, rb.Headers.Request[k], hostACL)
|
||||
}
|
||||
for _, k := range sortedKeys(rb.Headers.Response) {
|
||||
fmt.Fprintf(sb, " http-response set-header %s %q if %s\n", k, rb.Headers.Response[k], hostACL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeL7AuthDirectives writes Authelia forward-auth directives for eligible routes.
|
||||
func writeL7AuthDirectives(sb *strings.Builder, opts GenerateOpts) {
|
||||
authDomainACL := ""
|
||||
if opts.AuthDomain != "" {
|
||||
authDomainACL = "host_" + sanitizeName(opts.AuthDomain)
|
||||
}
|
||||
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
|
||||
// Skip auth portal domain (would cause infinite redirect)
|
||||
if hostACL == authDomainACL {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip domains outside the auth session scope
|
||||
if opts.AuthSessionDomain != "" &&
|
||||
httpRoute.Domain != opts.AuthSessionDomain &&
|
||||
!strings.HasSuffix(httpRoute.Domain, "."+opts.AuthSessionDomain) {
|
||||
continue
|
||||
}
|
||||
|
||||
hasAuth := false
|
||||
for _, rb := range httpRoute.Routes {
|
||||
if rb.AuthEnabled {
|
||||
hasAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAuth {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(sb, " # auth: %s\n", httpRoute.Domain)
|
||||
fmt.Fprintf(sb, " http-request lua.auth-request be_authelia /api/authz/forward-auth if %s\n", hostACL)
|
||||
fmt.Fprintf(sb, " http-request redirect location https://%s/?rd=https://%%[hdr(host)]%%[path] if %s !{ var(txn.auth_response_successful) -m bool }\n", opts.AuthDomain, hostACL)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// writeL7RoutingRules writes use_backend rules: path-specific first, then catch-all.
|
||||
func writeL7RoutingRules(sb *strings.Builder, httpRoutes []HTTPRoute) {
|
||||
for _, httpRoute := range httpRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
|
||||
for i, rb := range httpRoute.Routes {
|
||||
if len(rb.Paths) == 0 {
|
||||
continue
|
||||
}
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
pathACL := fmt.Sprintf("path_%s_%d", routeName, i)
|
||||
fmt.Fprintf(sb, " acl %s path_beg %s\n", pathACL, strings.Join(rb.Paths, " "))
|
||||
fmt.Fprintf(sb, " use_backend %s if %s %s\n", backendName, hostACL, pathACL)
|
||||
}
|
||||
|
||||
for i, rb := range httpRoute.Routes {
|
||||
if len(rb.Paths) > 0 {
|
||||
continue
|
||||
}
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
fmt.Fprintf(sb, " use_backend %s if %s\n", backendName, hostACL)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// writeL7Backends writes L7 HTTP backend definitions — one per route.
|
||||
func writeL7Backends(sb *strings.Builder, httpRoutes []HTTPRoute) {
|
||||
for _, httpRoute := range httpRoutes {
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
for i, rb := range httpRoute.Routes {
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
fmt.Fprintf(sb, "# service: %s\n", httpRoute.Domain)
|
||||
fmt.Fprintf(sb, "backend %s\n", backendName)
|
||||
sb.WriteString(" mode http\n")
|
||||
if rb.HealthPath != "" {
|
||||
fmt.Fprintf(sb, " option httpchk GET %s\n", rb.HealthPath)
|
||||
fmt.Fprintf(sb, " server s0 %s check\n", rb.Backend)
|
||||
} else {
|
||||
fmt.Fprintf(sb, " server s0 %s\n", rb.Backend)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeCustomRoutes writes custom TCP proxy frontend/backend pairs.
|
||||
func writeCustomRoutes(sb *strings.Builder, custom []CustomRoute) {
|
||||
for _, route := range custom {
|
||||
fmt.Fprintf(&sb, "# Custom route: %s\n", route.Name)
|
||||
fmt.Fprintf(&sb, "frontend fe_%s\n", sanitizeName(route.Name))
|
||||
fmt.Fprintf(&sb, " bind *:%d\n", route.Port)
|
||||
fmt.Fprintf(sb, "# Custom route: %s\n", route.Name)
|
||||
fmt.Fprintf(sb, "frontend fe_%s\n", sanitizeName(route.Name))
|
||||
fmt.Fprintf(sb, " bind *:%d\n", route.Port)
|
||||
sb.WriteString(" mode tcp\n")
|
||||
fmt.Fprintf(&sb, " default_backend be_%s\n", sanitizeName(route.Name))
|
||||
fmt.Fprintf(sb, " default_backend be_%s\n", sanitizeName(route.Name))
|
||||
sb.WriteString("\n")
|
||||
fmt.Fprintf(&sb, "backend be_%s\n", sanitizeName(route.Name))
|
||||
fmt.Fprintf(sb, "backend be_%s\n", sanitizeName(route.Name))
|
||||
sb.WriteString(" mode tcp\n")
|
||||
fmt.Fprintf(&sb, " server s0 %s\n", route.Backend)
|
||||
fmt.Fprintf(sb, " server s0 %s\n", route.Backend)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// sortedKeys returns the keys of a map in sorted order for deterministic output.
|
||||
|
||||
Reference in New Issue
Block a user