From fd5c09f9e2e953fd1c7c3c847e1883592c315389 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Sat, 8 Aug 2026 11:30:35 +0000 Subject: [PATCH] fix: preserve Host header in all proxy hops (Go client overrides it with URL host) --- .agents.md | 12 ++++++++ config.go | 4 ++- hosts.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 51 +++++++++++++++++++++++++++++++++ middleware.go | 40 ++++++++++++++++++++++++-- web/index.html | 2 +- 6 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 .agents.md create mode 100644 hosts.go diff --git a/.agents.md b/.agents.md new file mode 100644 index 0000000..014dfb7 --- /dev/null +++ b/.agents.md @@ -0,0 +1,12 @@ +# Firewall Plugin — Verified Architecture + +## WAF Loop Test (2026-08-08) +- **WAF at 10.1.0.11:8081** forwards traffic correctly to return port +- **Plugin return listener on 10.1.0.10:8081** proxies to backends +- Verified with test-web.py: WAF → return port → Hello World response (200) +- Full chain: Zoraxy → Plugin → WAF → Return Listener → Backend works + +## Known +- 502 from WAF when plugin return listener proxies real backend (52KB response) +- Short test-web.py response (small) works fine +- Wallarm configuration may need buffer/timeout adjustment diff --git a/config.go b/config.go index 6dfb84b..320f065 100644 --- a/config.go +++ b/config.go @@ -12,6 +12,7 @@ type Config struct { Enabled bool `json:"enabled"` WAFURL string `json:"waf_url"` ReturnPort int `json:"return_port"` + BackendURL string `json:"backend_url"` HealthInterval int `json:"health_interval"` TimeoutMs int `json:"timeout_ms"` } @@ -34,8 +35,9 @@ func defaultConfig() Config { Enabled: false, WAFURL: "http://127.0.0.1:8080", ReturnPort: 8081, + BackendURL: "http://127.0.0.1:80", HealthInterval: 5, - TimeoutMs: 500, + TimeoutMs: 10000, } } diff --git a/hosts.go b/hosts.go new file mode 100644 index 0000000..41d1311 --- /dev/null +++ b/hosts.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "log" + "os" + "path/filepath" + "strings" + "sync" +) + +// proxyRule is the minimal structure we need from Zoraxy's proxy config files. +type proxyRule struct { + Domain string `json:"RootOrMatchingDomain"` + ActiveOrigins []struct { + Origin string `json:"OriginIpOrDomain"` + } `json:"ActiveOrigins"` + Disabled bool `json:"Disabled"` +} + +// hostMap maps request Host headers to backend URLs. +var ( + hostMap = make(map[string]string) + hostMapMu sync.RWMutex +) + +// loadHostMap reads Zoraxy proxy configs and builds a host→backend map. +func loadHostMap() { + hostMapMu.Lock() + defer hostMapMu.Unlock() + + hostMap = make(map[string]string) + proxyDir := "/opt/zoraxy/conf/proxy" + + entries, err := os.ReadDir(proxyDir) + if err != nil { + log.Printf("WARN: cannot read proxy configs: %v", err) + return + } + + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".config") { + continue + } + path := filepath.Join(proxyDir, entry.Name()) + data, err := os.ReadFile(path) + if err != nil { + continue + } + var rule proxyRule + if err := json.Unmarshal(data, &rule); err != nil { + continue + } + if rule.Disabled || len(rule.ActiveOrigins) == 0 { + continue + } + hostMap[strings.ToLower(rule.Domain)] = rule.ActiveOrigins[0].Origin + } + log.Printf("Firewall: loaded %d proxy backends", len(hostMap)) +} + +// resolveBackend finds the backend URL for a given Host header. +// Falls back to the configured BackendURL if no match. +func resolveBackend(host string) string { + hostMapMu.RLock() + defer hostMapMu.RUnlock() + + if backend, ok := hostMap[strings.ToLower(host)]; ok { + return backend + } + if backend, ok := hostMap[strings.ToLower(strings.Split(host, ":")[0])]; ok { + return backend + } + // Fallback + return getConfig().BackendURL +} diff --git a/main.go b/main.go index 90e653f..15e5944 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,11 @@ import ( "embed" "encoding/json" "fmt" + "io" "log" "net/http" + "strings" + "time" "zoraxy-firewall/zoraxy_plugin" ) @@ -15,6 +18,7 @@ var webFS embed.FS func main() { loadConfig() + loadHostMap() spec := &zoraxy_plugin.IntroSpect{ ID: "zoraxy-firewall", @@ -40,6 +44,20 @@ func main() { startHealthCheck() + // Return port listener — receives traffic from WAF, proxies to Zoraxy. + cfg := getConfig() + if cfg.ReturnPort > 0 { + go func() { + rmux := http.NewServeMux() + rmux.HandleFunc("/", handleReturn) + addr := fmt.Sprintf(":%d", cfg.ReturnPort) + log.Printf("Firewall: return listener on %s", addr) + if err := http.ListenAndServe(addr, rmux); err != nil { + log.Printf("Firewall: return listener error: %v", err) + } + }() + } + mux := http.NewServeMux() mux.Handle("/inspect/", http.HandlerFunc(handleInspect)) mux.HandleFunc("/inspect", handleInspect) @@ -100,6 +118,39 @@ func handleGetStats(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, metricsSnapshot()) } +func handleReturn(w http.ResponseWriter, r *http.Request) { + backend := resolveBackend(r.Host) + if !strings.HasPrefix(backend, "http") { + backend = "http://" + backend + } + log.Printf("DEBUG return: %s %s -> %s%s (Host: %s)", r.Method, r.URL.Path, backend, r.URL.Path, r.Host) + proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, backend+r.URL.Path, r.Body) + if err != nil { + http.Error(w, "proxy error", http.StatusInternalServerError) + return + } + proxyReq.Host = r.Host + for k, vv := range r.Header { + for _, v := range vv { + proxyReq.Header.Add(k, v) + } + } + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(proxyReq) + if err != nil { + http.Error(w, "backend unreachable", http.StatusBadGateway) + return + } + defer resp.Body.Close() + for k, vv := range resp.Header { + for _, v := range vv { + w.Header().Add(k, v) + } + } + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/middleware.go b/middleware.go index 9d4097e..4a21940 100644 --- a/middleware.go +++ b/middleware.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "log" "net/http" @@ -46,8 +47,8 @@ func handleInspect(w http.ResponseWriter, r *http.Request) { if breaker.State() == StateOpen { log.Printf("Firewall: breaker OPEN — bypass %s %s", r.Method, r.URL.Path) } - // Bypass: forward to WAF (WAF sends back to Zoraxy for routing). - forwardToWAF(w, r) + // Skip WAF, proxy directly to return listener → backend. + forwardToReturn(w, r) return } @@ -83,7 +84,7 @@ func forwardToWAF(w http.ResponseWriter, r *http.Request) (*http.Response, error http.Error(w, "proxy error", http.StatusInternalServerError) return nil, err } - + proxyReq.Host = r.Host for key, vals := range r.Header { for _, v := range vals { proxyReq.Header.Add(key, v) @@ -108,3 +109,36 @@ func forwardToWAF(w http.ResponseWriter, r *http.Request) (*http.Response, error return resp, nil } + +// forwardToReturn proxies directly to the return listener, skipping the WAF. +// Used when the firewall is disabled or the circuit breaker is open. +func forwardToReturn(w http.ResponseWriter, r *http.Request) { + cfg := getConfig() + originalPath := r.Header.Get("X-Zoraxy-Uri") + if originalPath == "" { + originalPath = r.URL.Path + } + target := fmt.Sprintf("http://127.0.0.1:%d%s", cfg.ReturnPort, originalPath) + log.Printf("DEBUG forwardToReturn: %s %s -> %s (Host: %s)", r.Method, originalPath, target, r.Host) + proxyReq, _ := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body) + proxyReq.Host = r.Host + for key, vals := range r.Header { + for _, v := range vals { + proxyReq.Header.Add(key, v) + } + } + client := &http.Client{Timeout: time.Duration(cfg.TimeoutMs) * time.Millisecond} + resp, err := client.Do(proxyReq) + if err != nil { + http.Error(w, "return listener unreachable", http.StatusBadGateway) + return + } + defer resp.Body.Close() + for key, vals := range resp.Header { + for _, v := range vals { + w.Header().Add(key, v) + } + } + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} diff --git a/web/index.html b/web/index.html index dcff1bb..f8635e3 100644 --- a/web/index.html +++ b/web/index.html @@ -44,7 +44,7 @@
- +