Add a firewall page to the web UI
Allow rules between zones (LANs and tunnels) are edited on their own page: from, to, protocol, ports, destination. Port forwards and rules with more options are listed and kept as they are. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFirewallRulesRoundTrip(t *testing.T) {
|
||||
s := mustParse(t)
|
||||
if got := s.zones(); strings.Join(got, ",") != "main,vpn" {
|
||||
t.Fatalf("zones = %v", got)
|
||||
}
|
||||
// A port forward the page does not edit must survive.
|
||||
obj(obj(s, "router"), "firewall")["rules"] = []any{
|
||||
map[string]any{"from": "wan", "to": "main", "dnat": true, "destination": "192.168.10.5", "protocol": "tcp", "ports": []any{float64(443)}},
|
||||
}
|
||||
rows, other := s.firewallRules()
|
||||
if len(rows) != 0 || len(other) != 1 || !strings.Contains(other[0], "port forward") {
|
||||
t.Fatalf("rows=%+v other=%v", rows, other)
|
||||
}
|
||||
err := s.applyFirewall([]FirewallRule{
|
||||
{From: "main", To: "vpn", Protocol: "any"},
|
||||
{From: "vpn", To: "main", Protocol: "tcp", Ports: "22, 443 5000-5100", Destination: "192.168.10.0/24"},
|
||||
{Protocol: "any"}, // the empty add row
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, other = s.firewallRules()
|
||||
if len(rows) != 2 || len(other) != 1 {
|
||||
t.Fatalf("rows=%+v other=%v", rows, other)
|
||||
}
|
||||
if rows[1].Ports != "22, 443, 5000-5100" || rows[1].Protocol != "tcp" || rows[0].Protocol != "any" {
|
||||
t.Errorf("rows = %+v", rows)
|
||||
}
|
||||
out := string(mustMarshal(t, s))
|
||||
for _, want := range []string{`"dnat": true`, `"5000-5100"`} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("%s missing:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
rows[0].Remove, rows[1].Remove = true, true
|
||||
if err := s.applyFirewall(rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rows, other = s.firewallRules(); len(rows) != 0 || len(other) != 1 {
|
||||
t.Errorf("rows=%+v other=%v", rows, other)
|
||||
}
|
||||
if err := s.applyFirewall(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := s["router"].(map[string]any)["firewall"].(map[string]any)["rules"]; !ok {
|
||||
t.Error("the port forward was removed with the allow rules")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallRulesAreChecked(t *testing.T) {
|
||||
good := FirewallRule{From: "main", To: "vpn", Protocol: "tcp", Ports: "80"}
|
||||
for name, mod := range map[string]func(*FirewallRule){
|
||||
"from": func(r *FirewallRule) { r.From = "nowhere" },
|
||||
"to": func(r *FirewallRule) { r.To = "main" },
|
||||
"protocol": func(r *FirewallRule) { r.Protocol = "gre" },
|
||||
"ports": func(r *FirewallRule) { r.Ports = "80-70" },
|
||||
"destination": func(r *FirewallRule) { r.Destination = "the nas" },
|
||||
} {
|
||||
r := good
|
||||
mod(&r)
|
||||
err := mustParse(t).applyFirewall([]FirewallRule{r})
|
||||
fe, ok := err.(FormErrors)
|
||||
if !ok || fe["rule[0]."+name] == "" {
|
||||
t.Errorf("%s: err = %v", name, err)
|
||||
}
|
||||
}
|
||||
r := good
|
||||
r.Protocol = "any"
|
||||
if err := mustParse(t).applyFirewall([]FirewallRule{r}); err == nil {
|
||||
t.Error("ports without tcp/udp accepted")
|
||||
}
|
||||
if err := mustParse(t).applyFirewall([]FirewallRule{good, good}); err == nil {
|
||||
t.Error("the same rule twice accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallPage(t *testing.T) {
|
||||
a, sys := newTestApp(t)
|
||||
h := a.routes()
|
||||
cookie := do(h, "POST", "/login", url.Values{"password": {"hunter2hunter2"}}, nil).Result().Cookies()[0]
|
||||
body := do(h, "GET", "/firewall?from=main&to=vpn", nil, cookie).Body.String()
|
||||
if !strings.Contains(body, `value="vpn"`) {
|
||||
t.Errorf("zones missing:\n%s", body)
|
||||
}
|
||||
token := regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(body)[1]
|
||||
form := url.Values{"csrf": {token}, "rows": {"1"}, "rule.0.from": {"main"}, "rule.0.to": {"nowhere"}, "rule.0.protocol": {"any"}}
|
||||
if rec := do(h, "POST", "/firewall", form, cookie); rec.Code != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("unknown zone: %d", rec.Code)
|
||||
}
|
||||
form.Set("rule.0.to", "vpn")
|
||||
if rec := do(h, "POST", "/firewall", form, cookie); rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("save: %d\n%s", rec.Code, rec.Body)
|
||||
}
|
||||
s, _ := loadSettings(sys.settingsPath())
|
||||
if rows, _ := s.firewallRules(); len(rows) != 1 || rows[0].From != "main" || rows[0].To != "vpn" {
|
||||
t.Fatalf("not saved: %v", s)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user