From 8b3bb93be3dd28a3b5b748cafb1a7f19763e61a0 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Fri, 7 Aug 2026 14:36:05 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20auto-discover=20backends=20from=20Zorax?= =?UTF-8?q?y=20proxy=20rules=20=E2=80=94=20no=20per-domain=20config=20need?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hosts.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + middleware.go | 27 ++++++++++-------- 3 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 hosts.go 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 d6a225e..57d438a 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ var webFS embed.FS func main() { loadConfig() + loadHostMap() // build domain→backend mapping from Zoraxy proxy rules spec := &zoraxy_plugin.IntroSpect{ ID: "zoraxy-firewall", diff --git a/middleware.go b/middleware.go index 3f30063..cadaf82 100644 --- a/middleware.go +++ b/middleware.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "net/url" + "strings" "sync/atomic" "time" ) @@ -44,24 +45,26 @@ func handleInspect(w http.ResponseWriter, r *http.Request) { if !cfg.Enabled { atomic.AddUint64(&requestsBypassed, 1) - proxyToBackend(w, r) + w.Header().Set("X-Zoraxy-Status", "unhandled") + w.WriteHeader(http.StatusOK) return } if breaker.State() == StateOpen { atomic.AddUint64(&requestsBypassed, 1) - log.Printf("Firewall: breaker OPEN — fail-open, passing %s %s", r.Method, r.URL.Path) - proxyToBackend(w, r) + log.Printf("Firewall: breaker OPEN — fail-open %s %s", r.Method, r.URL.Path) + w.Header().Set("X-Zoraxy-Status", "unhandled") + w.WriteHeader(http.StatusOK) return } - // Inspect via WAF. status, err := inspect(r) if err != nil { atomic.AddUint64(&requestsBypassed, 1) log.Printf("Firewall: inspection error — fail-open: %v", err) breaker.RecordFailure() - proxyToBackend(w, r) + w.Header().Set("X-Zoraxy-Status", "unhandled") + w.WriteHeader(http.StatusOK) return } @@ -70,23 +73,25 @@ func handleInspect(w http.ResponseWriter, r *http.Request) { if status == 403 { atomic.AddUint64(&requestsBlocked, 1) log.Printf("Firewall: BLOCKED %s %s", r.Method, r.URL.Path) - w.Header().Set("Content-Type", "text/html") w.WriteHeader(403) w.Write([]byte("

403 Forbidden

Blocked by Firewall

")) return } - // WAF allows — proxy to backend. atomic.AddUint64(&requestsAllowed, 1) - proxyToBackend(w, r) + w.Header().Set("X-Zoraxy-Status", "unhandled") + w.WriteHeader(http.StatusOK) } // proxyToBackend forwards the request to Zoraxy's original upstream. // The original target is passed by Zoraxy via the X-Zoraxy-Uri header. func proxyToBackend(w http.ResponseWriter, r *http.Request) { - cfg := getConfig() - - target, err := url.Parse(cfg.BackendURL) + // Resolve the correct backend for this domain from Zoraxy's proxy rules. + backend := resolveBackend(r.Host) + if !strings.HasPrefix(backend, "http") { + backend = "http://" + backend + } + target, err := url.Parse(backend) if err != nil { http.Error(w, "invalid backend URL", http.StatusInternalServerError) return