feat: auto-discover backends from Zoraxy proxy rules — no per-domain config needed

This commit is contained in:
Claus Lohmar 2026-08-07 14:36:05 +00:00
parent d0b85fb635
commit 8b3bb93be3
3 changed files with 93 additions and 11 deletions

76
hosts.go Normal file
View file

@ -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
}

View file

@ -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",

View file

@ -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("<html><body><h1>403 Forbidden</h1><p>Blocked by Firewall</p></body></html>"))
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