2026-05-22 20:26:11 -04:00
|
|
|
package clientip
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
2026-07-05 16:44:36 -04:00
|
|
|
"os"
|
2026-05-22 20:26:11 -04:00
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SettingsStore is satisfied by *catalog.ServerSettingsRepo.
|
|
|
|
|
type SettingsStore interface {
|
|
|
|
|
Get(ctx context.Context, key string) (string, error)
|
|
|
|
|
Set(ctx context.Context, key, value string) error
|
|
|
|
|
GetAll(ctx context.Context) (map[string]string, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const (
|
2026-07-05 16:44:36 -04:00
|
|
|
// SettingTrustedProxies is the server_settings key holding the
|
|
|
|
|
// comma-separated CIDR list of trusted reverse proxies.
|
|
|
|
|
SettingTrustedProxies = "clientip.trusted_proxies"
|
|
|
|
|
// EnvTrustedProxies overrides SettingTrustedProxies at startup when set.
|
|
|
|
|
EnvTrustedProxies = "SILO_TRUSTED_PROXIES"
|
|
|
|
|
// DefaultTrustedProxies: RFC 1918 private ranges + loopback.
|
|
|
|
|
DefaultTrustedProxies = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8,::1/128"
|
2026-05-22 20:26:11 -04:00
|
|
|
)
|
|
|
|
|
|
2026-07-05 16:44:36 -04:00
|
|
|
// SeedDefaults ensures the trusted proxy setting exists. When the
|
|
|
|
|
// SILO_TRUSTED_PROXIES environment variable is set it wins: the value is
|
|
|
|
|
// validated and persisted so the admin UI shows the effective config (and is
|
|
|
|
|
// re-applied on every startup while the variable remains set). Otherwise the
|
|
|
|
|
// default list is written only if the setting is unset.
|
2026-05-22 20:26:11 -04:00
|
|
|
func SeedDefaults(ctx context.Context, store SettingsStore) error {
|
2026-07-05 16:44:36 -04:00
|
|
|
if env := strings.TrimSpace(os.Getenv(EnvTrustedProxies)); env != "" {
|
|
|
|
|
normalized, err := NormalizeCIDRList(env)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("invalid %s: %w", EnvTrustedProxies, err)
|
|
|
|
|
}
|
|
|
|
|
existing, err := store.Get(ctx, SettingTrustedProxies)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("seed clientip defaults: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if existing == normalized {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return store.Set(ctx, SettingTrustedProxies, normalized)
|
|
|
|
|
}
|
|
|
|
|
existing, err := store.Get(ctx, SettingTrustedProxies)
|
2026-05-22 20:26:11 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("seed clientip defaults: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if existing != "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-07-05 16:44:36 -04:00
|
|
|
return store.Set(ctx, SettingTrustedProxies, DefaultTrustedProxies)
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LoadTrustedCIDRs reads the trusted proxy CIDRs from settings.
|
|
|
|
|
func LoadTrustedCIDRs(ctx context.Context, store SettingsStore) ([]*net.IPNet, error) {
|
2026-07-05 16:44:36 -04:00
|
|
|
raw, err := store.Get(ctx, SettingTrustedProxies)
|
2026-05-22 20:26:11 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("load trusted proxies: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if raw == "" {
|
2026-07-05 16:44:36 -04:00
|
|
|
raw = DefaultTrustedProxies
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
|
|
|
|
return ParseCIDRs(raw)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:44:36 -04:00
|
|
|
// NormalizeCIDRList validates a comma-separated CIDR list and returns it in
|
|
|
|
|
// canonical form (trimmed entries joined by ", "). An empty list is valid and
|
|
|
|
|
// means "use the built-in defaults".
|
|
|
|
|
func NormalizeCIDRList(raw string) (string, error) {
|
|
|
|
|
parts := strings.Split(raw, ",")
|
|
|
|
|
var out []string
|
|
|
|
|
for _, p := range parts {
|
|
|
|
|
p = strings.TrimSpace(p)
|
|
|
|
|
if p == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := net.ParseCIDR(p); err != nil {
|
|
|
|
|
return "", fmt.Errorf("invalid CIDR %q (expected e.g. 203.0.113.7/32)", p)
|
|
|
|
|
}
|
|
|
|
|
out = append(out, p)
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(out, ", "), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
// ParseCIDRs parses a comma-separated list of CIDR strings.
|
|
|
|
|
func ParseCIDRs(raw string) ([]*net.IPNet, error) {
|
|
|
|
|
parts := strings.Split(raw, ",")
|
|
|
|
|
var cidrs []*net.IPNet
|
|
|
|
|
for _, p := range parts {
|
|
|
|
|
p = strings.TrimSpace(p)
|
|
|
|
|
if p == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
_, cidr, err := net.ParseCIDR(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid CIDR %q: %w", p, err)
|
|
|
|
|
}
|
|
|
|
|
cidrs = append(cidrs, cidr)
|
|
|
|
|
}
|
|
|
|
|
return cidrs, nil
|
|
|
|
|
}
|