fix(openflare): trust Cloudflare client IP ranges by default

This commit is contained in:
ryan
2026-10-07 23:51:36 +08:00
parent 7483897d3c
commit 6f63715182
15 changed files with 354 additions and 1 deletions
+43
View File
@@ -13,6 +13,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"net/netip"
"net/url"
"path"
"regexp"
@@ -170,7 +171,18 @@ func DedupeSupportFiles(files []SupportFile) []SupportFile {
}
func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot, limitReqRates []string) string {
trustedProxyCIDRs := cfg.TrustedProxyCIDRs
if trustedProxyCIDRs == nil {
trustedProxyCIDRs = DefaultTrustedProxyCIDRs()
}
trustedProxyDirectives := renderTrustedProxyDirectives(trustedProxyCIDRs, !hasNginxDirective(templateText, "real_ip_header"))
if !strings.Contains(templateText, RealIPDirectivesPlaceholder) && trustedProxyDirectives != "" {
// This required token expands to a complete map block in the http context.
// Injecting before it avoids depending on formatting or splitting directives.
templateText = strings.Replace(templateText, "{{OpenRestyConnectionUpgradeMap}}", trustedProxyDirectives+"{{OpenRestyConnectionUpgradeMap}}", 1)
}
replacer := strings.NewReplacer(
RealIPDirectivesPlaceholder, trustedProxyDirectives,
"{{OpenRestyWorkerProcesses}}", cfg.WorkerProcesses,
"{{OpenRestyWorkerConnections}}", strconv.Itoa(cfg.WorkerConnections),
"{{OpenRestyWorkerRlimitNofile}}", strconv.Itoa(cfg.WorkerRlimitNofile),
@@ -205,6 +217,37 @@ func renderMainConfigTemplate(templateText string, cfg ConfigSnapshot, limitReqR
return replacer.Replace(templateText)
}
func hasNginxDirective(templateText string, directive string) bool {
for _, line := range strings.Split(templateText, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
continue
}
if fields := strings.Fields(line); len(fields) > 0 && fields[0] == directive {
return true
}
}
return false
}
func renderTrustedProxyDirectives(cidrs []string, includeHeader bool) string {
if len(cidrs) == 0 {
return ""
}
var builder strings.Builder
if includeHeader {
builder.WriteString(" real_ip_header CF-Connecting-IP;\n real_ip_recursive on;\n")
}
for _, cidr := range cidrs {
prefix, err := netip.ParsePrefix(strings.TrimSpace(cidr))
if err != nil {
continue
}
fmt.Fprintf(&builder, " set_real_ip_from %s;\n", prefix.Masked())
}
return builder.String()
}
func renderTemplateDirective(enabled bool, statement string) string {
if !enabled {
return ""
+50
View File
@@ -9,6 +9,56 @@ import (
"testing"
)
func TestRenderMainConfigTrustedProxyCIDRs(t *testing.T) {
base := ConfigSnapshot{MainConfigTemplate: defaultMainConfigTemplate}
withDefaultTrust := RenderMainConfig(Document{OpenRestyConfig: base})
if count := strings.Count(withDefaultTrust, "set_real_ip_from "); count != 22 {
t.Fatalf("default config trusts %d proxy ranges, want all 22 Cloudflare ranges", count)
}
if !strings.Contains(withDefaultTrust, "set_real_ip_from 173.245.48.0/20;") || !strings.Contains(withDefaultTrust, "set_real_ip_from 2c0f:f248::/32;") {
t.Fatal("default config is missing Cloudflare IPv4 or IPv6 ranges")
}
base.TrustedProxyCIDRs = []string{}
withoutTrust := RenderMainConfig(Document{OpenRestyConfig: base})
if strings.Contains(withoutTrust, "real_ip_header") || strings.Contains(withoutTrust, "set_real_ip_from") {
t.Fatal("explicit empty trusted-proxy list must disable forwarded-header trust")
}
base.TrustedProxyCIDRs = []string{"173.245.48.0/20", "10.2.0.0/24", "2001:db8:1234::/48", "192.0.2.7/32"}
got := RenderMainConfig(Document{OpenRestyConfig: base})
for _, expected := range []string{"real_ip_header CF-Connecting-IP;", "set_real_ip_from 173.245.48.0/20;", "set_real_ip_from 10.2.0.0/24;", "set_real_ip_from 2001:db8:1234::/48;", "set_real_ip_from 192.0.2.7/32;"} {
if !strings.Contains(got, expected) {
t.Errorf("rendered config missing %q", expected)
}
}
}
func TestRenderMainConfigInjectsTrustedProxyForLegacyTemplate(t *testing.T) {
legacy := strings.ReplaceAll(defaultMainConfigTemplate, RealIPDirectivesPlaceholder+"\n", "")
got := RenderMainConfig(Document{OpenRestyConfig: ConfigSnapshot{MainConfigTemplate: legacy, TrustedProxyCIDRs: []string{"10.2.0.0/24"}}})
if !strings.Contains(got, "real_ip_header CF-Connecting-IP;\n real_ip_recursive on;\n set_real_ip_from 10.2.0.0/24;") || !strings.Contains(got, "map $http_upgrade $connection_upgrade {") {
t.Fatalf("trusted proxy directives were not injected into legacy template:\n%s", got)
}
if strings.Contains(got, "access_log real_ip_header") {
t.Fatalf("trusted proxy directives corrupted access_log directive:\n%s", got)
}
}
func TestRenderMainConfigPreservesExplicitRealIPHeader(t *testing.T) {
templateText := strings.Replace(defaultMainConfigTemplate, "{{OpenRestyConnectionUpgradeMap}}", "# real_ip_header in comment\nreal_ip_header X-Forwarded-For;\n{{OpenRestyConnectionUpgradeMap}}", 1)
got := RenderMainConfig(Document{OpenRestyConfig: ConfigSnapshot{
MainConfigTemplate: templateText,
TrustedProxyCIDRs: []string{"192.0.2.7/32"},
}})
if !strings.Contains(got, "real_ip_header X-Forwarded-For;") || strings.Contains(got, "real_ip_header CF-Connecting-IP;") {
t.Fatalf("explicit template header selection was not preserved:\n%s", got)
}
if !strings.Contains(got, "set_real_ip_from 192.0.2.7/32;") {
t.Fatalf("trusted CIDR was not added to explicit template header:\n%s", got)
}
}
func TestRenderOpenRestyUsesDedicatedWAFIPGroupSharedDict(t *testing.T) {
block := renderOpenRestyObservabilityTemplateBlock()
if !strings.Contains(block, "lua_shared_dict openflare_waf_config 1m;") {
+17
View File
@@ -24,6 +24,7 @@ const (
PowStaticDirPlaceholder = "__OPENFLARE_POW_STATIC_DIR__"
PagesDirPlaceholder = "__OPENFLARE_PAGES_DIR__"
ErrorPageTmplPlaceholder = "__OPENFLARE_ERROR_PAGE_TMPL__"
RealIPDirectivesPlaceholder = "{{OpenRestyRealIPDirectives}}"
SWDirPlaceholder = "__OPENFLARE_SW_DIR__"
SourceConfigFileName = "openresty_config.json"
@@ -102,6 +103,7 @@ http {
gzip {{OpenRestyGzip}};
gzip_min_length {{OpenRestyGzipMinLength}};
gzip_comp_level {{OpenRestyGzipCompLevel}};
{{OpenRestyRealIPDirectives}}
gzip_vary on;
gzip_types text/plain text/css text/xml application/javascript application/json application/xml application/rss+xml application/atom+xml image/svg+xml;
{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}};
@@ -281,6 +283,7 @@ type WAFDocument struct {
// ConfigSnapshot holds the full set of OpenResty tuning parameters that are
// rendered into the nginx main configuration template.
type ConfigSnapshot struct {
TrustedProxyCIDRs []string `json:"trusted_proxy_cidrs"`
DefaultServerReturnStatus int `json:"default_server_return_status"`
WorkerProcesses string `json:"worker_processes"`
WorkerConnections int `json:"worker_connections"`
@@ -335,6 +338,20 @@ type ConfigSnapshot struct {
SWOfflineDomains []string `json:"sw_offline_domains,omitempty"`
}
// DefaultTrustedProxyCIDRs returns Cloudflare's published IPv4 and IPv6
// networks. Callers can override the list, including with an explicit empty
// slice to disable trusted-proxy processing.
func DefaultTrustedProxyCIDRs() []string {
return []string{
"173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22",
"141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20",
"197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
"104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
"2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32",
"2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32",
}
}
// Document is the top-level input structure for the OpenResty renderer,
// combining routes, OpenResty tuning, and WAF configuration.
type Document struct {