fix: preserve Host header in all proxy hops (Go client overrides it with URL host)
This commit is contained in:
parent
f285295664
commit
fd5c09f9e2
6 changed files with 180 additions and 5 deletions
12
.agents.md
Normal file
12
.agents.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -12,6 +12,7 @@ type Config struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
WAFURL string `json:"waf_url"`
|
WAFURL string `json:"waf_url"`
|
||||||
ReturnPort int `json:"return_port"`
|
ReturnPort int `json:"return_port"`
|
||||||
|
BackendURL string `json:"backend_url"`
|
||||||
HealthInterval int `json:"health_interval"`
|
HealthInterval int `json:"health_interval"`
|
||||||
TimeoutMs int `json:"timeout_ms"`
|
TimeoutMs int `json:"timeout_ms"`
|
||||||
}
|
}
|
||||||
|
|
@ -34,8 +35,9 @@ func defaultConfig() Config {
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
WAFURL: "http://127.0.0.1:8080",
|
WAFURL: "http://127.0.0.1:8080",
|
||||||
ReturnPort: 8081,
|
ReturnPort: 8081,
|
||||||
|
BackendURL: "http://127.0.0.1:80",
|
||||||
HealthInterval: 5,
|
HealthInterval: 5,
|
||||||
TimeoutMs: 500,
|
TimeoutMs: 10000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
51
main.go
51
main.go
|
|
@ -4,8 +4,11 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"zoraxy-firewall/zoraxy_plugin"
|
"zoraxy-firewall/zoraxy_plugin"
|
||||||
)
|
)
|
||||||
|
|
@ -15,6 +18,7 @@ var webFS embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
loadConfig()
|
loadConfig()
|
||||||
|
loadHostMap()
|
||||||
|
|
||||||
spec := &zoraxy_plugin.IntroSpect{
|
spec := &zoraxy_plugin.IntroSpect{
|
||||||
ID: "zoraxy-firewall",
|
ID: "zoraxy-firewall",
|
||||||
|
|
@ -40,6 +44,20 @@ func main() {
|
||||||
|
|
||||||
startHealthCheck()
|
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 := http.NewServeMux()
|
||||||
mux.Handle("/inspect/", http.HandlerFunc(handleInspect))
|
mux.Handle("/inspect/", http.HandlerFunc(handleInspect))
|
||||||
mux.HandleFunc("/inspect", handleInspect)
|
mux.HandleFunc("/inspect", handleInspect)
|
||||||
|
|
@ -100,6 +118,39 @@ func handleGetStats(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, http.StatusOK, metricsSnapshot())
|
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{}) {
|
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -46,8 +47,8 @@ func handleInspect(w http.ResponseWriter, r *http.Request) {
|
||||||
if breaker.State() == StateOpen {
|
if breaker.State() == StateOpen {
|
||||||
log.Printf("Firewall: breaker OPEN — bypass %s %s", r.Method, r.URL.Path)
|
log.Printf("Firewall: breaker OPEN — bypass %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
// Bypass: forward to WAF (WAF sends back to Zoraxy for routing).
|
// Skip WAF, proxy directly to return listener → backend.
|
||||||
forwardToWAF(w, r)
|
forwardToReturn(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +84,7 @@ func forwardToWAF(w http.ResponseWriter, r *http.Request) (*http.Response, error
|
||||||
http.Error(w, "proxy error", http.StatusInternalServerError)
|
http.Error(w, "proxy error", http.StatusInternalServerError)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
proxyReq.Host = r.Host
|
||||||
for key, vals := range r.Header {
|
for key, vals := range r.Header {
|
||||||
for _, v := range vals {
|
for _, v := range vals {
|
||||||
proxyReq.Header.Add(key, v)
|
proxyReq.Header.Add(key, v)
|
||||||
|
|
@ -108,3 +109,36 @@ func forwardToWAF(w http.ResponseWriter, r *http.Request) (*http.Response, error
|
||||||
|
|
||||||
return resp, nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="timeout-ms">Proxy Timeout (ms)</label>
|
<label for="timeout-ms">Proxy Timeout (ms)</label>
|
||||||
<input type="number" id="timeout-ms" placeholder="500" min="100" max="10000">
|
<input type="number" id="timeout-ms" placeholder="10000" min="100" max="30000">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="save-btn" class="btn btn-primary">Save Configuration</button>
|
<button id="save-btn" class="btn btn-primary">Save Configuration</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue