simplify: inspection-only model — plugin inspects via WAF API, Zoraxy handles routing
This commit is contained in:
parent
6b851057b7
commit
32d60a3583
6 changed files with 49 additions and 112 deletions
10
config.go
10
config.go
|
|
@ -11,10 +11,9 @@ import (
|
|||
// Config holds all Firewall plugin settings.
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
WAFURL string `json:"waf_url"` // e.g. http://127.0.0.1:8081
|
||||
BackendURL string `json:"backend_url"` // e.g. http://10.1.0.20:5000
|
||||
HealthInterval int `json:"health_interval"` // seconds between health checks
|
||||
TimeoutMs int `json:"timeout_ms"` // proxy timeout in ms
|
||||
WAFURL string `json:"waf_url"`
|
||||
HealthInterval int `json:"health_interval"`
|
||||
TimeoutMs int `json:"timeout_ms"`
|
||||
}
|
||||
|
||||
func configPath() string {
|
||||
|
|
@ -33,8 +32,7 @@ var (
|
|||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Enabled: false,
|
||||
WAFURL: "http://127.0.0.1:8081",
|
||||
BackendURL: "http://127.0.0.1:80",
|
||||
WAFURL: "http://10.1.0.11:8081",
|
||||
HealthInterval: 5,
|
||||
TimeoutMs: 3000,
|
||||
}
|
||||
|
|
|
|||
3
main.go
3
main.go
|
|
@ -85,9 +85,6 @@ func handleSaveConfig(w http.ResponseWriter, r *http.Request) {
|
|||
if u := r.FormValue("waf_url"); u != "" {
|
||||
cfg.WAFURL = u
|
||||
}
|
||||
if u := r.FormValue("backend_url"); u != "" {
|
||||
cfg.BackendURL = u
|
||||
}
|
||||
if v := r.FormValue("health_interval"); v != "" {
|
||||
fmt.Sscanf(v, "%d", &cfg.HealthInterval)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
|
@ -12,7 +10,6 @@ import (
|
|||
|
||||
var breaker = newCircuitBreaker()
|
||||
|
||||
// Metrics
|
||||
var (
|
||||
requestsTotal uint64
|
||||
requestsAllowed uint64
|
||||
|
|
@ -43,7 +40,6 @@ func circuitStateStr(s CircuitState) string {
|
|||
return "unknown"
|
||||
}
|
||||
|
||||
// handleInspect is the static capture handler.
|
||||
func handleInspect(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddUint64(&requestsTotal, 1)
|
||||
cfg := getConfig()
|
||||
|
|
@ -54,48 +50,31 @@ func handleInspect(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
state := breaker.State()
|
||||
if state == StateOpen {
|
||||
// WAF is down — bypass directly to backend.
|
||||
if breaker.State() == StateOpen {
|
||||
atomic.AddUint64(&requestsBypassed, 1)
|
||||
log.Printf("Firewall: breaker OPEN — bypassing %s %s", r.Method, r.URL.Path)
|
||||
bypassToBackend(w, r)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_CAPTURED))
|
||||
log.Printf("Firewall: breaker OPEN — fail-open, passing %s %s", r.Method, r.URL.Path)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED))
|
||||
return
|
||||
}
|
||||
|
||||
// Proxy through WAF.
|
||||
resp, err := proxyThroughWAF(w, r)
|
||||
status, err := inspect(r)
|
||||
if err != nil {
|
||||
atomic.AddUint64(&requestsBypassed, 1)
|
||||
log.Printf("Firewall: WAF unreachable — bypassing %s %s: %v", r.Method, r.URL.Path, err)
|
||||
log.Printf("Firewall: inspection error — fail-open: %v", err)
|
||||
breaker.RecordFailure()
|
||||
bypassToBackend(w, r)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_CAPTURED))
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
breaker.RecordSuccess()
|
||||
|
||||
if resp.StatusCode == 403 {
|
||||
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(int(zoraxy_plugin.ControlStatusCode_CAPTURED))
|
||||
io.Copy(w, resp.Body)
|
||||
return
|
||||
}
|
||||
|
||||
// Pass-through: copy the WAF's response back.
|
||||
atomic.AddUint64(&requestsAllowed, 1)
|
||||
for key, vals := range resp.Header {
|
||||
for _, v := range vals {
|
||||
w.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
fmt.Fprintf(w, " ") // ensure WriteHeader is sent
|
||||
io.Copy(w, resp.Body)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_CAPTURED))
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED))
|
||||
}
|
||||
|
|
|
|||
97
proxy.go
97
proxy.go
|
|
@ -1,83 +1,54 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// proxyThroughWAF forwards the request to the WAF, returns the response.
|
||||
// The WAF is expected to either block (403) or forward to the backend (2xx).
|
||||
func proxyThroughWAF(w http.ResponseWriter, r *http.Request) (*http.Response, error) {
|
||||
const maxBodySample = 4096
|
||||
|
||||
// inspect sends the request metadata + body sample to the WAF for inspection.
|
||||
// Returns the WAF's HTTP status code or an error if unreachable.
|
||||
func inspect(r *http.Request) (int, error) {
|
||||
cfg := getConfig()
|
||||
|
||||
target, err := url.Parse(cfg.WAFURL)
|
||||
// Sample body.
|
||||
var bodySample []byte
|
||||
if r.Body != nil {
|
||||
bodySample = make([]byte, maxBodySample)
|
||||
n, _ := io.ReadFull(r.Body, bodySample)
|
||||
bodySample = bodySample[:n]
|
||||
// Restore body so downstream can read it.
|
||||
r.Body = io.NopCloser(io.MultiReader(bytes.NewReader(bodySample), r.Body))
|
||||
}
|
||||
|
||||
// Build inspection payload.
|
||||
payload, err := json.Marshal(map[string]interface{}{
|
||||
"method": r.Method,
|
||||
"url": r.URL.String(),
|
||||
"host": r.Host,
|
||||
"remote_addr": r.RemoteAddr,
|
||||
"content_type": r.Header.Get("Content-Type"),
|
||||
"content_length": r.ContentLength,
|
||||
"headers": r.Header,
|
||||
"body_sample": bodySample,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid WAF URL: %w", err)
|
||||
}
|
||||
|
||||
// Clone the request for the WAF.
|
||||
proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, target.String()+r.URL.Path, r.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create proxy request: %w", err)
|
||||
}
|
||||
|
||||
// Copy headers.
|
||||
for key, vals := range r.Header {
|
||||
for _, v := range vals {
|
||||
proxyReq.Header.Add(key, v)
|
||||
}
|
||||
}
|
||||
proxyReq.Header.Set("X-Forwarded-For", r.RemoteAddr)
|
||||
proxyReq.Header.Set("X-Real-IP", r.RemoteAddr)
|
||||
|
||||
client := &http.Client{Timeout: time.Duration(cfg.TimeoutMs) * time.Millisecond}
|
||||
resp, err := client.Do(proxyReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("WAF unreachable: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// bypassToBackend forwards the request directly to the backend (fail-open path).
|
||||
func bypassToBackend(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := getConfig()
|
||||
|
||||
target, err := url.Parse(cfg.BackendURL)
|
||||
if err != nil {
|
||||
http.Error(w, "backend unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, target.String()+r.URL.Path, r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "proxy error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for key, vals := range r.Header {
|
||||
for _, v := range vals {
|
||||
proxyReq.Header.Add(key, v)
|
||||
}
|
||||
return 0, fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: time.Duration(cfg.TimeoutMs) * time.Millisecond}
|
||||
resp, err := client.Do(proxyReq)
|
||||
resp, err := client.Post(cfg.WAFURL+"/inspect", "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
http.Error(w, "backend unreachable", http.StatusBadGateway)
|
||||
return
|
||||
return 0, fmt.Errorf("unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy response back.
|
||||
for key, vals := range resp.Header {
|
||||
for _, v := range vals {
|
||||
w.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
log.Printf("Firewall: %s %s → status %d", r.Method, r.URL.Path, resp.StatusCode)
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ function loadConfig() {
|
|||
$('#fw-enabled').prop('checked', data.enabled);
|
||||
$('#enabled-label').text(data.enabled ? 'Firewall Enabled' : 'Firewall Disabled');
|
||||
$('#waf-url').val(data.waf_url);
|
||||
$('#backend-url').val(data.backend_url);
|
||||
$('#health-interval').val(data.health_interval);
|
||||
$('#timeout-ms').val(data.timeout_ms);
|
||||
});
|
||||
|
|
@ -32,7 +31,6 @@ function saveConfig() {
|
|||
data: {
|
||||
enabled: $('#fw-enabled').is(':checked'),
|
||||
waf_url: $('#waf-url').val().trim(),
|
||||
backend_url: $('#backend-url').val().trim(),
|
||||
health_interval: $('#health-interval').val(),
|
||||
timeout_ms: $('#timeout-ms').val()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,15 +26,9 @@
|
|||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="waf-url">WAF Upstream URL</label>
|
||||
<input type="text" id="waf-url" placeholder="http://127.0.0.1:8081">
|
||||
<small>Requests are proxied through this WAF for inspection.</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="backend-url">Backend Fallback URL</label>
|
||||
<input type="text" id="backend-url" placeholder="http://10.1.0.20:5000">
|
||||
<small>Bypassed to directly when WAF is down (fail-open).</small>
|
||||
<label for="waf-url">WAF Inspection URL</label>
|
||||
<input type="text" id="waf-url" placeholder="http://10.1.0.11:8081">
|
||||
<small>The firewall service that inspects each request and returns a verdict.</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
|
|
|
|||
Loading…
Reference in a new issue