refactor: inline proxy model — WAF proxy + health checks + backend fallback + circuit breaker
This commit is contained in:
parent
2dc01e9aa0
commit
6b851057b7
9 changed files with 215 additions and 172 deletions
21
config.go
21
config.go
|
|
@ -8,15 +8,15 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// Config holds all Firewall plugin settings, persisted as JSON.
|
||||
// Config holds all Firewall plugin settings.
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
NodeURL string `json:"node_url"` // e.g. http://192.168.1.50:8080
|
||||
ReturnPort int `json:"return_port"` // port for async callbacks
|
||||
TimeoutMs int `json:"timeout_ms"` // inspection timeout in milliseconds
|
||||
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
|
||||
}
|
||||
|
||||
// configPath resolves to the config file next to the plugin binary.
|
||||
func configPath() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
|
|
@ -32,10 +32,11 @@ var (
|
|||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Enabled: false,
|
||||
NodeURL: "http://127.0.0.1:8080",
|
||||
ReturnPort: 9090,
|
||||
TimeoutMs: 3000,
|
||||
Enabled: false,
|
||||
WAFURL: "http://127.0.0.1:8081",
|
||||
BackendURL: "http://127.0.0.1:80",
|
||||
HealthInterval: 5,
|
||||
TimeoutMs: 3000,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
38
health.go
Normal file
38
health.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// startHealthCheck runs a periodic health check against the WAF.
|
||||
// It updates the circuit breaker based on WAF reachability.
|
||||
func startHealthCheck() {
|
||||
go func() {
|
||||
for {
|
||||
cfg := getConfig()
|
||||
interval := time.Duration(cfg.HealthInterval) * time.Second
|
||||
if interval < 1*time.Second {
|
||||
interval = 5 * time.Second
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
resp, err := client.Head(cfg.WAFURL + "/")
|
||||
if err != nil {
|
||||
log.Printf("WAF health check FAILED: %v", err)
|
||||
breaker.RecordFailure()
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode < 500 {
|
||||
breaker.RecordSuccess()
|
||||
} else {
|
||||
log.Printf("WAF health check: status %d", resp.StatusCode)
|
||||
breaker.RecordFailure()
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(interval)
|
||||
}
|
||||
}()
|
||||
}
|
||||
66
main.go
66
main.go
|
|
@ -17,19 +17,18 @@ func main() {
|
|||
loadConfig()
|
||||
|
||||
spec := &zoraxy_plugin.IntroSpect{
|
||||
ID: "zoraxy-firewall",
|
||||
Name: "Firewall",
|
||||
ID: "zoraxy-firewall",
|
||||
Name: "Firewall",
|
||||
Author: "Zoraxy Community",
|
||||
AuthorContact: "",
|
||||
Description: "Web Application Firewall with circuit breaker and fail-open protection.",
|
||||
Description: "Inline WAF proxy with health checks and fail-open bypass.",
|
||||
URL: "",
|
||||
Type: zoraxy_plugin.PluginType_Router, // Type 0 — intercepts traffic
|
||||
Type: zoraxy_plugin.PluginType_Router,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
UIPath: "/ui",
|
||||
|
||||
// Intercept ALL proxied requests.
|
||||
StaticCapturePaths: []zoraxy_plugin.StaticCaptureRule{{CapturePath: "/"}},
|
||||
StaticCaptureIngress: "/inspect",
|
||||
}
|
||||
|
|
@ -39,12 +38,15 @@ func main() {
|
|||
log.Fatalf("failed to receive config: %v", err)
|
||||
}
|
||||
|
||||
// Start background health checker.
|
||||
startHealthCheck()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// --- Static capture handler (traffic interception) ---
|
||||
// Static capture handler — all proxied traffic hits this.
|
||||
mux.HandleFunc("/inspect", handleInspect)
|
||||
|
||||
// --- UI + config API ---
|
||||
// UI + config API.
|
||||
uiRouter := zoraxy_plugin.NewPluginEmbedUIRouter(
|
||||
spec.ID, &webFS, "web", spec.UIPath,
|
||||
)
|
||||
|
|
@ -53,27 +55,12 @@ func main() {
|
|||
uiRouter.HandleFunc("/api/stats", handleGetStats, mux)
|
||||
|
||||
uiRouter.RegisterTerminateHandler(func() {
|
||||
log.Println("zoraxy-waf shutting down")
|
||||
log.Println("firewall shutting down")
|
||||
}, mux)
|
||||
uiRouter.AttachHandlerToMux(mux)
|
||||
|
||||
// --- Return port listener for inspection endpoint async callbacks (future) ---
|
||||
cfg := getConfig()
|
||||
if cfg.ReturnPort > 0 {
|
||||
go func() {
|
||||
rmux := http.NewServeMux()
|
||||
rmux.HandleFunc("/verdict", handleReturnVerdict)
|
||||
addr := fmt.Sprintf(":%d", cfg.ReturnPort)
|
||||
log.Printf("WAF return listener on %s", addr)
|
||||
if err := http.ListenAndServe(addr, rmux); err != nil {
|
||||
log.Printf("WAF return listener error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// --- Main plugin listener ---
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", config.Port)
|
||||
log.Printf("zoraxy-waf v%d.%d.%d listening on %s",
|
||||
log.Printf("firewall v%d.%d.%d listening on %s",
|
||||
spec.VersionMajor, spec.VersionMinor, spec.VersionPatch, addr)
|
||||
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
|
|
@ -81,11 +68,10 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Config API handlers ---
|
||||
// --- Config API ---
|
||||
|
||||
func handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
c := getConfig()
|
||||
writeJSON(w, http.StatusOK, c)
|
||||
writeJSON(w, http.StatusOK, getConfig())
|
||||
}
|
||||
|
||||
func handleSaveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -96,14 +82,17 @@ func handleSaveConfig(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
cfgMu.Lock()
|
||||
cfg.Enabled = r.FormValue("enabled") == "true"
|
||||
if u := r.FormValue("node_url"); u != "" {
|
||||
cfg.NodeURL = u
|
||||
if u := r.FormValue("waf_url"); u != "" {
|
||||
cfg.WAFURL = u
|
||||
}
|
||||
if p := r.FormValue("return_port"); p != "" {
|
||||
fmt.Sscanf(p, "%d", &cfg.ReturnPort)
|
||||
if u := r.FormValue("backend_url"); u != "" {
|
||||
cfg.BackendURL = u
|
||||
}
|
||||
if t := r.FormValue("timeout_ms"); t != "" {
|
||||
fmt.Sscanf(t, "%d", &cfg.TimeoutMs)
|
||||
if v := r.FormValue("health_interval"); v != "" {
|
||||
fmt.Sscanf(v, "%d", &cfg.HealthInterval)
|
||||
}
|
||||
if v := r.FormValue("timeout_ms"); v != "" {
|
||||
fmt.Sscanf(v, "%d", &cfg.TimeoutMs)
|
||||
}
|
||||
cfgMu.Unlock()
|
||||
|
||||
|
|
@ -119,17 +108,6 @@ func handleGetStats(w http.ResponseWriter, r *http.Request) {
|
|||
writeJSON(w, http.StatusOK, metricsSnapshot())
|
||||
}
|
||||
|
||||
func handleReturnVerdict(w http.ResponseWriter, r *http.Request) {
|
||||
// Future: inspection endpoint async callbacks arrive here.
|
||||
var resp InspectionResponse
|
||||
if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad verdict"})
|
||||
return
|
||||
}
|
||||
log.Printf("WAF async verdict: %s (reason=%s)", resp.Verdict, resp.Reason)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
|
@ -11,12 +12,12 @@ import (
|
|||
|
||||
var breaker = newCircuitBreaker()
|
||||
|
||||
// Metrics counters
|
||||
// Metrics
|
||||
var (
|
||||
requestsTotal uint64
|
||||
requestsAllowed uint64
|
||||
requestsBlocked uint64
|
||||
requestsBypassed uint64 // fail-open or disabled
|
||||
requestsBypassed uint64
|
||||
)
|
||||
|
||||
func metricsSnapshot() map[string]interface{} {
|
||||
|
|
@ -42,13 +43,11 @@ func circuitStateStr(s CircuitState) string {
|
|||
return "unknown"
|
||||
}
|
||||
|
||||
// handleInspect is the static capture ingress handler.
|
||||
// Zoraxy proxies matching requests here. The plugin decides:
|
||||
// - Return 280 (CAPTURED) → block the request
|
||||
// - Return 284 (UNHANDLED) → forward as normal
|
||||
// handleInspect is the static capture handler.
|
||||
func handleInspect(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddUint64(&requestsTotal, 1)
|
||||
cfg := getConfig()
|
||||
|
||||
if !cfg.Enabled {
|
||||
atomic.AddUint64(&requestsBypassed, 1)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED))
|
||||
|
|
@ -57,32 +56,46 @@ func handleInspect(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
state := breaker.State()
|
||||
if state == StateOpen {
|
||||
// WAF is down — bypass directly to backend.
|
||||
atomic.AddUint64(&requestsBypassed, 1)
|
||||
log.Printf("WAF: breaker OPEN — fail-open, passing %s %s", r.Method, r.URL.Path)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED))
|
||||
log.Printf("Firewall: breaker OPEN — bypassing %s %s", r.Method, r.URL.Path)
|
||||
bypassToBackend(w, r)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_CAPTURED))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := inspect(r)
|
||||
// Proxy through WAF.
|
||||
resp, err := proxyThroughWAF(w, r)
|
||||
if err != nil {
|
||||
atomic.AddUint64(&requestsBypassed, 1)
|
||||
log.Printf("WAF: inspection error: %v", err)
|
||||
log.Printf("Firewall: WAF unreachable — bypassing %s %s: %v", r.Method, r.URL.Path, err)
|
||||
breaker.RecordFailure()
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED))
|
||||
bypassToBackend(w, r)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_CAPTURED))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
breaker.RecordSuccess()
|
||||
|
||||
if resp.Verdict == "block" {
|
||||
if resp.StatusCode == 403 {
|
||||
atomic.AddUint64(&requestsBlocked, 1)
|
||||
log.Printf("WAF: BLOCKED %s %s — reason: %s", r.Method, r.URL.Path, resp.Reason)
|
||||
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))
|
||||
fmt.Fprintf(w, `<html><body><h1>403 Forbidden</h1><p>Request blocked by WAF.</p></body></html>`)
|
||||
io.Copy(w, resp.Body)
|
||||
return
|
||||
}
|
||||
|
||||
// Pass-through: copy the WAF's response back.
|
||||
atomic.AddUint64(&requestsAllowed, 1)
|
||||
w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED))
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
83
proxy.go
Normal file
83
proxy.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"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) {
|
||||
cfg := getConfig()
|
||||
|
||||
target, err := url.Parse(cfg.WAFURL)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: time.Duration(cfg.TimeoutMs) * time.Millisecond}
|
||||
resp, err := client.Do(proxyReq)
|
||||
if err != nil {
|
||||
http.Error(w, "backend unreachable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InspectionRequest is sent to the inspection endpoint.
|
||||
type InspectionRequest struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Host string `json:"host"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
ContentType string `json:"content_type"`
|
||||
ContentLength int64 `json:"content_length"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
BodySample []byte `json:"body_sample"` // first N bytes
|
||||
}
|
||||
|
||||
// InspectionResponse is returned by the inspection endpoint.
|
||||
type InspectionResponse struct {
|
||||
Verdict string `json:"verdict"` // "allow" or "block"
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Score int `json:"score,omitempty"`
|
||||
}
|
||||
|
||||
const maxBodySample = 4096 // only send first 4KB to inspection endpoint
|
||||
|
||||
// inspect sends a synchronous inspection request to the inspection endpoint.
|
||||
// Returns nil if allowed, or an error describing the block reason.
|
||||
func inspect(r *http.Request) (*InspectionResponse, error) {
|
||||
c := getConfig()
|
||||
|
||||
// Sample the first N bytes of the body.
|
||||
var bodySample []byte
|
||||
if r.Body != nil {
|
||||
bodySample = make([]byte, maxBodySample)
|
||||
n, _ := io.ReadFull(r.Body, bodySample)
|
||||
bodySample = bodySample[:n]
|
||||
// Restore body for downstream use.
|
||||
r.Body = io.NopCloser(io.MultiReader(
|
||||
bytes.NewReader(bodySample),
|
||||
r.Body,
|
||||
))
|
||||
}
|
||||
|
||||
inspReq := InspectionRequest{
|
||||
Method: r.Method,
|
||||
URL: r.URL.String(),
|
||||
Host: r.Host,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
ContentType: r.Header.Get("Content-Type"),
|
||||
ContentLength: r.ContentLength,
|
||||
Headers: map[string][]string(r.Header),
|
||||
BodySample: bodySample,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(inspReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal inspection request: %w", err)
|
||||
}
|
||||
|
||||
timeout := time.Duration(c.TimeoutMs) * time.Millisecond
|
||||
client := &http.Client{Timeout: timeout}
|
||||
|
||||
resp, err := client.Post(c.NodeURL+"/inspect", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspection node unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var inspResp InspectionResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&inspResp); err != nil {
|
||||
return nil, fmt.Errorf("decode inspection response: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("WAF: %s %s → %s (score=%d reason=%s)",
|
||||
r.Method, r.URL.Path, inspResp.Verdict, inspResp.Score, inspResp.Reason)
|
||||
|
||||
return &inspResp, nil
|
||||
}
|
||||
20
web/app.js
20
web/app.js
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
function loadConfig() {
|
||||
$.get('./api/config', function (data) {
|
||||
$('#waf-enabled').prop('checked', data.enabled);
|
||||
$('#fw-enabled').prop('checked', data.enabled);
|
||||
$('#enabled-label').text(data.enabled ? 'Firewall Enabled' : 'Firewall Disabled');
|
||||
$('#node-url').val(data.node_url);
|
||||
$('#return-port').val(data.return_port);
|
||||
$('#waf-url').val(data.waf_url);
|
||||
$('#backend-url').val(data.backend_url);
|
||||
$('#health-interval').val(data.health_interval);
|
||||
$('#timeout-ms').val(data.timeout_ms);
|
||||
});
|
||||
}
|
||||
|
|
@ -14,7 +15,7 @@ function loadStats() {
|
|||
$.get('./api/stats', function (data) {
|
||||
$('#circuit-state').text(data.circuit).attr('class', 'stat-value ' +
|
||||
(data.circuit === 'open' ? 'warn' : data.circuit === 'closed' ? 'ok' : 'warn'));
|
||||
$('#waf-status').text(data.enabled ? 'Active' : 'Disabled').attr('class', 'stat-value ' +
|
||||
$('#fw-status').text(data.enabled ? 'Active' : 'Disabled').attr('class', 'stat-value ' +
|
||||
(data.enabled ? 'ok' : 'off'));
|
||||
$('#stat-total').text(data.total || 0);
|
||||
$('#stat-allowed').text(data.allowed || 0);
|
||||
|
|
@ -29,9 +30,10 @@ function saveConfig() {
|
|||
url: './api/config/save',
|
||||
type: 'POST',
|
||||
data: {
|
||||
enabled: $('#waf-enabled').is(':checked'),
|
||||
node_url: $('#node-url').val().trim(),
|
||||
return_port: $('#return-port').val(),
|
||||
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()
|
||||
},
|
||||
success: function () {
|
||||
|
|
@ -46,7 +48,7 @@ function saveConfig() {
|
|||
});
|
||||
}
|
||||
|
||||
$('#waf-enabled').on('change', function () {
|
||||
$('#fw-enabled').on('change', function () {
|
||||
$('#enabled-label').text(this.checked ? 'Firewall Enabled' : 'Firewall Disabled');
|
||||
});
|
||||
|
||||
|
|
@ -55,5 +57,5 @@ $('#save-btn').on('click', saveConfig);
|
|||
$(document).ready(function () {
|
||||
loadConfig();
|
||||
loadStats();
|
||||
setInterval(loadStats, 10000); // refresh stats every 10s
|
||||
setInterval(loadStats, 5000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<body>
|
||||
<header>
|
||||
<h1>Firewall</h1>
|
||||
<p>Web Application Firewall — inspect traffic against a remote inspection engine.</p>
|
||||
<p>Inline WAF proxy with health checks and fail-open bypass.</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
|
@ -19,24 +19,31 @@
|
|||
|
||||
<div class="field">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="waf-enabled">
|
||||
<input type="checkbox" id="fw-enabled">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span id="enabled-label">Firewall Disabled</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="node-url">Inspection Endpoint URL</label>
|
||||
<input type="text" id="node-url" placeholder="http://192.168.1.50:8080">
|
||||
<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="return-port">Return Port (async callbacks)</label>
|
||||
<input type="number" id="return-port" placeholder="9090" min="1" max="65535">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="timeout-ms">Inspection Timeout (ms)</label>
|
||||
<label for="health-interval">Health Check Interval (s)</label>
|
||||
<input type="number" id="health-interval" placeholder="5" min="1" max="60">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="timeout-ms">Proxy Timeout (ms)</label>
|
||||
<input type="number" id="timeout-ms" placeholder="3000" min="100" max="30000">
|
||||
</div>
|
||||
|
||||
|
|
@ -52,7 +59,7 @@
|
|||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Status:</span>
|
||||
<span id="waf-status" class="stat-value">--</span>
|
||||
<span id="fw-status" class="stat-value">--</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Total Inspected:</span>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ section {
|
|||
box-shadow: 0 0 0 2px rgba(9,132,227,0.15);
|
||||
}
|
||||
|
||||
.field small {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #b2bec3;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
.toggle {
|
||||
position: relative;
|
||||
|
|
|
|||
Loading…
Reference in a new issue