[功能] 添加OpenRestyResolvers支持,优化DNS解析器配置和验证逻辑

This commit is contained in:
ryan
2026-03-18 13:52:58 +08:00
parent e2202d1456
commit 7a22167997
14 changed files with 332 additions and 31 deletions
+1
View File
@@ -1,4 +1,5 @@
.cache .cache
.gocache*
.idea .idea
.vscode .vscode
upload upload
+2
View File
@@ -76,12 +76,14 @@ OpenResty 性能参数与缓存参数继续统一保存在 `Option` 表。当前
* `OpenRestyProxyReadTimeout` * `OpenRestyProxyReadTimeout`
* `OpenRestyProxyBufferingEnabled` * `OpenRestyProxyBufferingEnabled`
* `OpenRestyGzipEnabled` * `OpenRestyGzipEnabled`
* `OpenRestyResolvers`
* `OpenRestyCacheEnabled` * `OpenRestyCacheEnabled`
* `OpenRestyCachePath` * `OpenRestyCachePath`
* `OpenRestyCacheMaxSize` * `OpenRestyCacheMaxSize`
这类参数必须以结构化方式校验、保存并参与版本渲染。 这类参数必须以结构化方式校验、保存并参与版本渲染。
* `OpenRestyResolvers` 由管理端性能页面维护,支持填写多个 DNS 服务器 IP;留空时不额外生成 `resolver` 指令。
### 1.5 前端构建环境变量 ### 1.5 前端构建环境变量
| 环境变量 | 作用 | 默认值 | | 环境变量 | 作用 | 默认值 |
+1 -1
View File
@@ -73,7 +73,7 @@ func main() {
NginxLuaDir: cfg.OpenrestyLuaDir, NginxLuaDir: cfg.OpenrestyLuaDir,
OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyPath, cfg.OpenrestyObservabilityPort), OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyPath, cfg.OpenrestyObservabilityPort),
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
OpenrestyResolverDirective: nginx.ResolverDirective(cfg.OpenrestyPath), OpenrestyResolverDirective: "",
Executor: nginx.NewExecutor(nginx.ExecutorOptions{ Executor: nginx.NewExecutor(nginx.ExecutorOptions{
NginxPath: cfg.OpenrestyPath, NginxPath: cfg.OpenrestyPath,
DockerBinary: cfg.DockerBinary, DockerBinary: cfg.DockerBinary,
+27
View File
@@ -33,6 +33,7 @@ type Config struct {
AgentVersion string `json:"-"` AgentVersion string `json:"-"`
NginxVersion string `json:"-"` NginxVersion string `json:"-"`
OpenrestyPath string `json:"openresty_path"` OpenrestyPath string `json:"openresty_path"`
OpenrestyResolvers []string `json:"openresty_resolvers,omitempty"`
OpenrestyContainerName string `json:"openresty_container_name"` OpenrestyContainerName string `json:"openresty_container_name"`
OpenrestyDockerImage string `json:"openresty_docker_image"` OpenrestyDockerImage string `json:"openresty_docker_image"`
DockerBinary string `json:"docker_binary"` DockerBinary string `json:"docker_binary"`
@@ -59,6 +60,7 @@ type configFile struct {
NodeName string `json:"node_name"` NodeName string `json:"node_name"`
NodeIP string `json:"node_ip"` NodeIP string `json:"node_ip"`
OpenrestyPath string `json:"openresty_path"` OpenrestyPath string `json:"openresty_path"`
OpenrestyResolvers []string `json:"openresty_resolvers"`
OpenrestyContainerName string `json:"openresty_container_name"` OpenrestyContainerName string `json:"openresty_container_name"`
OpenrestyDockerImage string `json:"openresty_docker_image"` OpenrestyDockerImage string `json:"openresty_docker_image"`
DockerBinary string `json:"docker_binary"` DockerBinary string `json:"docker_binary"`
@@ -93,6 +95,7 @@ func Load(path string) (*Config, error) {
NodeName: file.NodeName, NodeName: file.NodeName,
NodeIP: file.NodeIP, NodeIP: file.NodeIP,
OpenrestyPath: file.OpenrestyPath, OpenrestyPath: file.OpenrestyPath,
OpenrestyResolvers: append([]string{}, file.OpenrestyResolvers...),
OpenrestyContainerName: file.OpenrestyContainerName, OpenrestyContainerName: file.OpenrestyContainerName,
OpenrestyDockerImage: file.OpenrestyDockerImage, OpenrestyDockerImage: file.OpenrestyDockerImage,
DockerBinary: file.DockerBinary, DockerBinary: file.DockerBinary,
@@ -121,6 +124,7 @@ func Load(path string) (*Config, error) {
func applyDefaults(cfg *Config, baseDir string) { func applyDefaults(cfg *Config, baseDir string) {
baseDir = filepath.Clean(baseDir) baseDir = filepath.Clean(baseDir)
cfg.AgentVersion = AgentVersion cfg.AgentVersion = AgentVersion
cfg.OpenrestyResolvers = normalizeResolverList(cfg.OpenrestyResolvers)
if cfg.OpenrestyContainerName == "" { if cfg.OpenrestyContainerName == "" {
cfg.OpenrestyContainerName = "openflare-openresty" cfg.OpenrestyContainerName = "openflare-openresty"
} }
@@ -290,6 +294,29 @@ func detectHostname() string {
return strings.TrimSpace(host) return strings.TrimSpace(host)
} }
func normalizeResolverList(values []string) []string {
if len(values) == 0 {
return nil
}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
continue
}
if _, ok := seen[trimmed]; ok {
continue
}
seen[trimmed] = struct{}{}
result = append(result, trimmed)
}
if len(result) == 0 {
return nil
}
return result
}
func firstNonEmpty(values ...string) string { func firstNonEmpty(values ...string) string {
for _, value := range values { for _, value := range values {
if strings.TrimSpace(value) != "" { if strings.TrimSpace(value) != "" {
@@ -120,6 +120,40 @@ func TestLoadPathModeKeepsExplicitPaths(t *testing.T) {
} }
} }
func TestLoadNormalizesExplicitResolvers(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "agent.json")
payload := map[string]any{
"server_url": "http://127.0.0.1:3000",
"agent_token": "token",
"node_name": "edge-01",
"node_ip": "10.0.0.8",
"openresty_resolvers": []string{" 10.0.0.2 ", "10.0.0.2", "", "1.1.1.1"},
}
data, err := json.Marshal(payload)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err = os.WriteFile(configPath, data, 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
cfg, err := Load(configPath)
if err != nil {
t.Fatalf("Load failed: %v", err)
}
expected := []string{"10.0.0.2", "1.1.1.1"}
if len(cfg.OpenrestyResolvers) != len(expected) {
t.Fatalf("unexpected resolver count: %#v", cfg.OpenrestyResolvers)
}
for index, value := range expected {
if cfg.OpenrestyResolvers[index] != value {
t.Fatalf("unexpected resolver at %d: got %q want %q", index, cfg.OpenrestyResolvers[index], value)
}
}
}
func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
configPath := filepath.Join(dir, "agent.json") configPath := filepath.Join(dir, "agent.json")
@@ -209,6 +243,7 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) {
cfg.NginxVersion = "1.27.1.2" cfg.NginxVersion = "1.27.1.2"
cfg.HeartbeatInterval = MillisecondDuration(5 * time.Second) cfg.HeartbeatInterval = MillisecondDuration(5 * time.Second)
cfg.RequestTimeout = MillisecondDuration(7 * time.Second) cfg.RequestTimeout = MillisecondDuration(7 * time.Second)
cfg.OpenrestyResolvers = []string{"10.0.0.2", "1.1.1.1"}
if err = cfg.Save(); err != nil { if err = cfg.Save(); err != nil {
t.Fatalf("Save failed: %v", err) t.Fatalf("Save failed: %v", err)
@@ -234,6 +269,10 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) {
if decoded["request_timeout"] != float64(7000) { if decoded["request_timeout"] != float64(7000) {
t.Fatalf("unexpected request timeout: %#v", decoded["request_timeout"]) t.Fatalf("unexpected request timeout: %#v", decoded["request_timeout"])
} }
resolvers, ok := decoded["openresty_resolvers"].([]any)
if !ok || len(resolvers) != 2 || resolvers[0] != "10.0.0.2" || resolvers[1] != "1.1.1.1" {
t.Fatalf("unexpected resolvers: %#v", decoded["openresty_resolvers"])
}
if decoded["openresty_observability_port"] != float64(defaultOpenRestyObservabilityPort) { if decoded["openresty_observability_port"] != float64(defaultOpenRestyObservabilityPort) {
t.Fatalf("unexpected observability port: %#v", decoded["openresty_observability_port"]) t.Fatalf("unexpected observability port: %#v", decoded["openresty_observability_port"])
} }
+57 -6
View File
@@ -8,6 +8,8 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"log/slog" "log/slog"
"net"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -394,6 +396,9 @@ func (m *Manager) writeTargetFiles(mainConfig string, routeConfig string, suppor
if err := m.writeCertFiles(supportFiles); err != nil { if err := m.writeCertFiles(supportFiles); err != nil {
return err return err
} }
if strings.TrimSpace(m.OpenrestyResolverDirective) == "" && strings.Contains(routeConfig, "set $openflare_upstream ") {
slog.Warn("runtime-resolved hostname upstreams detected without available resolvers; hostname origin requests may fail until resolvers are configured")
}
renderedMainConfig := m.renderMainConfig(mainConfig) renderedMainConfig := m.renderMainConfig(mainConfig)
if err := os.WriteFile(m.MainConfigPath, []byte(renderedMainConfig), 0o644); err != nil { if err := os.WriteFile(m.MainConfigPath, []byte(renderedMainConfig), 0o644); err != nil {
return err return err
@@ -1022,23 +1027,27 @@ func ObservabilityListenAddress(openrestyPath string, port int) string {
return fmt.Sprintf("%d", port) return fmt.Sprintf("%d", port)
} }
func ResolverDirective(openrestyPath string) string { func ResolverDirective(openrestyPath string, explicitResolvers []string) string {
resolvers := resolverAddresses(openrestyPath) resolvers := resolverAddresses(openrestyPath, explicitResolvers)
if len(resolvers) == 0 { if len(resolvers) == 0 {
return "" return ""
} }
return fmt.Sprintf(" resolver %s valid=30s ipv6=off;\n resolver_timeout 5s;\n", strings.Join(resolvers, " ")) return fmt.Sprintf(" resolver %s valid=30s ipv6=off;\n resolver_timeout 5s;\n", strings.Join(resolvers, " "))
} }
func resolverAddresses(openrestyPath string) []string { func resolverAddresses(openrestyPath string, explicitResolvers []string) []string {
if strings.TrimSpace(openrestyPath) == "" { if resolvers := normalizeResolverAddresses(explicitResolvers); len(resolvers) > 0 {
return []string{"127.0.0.11"} return resolvers
} }
data, err := os.ReadFile("/etc/resolv.conf") data, err := os.ReadFile("/etc/resolv.conf")
if err != nil { if err != nil {
return nil return nil
} }
lines := strings.Split(string(data), "\n") return parseResolverAddresses(string(data), strings.TrimSpace(openrestyPath) == "")
}
func parseResolverAddresses(content string, dockerMode bool) []string {
lines := strings.Split(content, "\n")
resolvers := make([]string, 0, 2) resolvers := make([]string, 0, 2)
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, line := range lines { for _, line := range lines {
@@ -1050,6 +1059,9 @@ func resolverAddresses(openrestyPath string) []string {
if addr == "" { if addr == "" {
continue continue
} }
if dockerMode && !isUsableDockerResolver(addr) {
continue
}
if _, ok := seen[addr]; ok { if _, ok := seen[addr]; ok {
continue continue
} }
@@ -1059,6 +1071,45 @@ func resolverAddresses(openrestyPath string) []string {
return resolvers return resolvers
} }
func isUsableDockerResolver(addr string) bool {
ip := net.ParseIP(addr)
if ip == nil {
return false
}
return !ip.IsLoopback() && !ip.IsUnspecified()
}
func normalizeResolverAddresses(values []string) []string {
if len(values) == 0 {
return nil
}
resolvers := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
addr := strings.TrimSpace(value)
if addr == "" {
continue
}
if _, ok := seen[addr]; ok {
continue
}
seen[addr] = struct{}{}
resolvers = append(resolvers, addr)
}
if len(resolvers) == 0 {
return nil
}
return resolvers
}
func RequiresRuntimeResolver(originURL string) bool {
parsed, err := url.Parse(strings.TrimSpace(originURL))
if err != nil || parsed.Hostname() == "" {
return false
}
return net.ParseIP(parsed.Hostname()) == nil
}
func (m *Manager) routeConfigIncludePath() string { func (m *Manager) routeConfigIncludePath() string {
if strings.TrimSpace(m.RuntimeRouteConfigPath) != "" { if strings.TrimSpace(m.RuntimeRouteConfigPath) != "" {
return strings.TrimSpace(m.RuntimeRouteConfigPath) return strings.TrimSpace(m.RuntimeRouteConfigPath)
+49 -4
View File
@@ -762,10 +762,55 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) {
} }
} }
func TestResolverDirectiveForDockerMode(t *testing.T) { func TestResolverDirectiveUsesExplicitResolvers(t *testing.T) {
got := ResolverDirective("") got := ResolverDirective("", []string{"10.0.0.2", "1.1.1.1"})
if !strings.Contains(got, "resolver 127.0.0.11") { if !strings.Contains(got, "resolver 10.0.0.2 1.1.1.1") {
t.Fatalf("expected docker resolver directive, got %q", got) t.Fatalf("expected explicit resolver directive, got %q", got)
}
}
func TestParseResolverAddressesFiltersLoopbackForDocker(t *testing.T) {
content := strings.Join([]string{
"nameserver 127.0.0.53",
"nameserver 10.0.0.2",
"nameserver ::1",
"nameserver 1.1.1.1",
}, "\n")
got := parseResolverAddresses(content, true)
expected := []string{"10.0.0.2", "1.1.1.1"}
if !reflect.DeepEqual(got, expected) {
t.Fatalf("unexpected docker resolvers: got %#v want %#v", got, expected)
}
}
func TestParseResolverAddressesKeepsLoopbackForLocalBinary(t *testing.T) {
content := strings.Join([]string{
"nameserver 127.0.0.53",
"nameserver 10.0.0.2",
}, "\n")
got := parseResolverAddresses(content, false)
expected := []string{"127.0.0.53", "10.0.0.2"}
if !reflect.DeepEqual(got, expected) {
t.Fatalf("unexpected local resolvers: got %#v want %#v", got, expected)
}
}
func TestRequiresRuntimeResolver(t *testing.T) {
testCases := []struct {
name string
originURL string
want bool
}{
{name: "hostname", originURL: "https://origin.internal", want: true},
{name: "ipv4", originURL: "https://10.0.0.8", want: false},
{name: "ipv6", originURL: "https://[2001:db8::1]", want: false},
{name: "invalid", originURL: "://bad", want: false},
}
for _, testCase := range testCases {
if got := RequiresRuntimeResolver(testCase.originURL); got != testCase.want {
t.Fatalf("%s: got %v want %v", testCase.name, got, testCase.want)
}
} }
} }
+1
View File
@@ -81,6 +81,7 @@ var OpenRestyProxyBusyBuffersSize = "64k"
var OpenRestyGzipEnabled = true var OpenRestyGzipEnabled = true
var OpenRestyGzipMinLength = 1024 var OpenRestyGzipMinLength = 1024
var OpenRestyGzipCompLevel = 5 var OpenRestyGzipCompLevel = 5
var OpenRestyResolvers = ""
var OpenRestyCacheEnabled = false var OpenRestyCacheEnabled = false
var OpenRestyCachePath = "" var OpenRestyCachePath = ""
var OpenRestyCacheLevels = "1:2" var OpenRestyCacheLevels = "1:2"
+17
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net"
"net/http" "net/http"
"openflare/common" "openflare/common"
"openflare/model" "openflare/model"
@@ -113,6 +114,16 @@ func validateOpenRestyOption(key string, value string) error {
default: default:
return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key) return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key)
} }
case "OpenRestyResolvers":
if trimmed == "" {
return nil
}
for _, token := range splitOpenRestyResolvers(trimmed) {
if net.ParseIP(token) == nil {
return fmt.Errorf("%s only supports IP resolver entries, invalid value %q", key, token)
}
}
return nil
case "OpenRestyEventsMultiAcceptEnabled", case "OpenRestyEventsMultiAcceptEnabled",
"OpenRestyWebsocketEnabled", "OpenRestyWebsocketEnabled",
"OpenRestyProxyRequestBufferingEnabled", "OpenRestyProxyRequestBufferingEnabled",
@@ -176,6 +187,12 @@ func validateOpenRestyOption(key string, value string) error {
} }
} }
func splitOpenRestyResolvers(value string) []string {
return strings.FieldsFunc(value, func(r rune) bool {
return r == ',' || r == '\n' || r == '\r' || r == '\t' || r == ' '
})
}
// GetOptions godoc // GetOptions godoc
// @Summary List editable options // @Summary List editable options
// @Tags Options // @Tags Options
@@ -32,6 +32,9 @@ func TestValidateOpenRestyOption(t *testing.T) {
{name: "cache use stale invalid", key: "OpenRestyCacheUseStale", value: "error whatever", wantErr: true}, {name: "cache use stale invalid", key: "OpenRestyCacheUseStale", value: "error whatever", wantErr: true},
{name: "gzip level valid", key: "OpenRestyGzipCompLevel", value: "9"}, {name: "gzip level valid", key: "OpenRestyGzipCompLevel", value: "9"},
{name: "gzip level invalid", key: "OpenRestyGzipCompLevel", value: "10", wantErr: true}, {name: "gzip level invalid", key: "OpenRestyGzipCompLevel", value: "10", wantErr: true},
{name: "resolvers empty", key: "OpenRestyResolvers", value: ""},
{name: "resolvers valid", key: "OpenRestyResolvers", value: "1.1.1.1, 8.8.8.8"},
{name: "resolvers invalid", key: "OpenRestyResolvers", value: "dns.internal", wantErr: true},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
+3
View File
@@ -80,6 +80,7 @@ func InitOptionMap() {
common.OptionMap["OpenRestyGzipEnabled"] = strconv.FormatBool(common.OpenRestyGzipEnabled) common.OptionMap["OpenRestyGzipEnabled"] = strconv.FormatBool(common.OpenRestyGzipEnabled)
common.OptionMap["OpenRestyGzipMinLength"] = strconv.Itoa(common.OpenRestyGzipMinLength) common.OptionMap["OpenRestyGzipMinLength"] = strconv.Itoa(common.OpenRestyGzipMinLength)
common.OptionMap["OpenRestyGzipCompLevel"] = strconv.Itoa(common.OpenRestyGzipCompLevel) common.OptionMap["OpenRestyGzipCompLevel"] = strconv.Itoa(common.OpenRestyGzipCompLevel)
common.OptionMap["OpenRestyResolvers"] = common.OpenRestyResolvers
common.OptionMap["OpenRestyCacheEnabled"] = strconv.FormatBool(common.OpenRestyCacheEnabled) common.OptionMap["OpenRestyCacheEnabled"] = strconv.FormatBool(common.OpenRestyCacheEnabled)
common.OptionMap["OpenRestyCachePath"] = common.OpenRestyCachePath common.OptionMap["OpenRestyCachePath"] = common.OpenRestyCachePath
common.OptionMap["OpenRestyCacheLevels"] = common.OpenRestyCacheLevels common.OptionMap["OpenRestyCacheLevels"] = common.OpenRestyCacheLevels
@@ -298,6 +299,8 @@ func updateOptionMap(key string, value string) {
if v, err := strconv.Atoi(value); err == nil && v > 0 { if v, err := strconv.Atoi(value); err == nil && v > 0 {
common.OpenRestyGzipCompLevel = v common.OpenRestyGzipCompLevel = v
} }
case "OpenRestyResolvers":
common.OpenRestyResolvers = strings.TrimSpace(value)
case "OpenRestyCacheEnabled": case "OpenRestyCacheEnabled":
common.OpenRestyCacheEnabled = value == "true" common.OpenRestyCacheEnabled = value == "true"
case "OpenRestyCachePath": case "OpenRestyCachePath":
+48 -12
View File
@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net"
"net/url" "net/url"
"openflare/common" "openflare/common"
"openflare/model" "openflare/model"
@@ -89,6 +90,7 @@ type openRestyConfigSnapshot struct {
GzipEnabled bool `json:"gzip_enabled"` GzipEnabled bool `json:"gzip_enabled"`
GzipMinLength int `json:"gzip_min_length"` GzipMinLength int `json:"gzip_min_length"`
GzipCompLevel int `json:"gzip_comp_level"` GzipCompLevel int `json:"gzip_comp_level"`
Resolvers string `json:"resolvers,omitempty"`
CacheEnabled bool `json:"cache_enabled"` CacheEnabled bool `json:"cache_enabled"`
CachePath string `json:"cache_path,omitempty"` CachePath string `json:"cache_path,omitempty"`
CacheLevels string `json:"cache_levels"` CacheLevels string `json:"cache_levels"`
@@ -124,7 +126,6 @@ const (
nginxLuaDirPlaceholder = "__OPENFLARE_LUA_DIR__" nginxLuaDirPlaceholder = "__OPENFLARE_LUA_DIR__"
nginxObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__" nginxObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__"
nginxObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__" nginxObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__"
nginxResolverDirectivePlaceholder = "__OPENFLARE_RESOLVER_DIRECTIVE__"
) )
var requiredMainConfigTemplatePlaceholders = []string{ var requiredMainConfigTemplatePlaceholders = []string{
@@ -355,7 +356,7 @@ func buildCurrentConfigBundle(requireRoutes bool) (*configBundle, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
routeConfig, supportFiles, err := renderRouteConfig(routes) routeConfig, supportFiles, err := renderRouteConfig(routes, openRestyConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -469,6 +470,7 @@ func buildOpenRestyConfigSnapshot() openRestyConfigSnapshot {
GzipEnabled: common.OpenRestyGzipEnabled, GzipEnabled: common.OpenRestyGzipEnabled,
GzipMinLength: common.OpenRestyGzipMinLength, GzipMinLength: common.OpenRestyGzipMinLength,
GzipCompLevel: common.OpenRestyGzipCompLevel, GzipCompLevel: common.OpenRestyGzipCompLevel,
Resolvers: common.OpenRestyResolvers,
CacheEnabled: common.OpenRestyCacheEnabled, CacheEnabled: common.OpenRestyCacheEnabled,
CachePath: common.OpenRestyCachePath, CachePath: common.OpenRestyCachePath,
CacheLevels: common.OpenRestyCacheLevels, CacheLevels: common.OpenRestyCacheLevels,
@@ -530,6 +532,7 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon
appendIfChanged("OpenRestyGzipEnabled", fmt.Sprintf("%t", left.GzipEnabled), fmt.Sprintf("%t", right.GzipEnabled)) appendIfChanged("OpenRestyGzipEnabled", fmt.Sprintf("%t", left.GzipEnabled), fmt.Sprintf("%t", right.GzipEnabled))
appendIfChanged("OpenRestyGzipMinLength", fmt.Sprintf("%d", left.GzipMinLength), fmt.Sprintf("%d", right.GzipMinLength)) appendIfChanged("OpenRestyGzipMinLength", fmt.Sprintf("%d", left.GzipMinLength), fmt.Sprintf("%d", right.GzipMinLength))
appendIfChanged("OpenRestyGzipCompLevel", fmt.Sprintf("%d", left.GzipCompLevel), fmt.Sprintf("%d", right.GzipCompLevel)) appendIfChanged("OpenRestyGzipCompLevel", fmt.Sprintf("%d", left.GzipCompLevel), fmt.Sprintf("%d", right.GzipCompLevel))
appendIfChanged("OpenRestyResolvers", left.Resolvers, right.Resolvers)
appendIfChanged("OpenRestyCacheEnabled", fmt.Sprintf("%t", left.CacheEnabled), fmt.Sprintf("%t", right.CacheEnabled)) appendIfChanged("OpenRestyCacheEnabled", fmt.Sprintf("%t", left.CacheEnabled), fmt.Sprintf("%t", right.CacheEnabled))
appendIfChanged("OpenRestyCachePath", left.CachePath, right.CachePath) appendIfChanged("OpenRestyCachePath", left.CachePath, right.CachePath)
appendIfChanged("OpenRestyCacheLevels", left.CacheLevels, right.CacheLevels) appendIfChanged("OpenRestyCacheLevels", left.CacheLevels, right.CacheLevels)
@@ -576,6 +579,7 @@ func openRestyOptionKeys() []string {
"OpenRestyGzipEnabled", "OpenRestyGzipEnabled",
"OpenRestyGzipMinLength", "OpenRestyGzipMinLength",
"OpenRestyGzipCompLevel", "OpenRestyGzipCompLevel",
"OpenRestyResolvers",
"OpenRestyCacheEnabled", "OpenRestyCacheEnabled",
"OpenRestyCachePath", "OpenRestyCachePath",
"OpenRestyCacheLevels", "OpenRestyCacheLevels",
@@ -588,7 +592,7 @@ func openRestyOptionKeys() []string {
} }
} }
func renderRouteConfig(routes []*model.ProxyRoute) (string, []SupportFile, error) { func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) (string, []SupportFile, error) {
var builder strings.Builder var builder strings.Builder
builder.WriteString("# This file is generated by OpenFlare. Do not edit manually.\n") builder.WriteString("# This file is generated by OpenFlare. Do not edit manually.\n")
supportFiles := make([]SupportFile, 0) supportFiles := make([]SupportFile, 0)
@@ -598,7 +602,7 @@ func renderRouteConfig(routes []*model.ProxyRoute) (string, []SupportFile, error
return "", nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain) return "", nil, fmt.Errorf("路由 %s 自定义请求头无效", route.Domain)
} }
if !route.EnableHTTPS { if !route.EnableHTTPS {
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders)) builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cfg))
continue continue
} }
if route.CertID == nil || *route.CertID == 0 { if route.CertID == nil || *route.CertID == 0 {
@@ -615,9 +619,9 @@ func renderRouteConfig(routes []*model.ProxyRoute) (string, []SupportFile, error
if route.RedirectHTTP { if route.RedirectHTTP {
builder.WriteString(renderHTTPRedirectServer(route.Domain)) builder.WriteString(renderHTTPRedirectServer(route.Domain))
} else { } else {
builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders)) builder.WriteString(renderHTTPProxyServer(route.Domain, route.OriginURL, route.OriginHost, customHeaders, cfg))
} }
builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, route.OriginHost, certificate.ID, customHeaders)) builder.WriteString(renderHTTPSServer(route.Domain, route.OriginURL, route.OriginHost, certificate.ID, customHeaders, cfg))
} }
return builder.String(), dedupeSupportFiles(supportFiles), nil return builder.String(), dedupeSupportFiles(supportFiles), nil
} }
@@ -673,7 +677,7 @@ func renderMainConfigTemplate(templateText string, cfg openRestyConfigSnapshot)
"{{OpenRestyGzip}}", onOff(cfg.GzipEnabled), "{{OpenRestyGzip}}", onOff(cfg.GzipEnabled),
"{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength), "{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength),
"{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel), "{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel),
"{{OpenRestyResolverDirective}}", nginxResolverDirectivePlaceholder, "{{OpenRestyResolverDirective}}", renderResolverDirective(cfg.Resolvers),
"{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg), "{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg),
"{{OpenRestyRouteConfigInclude}}", nginxRouteConfigPlaceholder, "{{OpenRestyRouteConfigInclude}}", nginxRouteConfigPlaceholder,
) )
@@ -752,18 +756,18 @@ func nextVersionNumber(now time.Time) (string, error) {
return fmt.Sprintf("%s-%03d", prefix, count+1), nil return fmt.Sprintf("%s-%03d", prefix, count+1), nil
} }
func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput) string { func renderHTTPProxyServer(domain string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cfg openRestyConfigSnapshot) string {
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n location / {\n%s%s }\n}\n\n", domain, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL)) return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n location / {\n%s%s }\n}\n\n", domain, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL, cfg))
} }
func renderHTTPRedirectServer(domain string) string { func renderHTTPRedirectServer(domain string) string {
return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n return 301 https://$host$request_uri;\n}\n\n", domain, renderExactHostGuard(domain)) return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s\n return 301 https://$host$request_uri;\n}\n\n", domain, renderExactHostGuard(domain))
} }
func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput) string { func renderHTTPSServer(domain string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cfg openRestyConfigSnapshot) string {
certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID))
keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID)) keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID))
return fmt.Sprintf("server {\n listen 443 ssl;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s\n location / {\n%s%s }\n}\n\n", domain, certPath, keyPath, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL)) return fmt.Sprintf("server {\n listen 443 ssl;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s\n location / {\n%s%s }\n}\n\n", domain, certPath, keyPath, renderExactHostGuard(domain), renderProxyHeaderBlock(originURL, originHost, customHeaders), renderProxyPassBlock(originURL, cfg))
} }
func renderExactHostGuard(domain string) string { func renderExactHostGuard(domain string) string {
@@ -798,11 +802,14 @@ func renderProxyHeaderBlock(originURL string, originHost string, customHeaders [
return builder.String() return builder.String()
} }
func renderProxyPassBlock(originURL string) string { func renderProxyPassBlock(originURL string, cfg openRestyConfigSnapshot) string {
parsed, err := url.Parse(originURL) parsed, err := url.Parse(originURL)
if err != nil || parsed.Host == "" || parsed.Scheme == "" { if err != nil || parsed.Host == "" || parsed.Scheme == "" {
return fmt.Sprintf(" proxy_pass %s;\n", originURL) return fmt.Sprintf(" proxy_pass %s;\n", originURL)
} }
if !shouldUseRuntimeResolver(originURL, cfg.Resolvers) {
return fmt.Sprintf(" proxy_pass %s;\n", originURL)
}
upstreamURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) upstreamURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
basePath := strings.TrimRight(parsed.EscapedPath(), "/") basePath := strings.TrimRight(parsed.EscapedPath(), "/")
if basePath == "" || basePath == "." { if basePath == "" || basePath == "." {
@@ -825,6 +832,35 @@ func renderProxyPassBlock(originURL string) string {
return builder.String() return builder.String()
} }
func shouldUseRuntimeResolver(originURL string, resolvers string) bool {
if strings.TrimSpace(resolvers) == "" {
return false
}
return requiresRuntimeResolver(originURL)
}
func requiresRuntimeResolver(originURL string) bool {
parsed, err := url.Parse(strings.TrimSpace(originURL))
if err != nil || parsed.Hostname() == "" {
return false
}
return net.ParseIP(parsed.Hostname()) == nil
}
func renderResolverDirective(value string) string {
resolvers := splitResolverList(value)
if len(resolvers) == 0 {
return ""
}
return fmt.Sprintf(" resolver %s valid=30s ipv6=off;\n resolver_timeout 5s;\n", strings.Join(resolvers, " "))
}
func splitResolverList(value string) []string {
return strings.FieldsFunc(strings.TrimSpace(value), func(r rune) bool {
return r == ',' || r == '\n' || r == '\r' || r == '\t' || r == ' '
})
}
func resolveUpstreamServerName(originURL string, originHost string) string { func resolveUpstreamServerName(originURL string, originHost string) string {
parsed, err := url.Parse(originURL) parsed, err := url.Parse(originURL)
if err != nil || !strings.EqualFold(parsed.Scheme, "https") { if err != nil || !strings.EqualFold(parsed.Scheme, "https") {
+62 -8
View File
@@ -63,8 +63,8 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) {
if !strings.Contains(result.Version.MainConfig, "listen __OPENFLARE_OBSERVABILITY_LISTEN__;") { if !strings.Contains(result.Version.MainConfig, "listen __OPENFLARE_OBSERVABILITY_LISTEN__;") {
t.Fatal("expected main config to include managed openresty observability listen placeholder") t.Fatal("expected main config to include managed openresty observability listen placeholder")
} }
if !strings.Contains(result.Version.MainConfig, "__OPENFLARE_RESOLVER_DIRECTIVE__") { if strings.Contains(result.Version.MainConfig, "resolver ") {
t.Fatal("expected main config to include managed resolver directive placeholder") t.Fatal("expected main config to omit resolver directive when no resolvers are configured")
} }
if strings.Contains(result.Version.MainConfig, "allow 127.0.0.1;") { if strings.Contains(result.Version.MainConfig, "allow 127.0.0.1;") {
t.Fatal("expected main config to avoid hard-coded allow rules on observability server") t.Fatal("expected main config to avoid hard-coded allow rules on observability server")
@@ -141,11 +141,11 @@ func TestPublishConfigVersionRendersCustomHeaders(t *testing.T) {
if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Connection $http_connection;") { if !strings.Contains(result.Version.RenderedConfig, "proxy_set_header Connection $http_connection;") {
t.Fatal("expected rendered config to forward websocket connection header") t.Fatal("expected rendered config to forward websocket connection header")
} }
if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "https://origin.internal";`) { if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://origin.internal;") {
t.Fatal("expected rendered config to defer upstream resolution via variable proxy_pass") t.Fatal("expected rendered config to keep direct proxy_pass when no resolvers are configured")
} }
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") { if strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") {
t.Fatal("expected rendered config to proxy via runtime-resolved upstream variable") t.Fatal("expected rendered config to avoid runtime-resolved proxy_pass when no resolvers are configured")
} }
} }
@@ -175,14 +175,68 @@ func TestPublishConfigVersionOverridesOriginHostHeader(t *testing.T) {
if !strings.Contains(result.Version.RenderedConfig, `proxy_ssl_name "git.arctel.net";`) { if !strings.Contains(result.Version.RenderedConfig, `proxy_ssl_name "git.arctel.net";`) {
t.Fatal("expected rendered config to set proxy ssl name from origin host override") t.Fatal("expected rendered config to set proxy ssl name from origin host override")
} }
if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "https://git.arctel.net";`) { if !strings.Contains(result.Version.RenderedConfig, "proxy_pass https://git.arctel.net;") {
t.Fatal("expected rendered config to avoid resolving https upstream during config load") t.Fatal("expected rendered config to keep direct proxy_pass for hostname origin when resolvers are blank")
} }
if !strings.Contains(result.Version.SnapshotJSON, `"origin_host":"git.arctel.net"`) { if !strings.Contains(result.Version.SnapshotJSON, `"origin_host":"git.arctel.net"`) {
t.Fatal("expected snapshot to include origin_host override") t.Fatal("expected snapshot to include origin_host override")
} }
} }
func TestPublishConfigVersionUsesRuntimeResolverWhenConfigured(t *testing.T) {
setupServiceTestDB(t)
if err := model.UpdateOption("OpenRestyResolvers", "1.1.1.1, 8.8.8.8"); err != nil {
t.Fatalf("UpdateOption OpenRestyResolvers failed: %v", err)
}
_, err := CreateProxyRoute(ProxyRouteInput{
Domain: "resolver.example.com",
OriginURL: "https://origin.internal",
Enabled: true,
})
if err != nil {
t.Fatalf("CreateProxyRoute failed: %v", err)
}
result, err := PublishConfigVersion("root")
if err != nil {
t.Fatalf("PublishConfigVersion failed: %v", err)
}
if !strings.Contains(result.Version.MainConfig, "resolver 1.1.1.1 8.8.8.8 valid=30s ipv6=off;") {
t.Fatal("expected main config to render configured resolver directive")
}
if !strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "https://origin.internal";`) {
t.Fatal("expected rendered config to use runtime upstream variable when resolvers are configured")
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass $openflare_upstream$request_uri;") {
t.Fatal("expected rendered config to proxy via runtime-resolved upstream variable when resolvers are configured")
}
}
func TestPublishConfigVersionKeepsDirectProxyPassForIPOrigins(t *testing.T) {
setupServiceTestDB(t)
_, err := CreateProxyRoute(ProxyRouteInput{
Domain: "ip-origin.example.com",
OriginURL: "http://10.0.0.8:8080",
Enabled: true,
})
if err != nil {
t.Fatalf("CreateProxyRoute failed: %v", err)
}
result, err := PublishConfigVersion("root")
if err != nil {
t.Fatalf("PublishConfigVersion failed: %v", err)
}
if !strings.Contains(result.Version.RenderedConfig, "proxy_pass http://10.0.0.8:8080;") {
t.Fatal("expected rendered config to keep direct proxy_pass for IP origin")
}
if strings.Contains(result.Version.RenderedConfig, `set $openflare_upstream "http://10.0.0.8:8080"`) {
t.Fatal("expected rendered config to avoid runtime resolver variables for IP origin")
}
}
func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) { func TestPreviewConfigVersionCanDisableWebsocketHeaders(t *testing.T) {
setupServiceTestDB(t) setupServiceTestDB(t)
@@ -55,6 +55,7 @@ const defaultPerformanceFields = {
OpenRestyGzipEnabled: true, OpenRestyGzipEnabled: true,
OpenRestyGzipMinLength: '1024', OpenRestyGzipMinLength: '1024',
OpenRestyGzipCompLevel: '5', OpenRestyGzipCompLevel: '5',
OpenRestyResolvers: '',
OpenRestyCacheEnabled: false, OpenRestyCacheEnabled: false,
OpenRestyCachePath: '', OpenRestyCachePath: '',
OpenRestyCacheLevels: '1:2', OpenRestyCacheLevels: '1:2',
@@ -103,6 +104,8 @@ const performanceFieldTooltips: Record<string, string> = {
gzip_min_length: gzip_min_length:
'只有响应体超过该字节数时才会启用 gzip,避免对极小响应做无意义压缩。', '只有响应体超过该字节数时才会启用 gzip,避免对极小响应做无意义压缩。',
gzip_comp_level: 'gzip 压缩等级,1 更省 CPU,9 压缩更高但更耗 CPU。', gzip_comp_level: 'gzip 压缩等级,1 更省 CPU,9 压缩更高但更耗 CPU。',
resolvers:
'可选填写运行时 DNS 解析器 IP,支持逗号、空格或换行分隔;留空时不额外生成 resolver 指令。',
proxy_cache_path: '缓存目录路径,对应 proxy_cache_path 指令中的磁盘位置。', proxy_cache_path: '缓存目录路径,对应 proxy_cache_path 指令中的磁盘位置。',
levels: '缓存目录层级,例如 1:2,可控制缓存文件的目录分布。', levels: '缓存目录层级,例如 1:2,可控制缓存文件的目录分布。',
inactive: '缓存对象在未命中访问时的失活时间,例如 30m。', inactive: '缓存对象在未命中访问时的失活时间,例如 30m。',
@@ -241,6 +244,7 @@ export function PerformancePage() {
OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true), OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true),
OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? '1024', OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? '1024',
OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? '5', OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? '5',
OpenRestyResolvers: optionMap.OpenRestyResolvers ?? '',
OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false), OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false),
OpenRestyCachePath: optionMap.OpenRestyCachePath ?? '', OpenRestyCachePath: optionMap.OpenRestyCachePath ?? '',
OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? '1:2', OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? '1:2',
@@ -420,6 +424,7 @@ export function PerformancePage() {
'OpenRestyProxyReadTimeout', 'OpenRestyProxyReadTimeout',
performanceFields.OpenRestyProxyReadTimeout.trim(), performanceFields.OpenRestyProxyReadTimeout.trim(),
], ],
['OpenRestyResolvers', performanceFields.OpenRestyResolvers.trim()],
[ [
'OpenRestyWebsocketEnabled', 'OpenRestyWebsocketEnabled',
String(performanceFields.OpenRestyWebsocketEnabled), String(performanceFields.OpenRestyWebsocketEnabled),
@@ -938,6 +943,23 @@ export function PerformancePage() {
} }
/> />
</ResourceField> </ResourceField>
<ResourceField
label="resolver"
tooltip={performanceFieldTooltips.resolvers}
hint="留空时走 OpenResty 默认行为;填写时请使用 DNS 服务器 IP。"
>
<ResourceTextarea
value={performanceFields.OpenRestyResolvers}
onChange={(event) =>
setPerformanceFields((previous) => ({
...previous,
OpenRestyResolvers: event.target.value,
}))
}
placeholder="例如:10.0.0.2, 1.1.1.1"
minRows={3}
/>
</ResourceField>
<ToggleField <ToggleField
label="websocket" label="websocket"
tooltip={performanceFieldTooltips.websocket} tooltip={performanceFieldTooltips.websocket}