feat: add trusted_proxies config key

Add a trusted_proxies CIDR-list config key alongside blocked_networks.
Generalize the blocked_networks parser into parseCIDRList and
cidrListEntries, which take the key name as a parameter, so both keys
share one parser rather than a second copy. An invalid entry aborts
startup naming the key and value; an omitted or empty key leaves the
list empty.

Model: opus-4-8
This commit is contained in:
2026-09-21 23:23:47 +00:00
parent 7697822c53
commit c5f4682b0b
+35 -19
View File
@@ -44,6 +44,7 @@ const (
keyUpstreamConnectionsPerHost = "upstream_connections_per_host" keyUpstreamConnectionsPerHost = "upstream_connections_per_host"
keyCacheMaxBytes = "cache_max_bytes" keyCacheMaxBytes = "cache_max_bytes"
keyBlockedNetworks = "blocked_networks" keyBlockedNetworks = "blocked_networks"
keyTrustedProxies = "trusted_proxies"
) )
// placeholderSigningKey is the dummy signing_key shipped in // placeholderSigningKey is the dummy signing_key shipped in
@@ -117,6 +118,14 @@ type Config struct {
// fetcher's dialer; the built-in ranges always apply. // fetcher's dialer; the built-in ranges always apply.
BlockedNetworks []netip.Prefix BlockedNetworks []netip.Prefix
// TrustedProxies are the CIDR ranges of reverse proxies whose
// forwarding headers may be believed. Forwarded headers are honored
// only when the immediate peer falls inside one of these ranges;
// otherwise the peer address is used and the headers are ignored, so
// an untrusted client cannot spoof its address. Empty means trust
// nothing and always use the peer address.
TrustedProxies []netip.Prefix
// CacheMaxBytes is the disk cache size limit in bytes. Zero // CacheMaxBytes is the disk cache size limit in bytes. Zero
// disables the disk cache entirely. When cache_max_bytes is // disables the disk cache entirely. When cache_max_bytes is
// omitted from the configuration, this holds the computed default // omitted from the configuration, this holds the computed default
@@ -185,7 +194,12 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
} }
} }
blockedNetworks, err := getBlockedNetworks(sc) blockedNetworks, err := parseCIDRList(sc, keyBlockedNetworks)
if err != nil {
return nil, err
}
trustedProxies, err := parseCIDRList(sc, keyTrustedProxies)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -207,6 +221,7 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
keyUpstreamConnectionsPerHost, DefaultUpstreamConnectionsPerHost), keyUpstreamConnectionsPerHost, DefaultUpstreamConnectionsPerHost),
CacheMaxBytes: loader.int64Val(keyCacheMaxBytes, 0), CacheMaxBytes: loader.int64Val(keyCacheMaxBytes, 0),
BlockedNetworks: blockedNetworks, BlockedNetworks: blockedNetworks,
TrustedProxies: trustedProxies,
} }
// The computed default for cache_max_bytes needs a validated // The computed default for cache_max_bytes needs a validated
@@ -322,7 +337,8 @@ func isKnownConfigKey(key string) bool {
switch key { switch key {
case keyDebug, keyMaintenanceMode, keyPort, keyStateDir, keySentryDSN, case keyDebug, keyMaintenanceMode, keyPort, keyStateDir, keySentryDSN,
keyDBURL, keyMetrics, keySigningKey, keyAllowlistHosts, keyAllowHTTP, keyDBURL, keyMetrics, keySigningKey, keyAllowlistHosts, keyAllowHTTP,
keyUpstreamConnectionsPerHost, keyCacheMaxBytes, keyBlockedNetworks, "env": keyUpstreamConnectionsPerHost, keyCacheMaxBytes, keyBlockedNetworks,
keyTrustedProxies, "env":
return true return true
} }
@@ -817,27 +833,27 @@ func getStringSlice(sc *smartconfig.Config) []string {
return nil return nil
} }
// getBlockedNetworks parses the blocked_networks value into CIDR prefixes, // parseCIDRList parses the value of the named config key into CIDR
// or returns nil if the key is omitted. It accepts a YAML list of strings // prefixes, or returns nil if the key is omitted. It accepts a YAML list
// or a comma-separated string. An explicitly null value, a wrong type, an // of strings or a comma-separated string. An explicitly null value, a
// empty entry, a non-string entry, or an unparseable CIDR aborts startup // wrong type, an empty entry, a non-string entry, or an unparseable CIDR
// naming the key and the offending value; a default (the built-in // aborts startup naming the key and the offending value; the default
// blocklist alone) applies only to an omitted key. // (an empty list) applies only to an omitted key.
func getBlockedNetworks(sc *smartconfig.Config) ([]netip.Prefix, error) { func parseCIDRList(sc *smartconfig.Config, key string) ([]netip.Prefix, error) {
if sc == nil { if sc == nil {
return nil, nil return nil, nil
} }
raw, ok := sc.Get(keyBlockedNetworks) raw, ok := sc.Get(key)
if !ok { if !ok {
return nil, nil return nil, nil
} }
if raw == nil { if raw == nil {
return nil, errNullConfigValue(keyBlockedNetworks) return nil, errNullConfigValue(key)
} }
entries, err := blockedNetworkEntries(raw) entries, err := cidrListEntries(raw, key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -848,7 +864,7 @@ func getBlockedNetworks(sc *smartconfig.Config) ([]netip.Prefix, error) {
prefix, err := netip.ParsePrefix(entry) prefix, err := netip.ParsePrefix(entry)
if err != nil { if err != nil {
return nil, fmt.Errorf("config key %q: value %q is %w", return nil, fmt.Errorf("config key %q: value %q is %w",
keyBlockedNetworks, entry, errNotAValidCIDR) key, entry, errNotAValidCIDR)
} }
prefixes = append(prefixes, prefix) prefixes = append(prefixes, prefix)
@@ -857,10 +873,10 @@ func getBlockedNetworks(sc *smartconfig.Config) ([]netip.Prefix, error) {
return prefixes, nil return prefixes, nil
} }
// blockedNetworkEntries extracts the raw blocked_networks entries as // cidrListEntries extracts the raw entries of the named CIDR-list key as
// trimmed, non-empty strings, from either a YAML list of strings or a // trimmed, non-empty strings, from either a YAML list of strings or a
// comma-separated string. Any other shape is a configuration error. // comma-separated string. Any other shape is a configuration error.
func blockedNetworkEntries(raw any) ([]string, error) { func cidrListEntries(raw any, key string) ([]string, error) {
switch val := raw.(type) { switch val := raw.(type) {
case []any: case []any:
entries := make([]string, 0, len(val)) entries := make([]string, 0, len(val))
@@ -869,12 +885,12 @@ func blockedNetworkEntries(raw any) ([]string, error) {
str, ok := item.(string) str, ok := item.(string)
if !ok { if !ok {
return nil, fmt.Errorf("config key %q: list entry %v (%T) is %w", return nil, fmt.Errorf("config key %q: list entry %v (%T) is %w",
keyBlockedNetworks, item, item, errNotAString) key, item, item, errNotAString)
} }
if strings.TrimSpace(str) == "" { if strings.TrimSpace(str) == "" {
return nil, fmt.Errorf("config key %q: %w", return nil, fmt.Errorf("config key %q: %w",
keyBlockedNetworks, errEmptyListEntry) key, errEmptyListEntry)
} }
entries = append(entries, strings.TrimSpace(str)) entries = append(entries, strings.TrimSpace(str))
@@ -888,7 +904,7 @@ func blockedNetworkEntries(raw any) ([]string, error) {
trimmed := strings.TrimSpace(part) trimmed := strings.TrimSpace(part)
if trimmed == "" { if trimmed == "" {
return nil, fmt.Errorf("config key %q: value %q %w", return nil, fmt.Errorf("config key %q: value %q %w",
keyBlockedNetworks, val, errEmptyEntry) key, val, errEmptyEntry)
} }
entries = append(entries, trimmed) entries = append(entries, trimmed)
@@ -897,6 +913,6 @@ func blockedNetworkEntries(raw any) ([]string, error) {
return entries, nil return entries, nil
default: default:
return nil, fmt.Errorf("config key %q: value %v (%T) is %w", return nil, fmt.Errorf("config key %q: value %v (%T) is %w",
keyBlockedNetworks, raw, raw, errNotAStringList) key, raw, raw, errNotAStringList)
} }
} }