mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 16:46:37 +08:00
fix(openflare): trust Cloudflare client IP ranges by default
This commit is contained in:
@@ -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 ""
|
||||
|
||||
@@ -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;") {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user