feat: auto-discover backends from Zoraxy proxy rules — no per-domain config needed
This commit is contained in:
parent
d0b85fb635
commit
8b3bb93be3
3 changed files with 93 additions and 11 deletions
76
hosts.go
Normal file
76
hosts.go
Normal 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
|
||||||
|
}
|
||||||
1
main.go
1
main.go
|
|
@ -15,6 +15,7 @@ var webFS embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
loadConfig()
|
loadConfig()
|
||||||
|
loadHostMap() // build domain→backend mapping from Zoraxy proxy rules
|
||||||
|
|
||||||
spec := &zoraxy_plugin.IntroSpect{
|
spec := &zoraxy_plugin.IntroSpect{
|
||||||
ID: "zoraxy-firewall",
|
ID: "zoraxy-firewall",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -44,24 +45,26 @@ func handleInspect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if !cfg.Enabled {
|
if !cfg.Enabled {
|
||||||
atomic.AddUint64(&requestsBypassed, 1)
|
atomic.AddUint64(&requestsBypassed, 1)
|
||||||
proxyToBackend(w, r)
|
w.Header().Set("X-Zoraxy-Status", "unhandled")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if breaker.State() == StateOpen {
|
if breaker.State() == StateOpen {
|
||||||
atomic.AddUint64(&requestsBypassed, 1)
|
atomic.AddUint64(&requestsBypassed, 1)
|
||||||
log.Printf("Firewall: breaker OPEN — fail-open, passing %s %s", r.Method, r.URL.Path)
|
log.Printf("Firewall: breaker OPEN — fail-open %s %s", r.Method, r.URL.Path)
|
||||||
proxyToBackend(w, r)
|
w.Header().Set("X-Zoraxy-Status", "unhandled")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inspect via WAF.
|
|
||||||
status, err := inspect(r)
|
status, err := inspect(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
atomic.AddUint64(&requestsBypassed, 1)
|
atomic.AddUint64(&requestsBypassed, 1)
|
||||||
log.Printf("Firewall: inspection error — fail-open: %v", err)
|
log.Printf("Firewall: inspection error — fail-open: %v", err)
|
||||||
breaker.RecordFailure()
|
breaker.RecordFailure()
|
||||||
proxyToBackend(w, r)
|
w.Header().Set("X-Zoraxy-Status", "unhandled")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,23 +73,25 @@ func handleInspect(w http.ResponseWriter, r *http.Request) {
|
||||||
if status == 403 {
|
if status == 403 {
|
||||||
atomic.AddUint64(&requestsBlocked, 1)
|
atomic.AddUint64(&requestsBlocked, 1)
|
||||||
log.Printf("Firewall: BLOCKED %s %s", r.Method, r.URL.Path)
|
log.Printf("Firewall: BLOCKED %s %s", r.Method, r.URL.Path)
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
w.WriteHeader(403)
|
w.WriteHeader(403)
|
||||||
w.Write([]byte("<html><body><h1>403 Forbidden</h1><p>Blocked by Firewall</p></body></html>"))
|
w.Write([]byte("<html><body><h1>403 Forbidden</h1><p>Blocked by Firewall</p></body></html>"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// WAF allows — proxy to backend.
|
|
||||||
atomic.AddUint64(&requestsAllowed, 1)
|
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.
|
// proxyToBackend forwards the request to Zoraxy's original upstream.
|
||||||
// The original target is passed by Zoraxy via the X-Zoraxy-Uri header.
|
// The original target is passed by Zoraxy via the X-Zoraxy-Uri header.
|
||||||
func proxyToBackend(w http.ResponseWriter, r *http.Request) {
|
func proxyToBackend(w http.ResponseWriter, r *http.Request) {
|
||||||
cfg := getConfig()
|
// Resolve the correct backend for this domain from Zoraxy's proxy rules.
|
||||||
|
backend := resolveBackend(r.Host)
|
||||||
target, err := url.Parse(cfg.BackendURL)
|
if !strings.HasPrefix(backend, "http") {
|
||||||
|
backend = "http://" + backend
|
||||||
|
}
|
||||||
|
target, err := url.Parse(backend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "invalid backend URL", http.StatusInternalServerError)
|
http.Error(w, "invalid backend URL", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue