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:
@@ -531,6 +531,7 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon
|
||||
appendIfChanged("OpenRestyCacheLockEnabled", strconv.FormatBool(left.CacheLockEnabled), strconv.FormatBool(right.CacheLockEnabled))
|
||||
appendIfChanged("OpenRestyCacheLockTimeout", left.CacheLockTimeout, right.CacheLockTimeout)
|
||||
appendIfChanged("OpenRestyCacheUseStale", left.CacheUseStale, right.CacheUseStale)
|
||||
appendIfChanged("OpenRestyTrustedProxyCIDRs", strings.Join(effectiveTrustedProxyCIDRs(left.TrustedProxyCIDRs), ","), strings.Join(effectiveTrustedProxyCIDRs(right.TrustedProxyCIDRs), ","))
|
||||
appendIfChanged("OpenRestyDefaultLimitConnPerServer", strconv.Itoa(left.DefaultLimitConnPerServer), strconv.Itoa(right.DefaultLimitConnPerServer))
|
||||
appendIfChanged("OpenRestyDefaultLimitConnPerIP", strconv.Itoa(left.DefaultLimitConnPerIP), strconv.Itoa(right.DefaultLimitConnPerIP))
|
||||
appendIfChanged("OpenRestyDefaultLimitRate", left.DefaultLimitRate, right.DefaultLimitRate)
|
||||
@@ -612,6 +613,7 @@ func openRestyOptionKeys() []string {
|
||||
"OpenRestyCacheLockEnabled",
|
||||
"OpenRestyCacheLockTimeout",
|
||||
"OpenRestyCacheUseStale",
|
||||
"OpenRestyTrustedProxyCIDRs",
|
||||
"OpenRestyDefaultLimitConnPerServer",
|
||||
"OpenRestyDefaultLimitConnPerIP",
|
||||
"OpenRestyDefaultLimitRate",
|
||||
@@ -625,3 +627,10 @@ func openRestyOptionKeys() []string {
|
||||
"SWOfflineDomains",
|
||||
}
|
||||
}
|
||||
|
||||
func effectiveTrustedProxyCIDRs(cidrs []string) []string {
|
||||
if cidrs == nil {
|
||||
return openrestyrender.DefaultTrustedProxyCIDRs()
|
||||
}
|
||||
return cidrs
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
||||
"gorm.io/gorm"
|
||||
@@ -101,6 +102,7 @@ type snapshotWAFDocument struct {
|
||||
}
|
||||
|
||||
type openRestyConfigSnapshot struct {
|
||||
TrustedProxyCIDRs []string `json:"trusted_proxy_cidrs"`
|
||||
DefaultServerReturnStatus int `json:"default_server_return_status"`
|
||||
WorkerProcesses string `json:"worker_processes"`
|
||||
WorkerConnections int `json:"worker_connections"`
|
||||
@@ -568,6 +570,7 @@ func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot {
|
||||
CacheLockTimeout: getStringConfig(model.ConfigKeyOpenRestyCacheLockTimeout, "5s"),
|
||||
CacheUseStale: getStringConfig(model.ConfigKeyOpenRestyCacheUseStale, "error timeout updating http_500 http_502 http_503 http_504"),
|
||||
MainConfigTemplate: getStringConfig(model.ConfigKeyOpenRestyMainConfigTemplate, model.DefaultOpenRestyMainConfigTemplate),
|
||||
TrustedProxyCIDRs: getTrustedProxyCIDRsConfig(ctx),
|
||||
DefaultLimitConnPerServer: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerServer, 0),
|
||||
DefaultLimitConnPerIP: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerIP, 0),
|
||||
DefaultLimitRate: strings.ToLower(strings.TrimSpace(getStringConfig(model.ConfigKeyOpenRestyDefaultLimitRate, ""))),
|
||||
@@ -590,6 +593,26 @@ func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot {
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func parseTrustedProxyCIDRs(value string) []string {
|
||||
var cidrs []string
|
||||
if err := json.Unmarshal([]byte(value), &cidrs); err != nil || cidrs == nil {
|
||||
return []string{}
|
||||
}
|
||||
return cidrs
|
||||
}
|
||||
|
||||
func getTrustedProxyCIDRsConfig(ctx context.Context) []string {
|
||||
config, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyOpenRestyTrustedProxyCIDRs)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return openrestyrender.DefaultTrustedProxyCIDRs()
|
||||
}
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "[OpenFlareConfig] read trusted proxy CIDRs failed: error=%v", err)
|
||||
return []string{}
|
||||
}
|
||||
return parseTrustedProxyCIDRs(config.Value)
|
||||
}
|
||||
|
||||
func parseOriginErrorPageStatusCodes(raw string) []string {
|
||||
const defaultTag = "500-599"
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package config_version
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
|
||||
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
||||
)
|
||||
|
||||
func TestTrustedProxyCIDRsSurviveSnapshotRendering(t *testing.T) {
|
||||
snapshot := snapshotDocument{
|
||||
OpenRestyConfig: openRestyConfigSnapshot{
|
||||
MainConfigTemplate: model.DefaultOpenRestyMainConfigTemplate,
|
||||
TrustedProxyCIDRs: parseTrustedProxyCIDRs(`["173.245.48.0/20","2001:db8:1234::/48"]`),
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rendered, err := renderSnapshotConfig(string(data), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, expected := range []string{"real_ip_header CF-Connecting-IP;", "set_real_ip_from 173.245.48.0/20;", "set_real_ip_from 2001:db8:1234::/48;"} {
|
||||
if !strings.Contains(rendered.MainConfig, expected) {
|
||||
t.Errorf("rendered snapshot missing %q", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedProxyCIDRsSnapshotEmptyAndLegacyDefaults(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
json string
|
||||
want int
|
||||
}{
|
||||
{name: "explicit empty list remains disabled", json: `{"openresty_config":{"trusted_proxy_cidrs":[]}}`, want: 0},
|
||||
{name: "legacy snapshot defaults to Cloudflare ranges", json: `{"openresty_config":{}}`, want: 22},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
rendered, err := renderSnapshotConfig(testCase.json, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := strings.Count(rendered.MainConfig, "set_real_ip_from "); got != testCase.want {
|
||||
t.Fatalf("rendered trusted proxy range count = %d, want %d", got, testCase.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedProxyCIDRConfigMissingAndExplicitEmpty(t *testing.T) {
|
||||
cleanup := setupOriginErrorPageSnapshotDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
missing := buildOpenRestyConfigSnapshot(ctx).TrustedProxyCIDRs
|
||||
if len(missing) != 22 || !slices.Equal(missing, openrestyrender.DefaultTrustedProxyCIDRs()) {
|
||||
t.Fatalf("missing database key defaults to %#v, want the 22 Cloudflare ranges", missing)
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&model.SystemConfig{Key: model.ConfigKeyOpenRestyTrustedProxyCIDRs, Value: `[]`, Type: "business"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
explicitEmpty := buildOpenRestyConfigSnapshot(ctx).TrustedProxyCIDRs
|
||||
if explicitEmpty == nil || len(explicitEmpty) != 0 {
|
||||
t.Fatalf("explicit [] must remain a non-nil empty list, got %#v", explicitEmpty)
|
||||
}
|
||||
payload, err := json.Marshal(snapshotDocument{OpenRestyConfig: buildOpenRestyConfigSnapshot(ctx)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(payload), `"trusted_proxy_cidrs":[]`) {
|
||||
t.Fatalf("explicit empty list was omitted from snapshot: %s", payload)
|
||||
}
|
||||
rendered, err := renderSnapshotConfig(string(payload), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(rendered.MainConfig, "set_real_ip_from ") {
|
||||
t.Fatal("explicit empty list still trusts proxy ranges")
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Model(&model.SystemConfig{}).Where("key = ?", model.ConfigKeyOpenRestyTrustedProxyCIDRs).Update("value", "invalid").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ram.ResetForTest()
|
||||
malformed := buildOpenRestyConfigSnapshot(ctx).TrustedProxyCIDRs
|
||||
if malformed == nil || len(malformed) != 0 {
|
||||
t.Fatalf("malformed configuration must fail closed, got %#v", malformed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedProxyCIDRDiffTreatsLegacyNilAsCloudflareDefaults(t *testing.T) {
|
||||
diffs := diffOpenRestyOptionDetails(openRestyConfigSnapshot{}, openRestyConfigSnapshot{TrustedProxyCIDRs: []string{}})
|
||||
for _, diff := range diffs {
|
||||
if diff.Key == "OpenRestyTrustedProxyCIDRs" {
|
||||
if diff.PreviousValue == diff.CurrentValue {
|
||||
t.Fatal("legacy nil and explicit empty list should have different effective values")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("trusted proxy CIDR change was not included in config diff")
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package option
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
const (
|
||||
maxOriginErrorPageHTMLBytes = 256 << 10 // 256 KiB
|
||||
maxSWOfflineDomains = 1000
|
||||
maxTrustedProxyCIDRs = 256
|
||||
)
|
||||
|
||||
var openRestyOptionValidators = map[string]func(key, value string) error{
|
||||
@@ -57,6 +59,7 @@ var openRestyOptionValidators = map[string]func(key, value string) error{
|
||||
model.ConfigKeyOpenRestyCacheKeyTemplate: validateOpenRestyCacheKeyTemplate,
|
||||
model.ConfigKeyOpenRestyCacheUseStale: validateOpenRestyCacheUseStale,
|
||||
model.ConfigKeyOpenRestyMainConfigTemplate: validateOpenRestyMainConfigTemplate,
|
||||
model.ConfigKeyOpenRestyTrustedProxyCIDRs: validateOpenRestyTrustedProxyCIDRs,
|
||||
model.ConfigKeyOpenRestyDefaultLimitConnPerServer: validateNonNegativeIntegerOption,
|
||||
model.ConfigKeyOpenRestyDefaultLimitConnPerIP: validateNonNegativeIntegerOption,
|
||||
model.ConfigKeyOpenRestyDefaultLimitRate: validateOpenRestyDefaultLimitRate,
|
||||
@@ -204,6 +207,23 @@ func validateOpenRestyMainConfigTemplate(key, value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOpenRestyTrustedProxyCIDRs(key, value string) error {
|
||||
var cidrs []string
|
||||
if err := json.Unmarshal([]byte(value), &cidrs); err != nil || cidrs == nil {
|
||||
return fmt.Errorf("%s 必须为 JSON 字符串数组", key)
|
||||
}
|
||||
if len(cidrs) > maxTrustedProxyCIDRs {
|
||||
return fmt.Errorf("%s 最多允许 %d 个网段", key, maxTrustedProxyCIDRs)
|
||||
}
|
||||
for _, cidr := range cidrs {
|
||||
prefix, err := netip.ParsePrefix(strings.TrimSpace(cidr))
|
||||
if err != nil || prefix != prefix.Masked() {
|
||||
return fmt.Errorf("%s 包含无效网段 %q", key, cidr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOpenRestyDefaultLimitRate(key, trimmed string) error {
|
||||
if trimmed == "" || trimmed == "0" {
|
||||
return nil
|
||||
|
||||
@@ -70,6 +70,14 @@ func TestValidateOriginErrorPageStatusCodes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOpenRestyTrustedProxyCIDRs(t *testing.T) {
|
||||
require.NoError(t, validateOpenRestyOption(model.ConfigKeyOpenRestyTrustedProxyCIDRs, `[]`))
|
||||
require.NoError(t, validateOpenRestyOption(model.ConfigKeyOpenRestyTrustedProxyCIDRs, `["173.245.48.0/20","10.2.0.0/24"]`))
|
||||
for _, value := range []string{`null`, `10.2.0.0/24`, `["10.2.0.0/24;return 200"]`, `["]`} {
|
||||
require.Error(t, validateOpenRestyOption(model.ConfigKeyOpenRestyTrustedProxyCIDRs, value), "value %q must be rejected", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOriginErrorPageHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user