Edit port forwards and openings from the internet on the firewall page

Rules from wan are edited in their own table: protocol, ports, zone, host,
host port, allowed sources, port forward (NAT), hairpin, log and rate
limit, with the module's checks done up front.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-10-30 10:20:50 +01:00
co-authored by Claude Sonnet 5.5
parent 9f9628eecc
commit ec2a10658f
5 changed files with 419 additions and 35 deletions
+27 -14
View File
@@ -123,6 +123,9 @@ func (s Settings) firewallRules() (simple []FirewallRule, other []string) {
if r == nil {
continue
}
if wanRuleSimple(r) {
continue
}
if !ruleSimple(r) {
desc := str(r, "from") + " → " + str(r, "to")
if d := str(r, "destination"); d != "" {
@@ -260,7 +263,7 @@ func (s Settings) applyZones(zf ZoneForm, errs FormErrors) {
// applyFirewall changes the zones, then replaces the rules the page edits with
// rows (an untouched "add" row is skipped); the other rules stay as they are.
func (s Settings) applyFirewall(zf ZoneForm, rows []FirewallRule) error {
func (s Settings) applyFirewall(zf ZoneForm, rows []FirewallRule, fwds []WANRule) error {
errs := FormErrors{}
s.applyZones(zf, errs)
zones := s.zones()
@@ -333,12 +336,13 @@ func (s Settings) applyFirewall(zf ZoneForm, rows []FirewallRule) error {
}
next = append(next, rule)
}
next = append(next, s.forwardRules(fwds, zones, errs)...)
router := obj(s, "router")
fw := obj(router, "firewall")
old, _ := fw["rules"].([]any)
for _, v := range old {
r, _ := v.(map[string]any)
if r == nil || ruleSimple(r) {
if r == nil || ruleSimple(r) || wanRuleSimple(r) {
continue
}
// Port forwards and the like are not edited here, but must keep a zone to point at.
@@ -365,18 +369,19 @@ func (s Settings) applyFirewall(zf ZoneForm, rows []FirewallRule) error {
}
type firewallData struct {
Rows []FirewallRule
Other []string
Zones []string
Members []ZoneMember
Empty []string // zones without members, which can be removed
New string
Errors FormErrors
Rows []FirewallRule
Forwards []WANRule
Other []string
Zones []string
Members []ZoneMember
Empty []string // zones without members, which can be removed
New string
Errors FormErrors
}
func (s Settings) firewallData(rows []FirewallRule, members []ZoneMember, errs FormErrors) firewallData {
func (s Settings) firewallData(rows []FirewallRule, fwds []WANRule, members []ZoneMember, errs FormErrors) firewallData {
_, other := s.firewallRules()
d := firewallData{Rows: rows, Other: other, Zones: s.zones(), Members: members, Errors: errs}
d := firewallData{Rows: rows, Forwards: fwds, Other: other, Zones: s.zones(), Members: members, Errors: errs}
if d.Members == nil {
d.Members = s.zoneMembers()
}
@@ -398,7 +403,7 @@ func (a *app) firewallPage(w http.ResponseWriter, r *http.Request, sess session)
}
rows, _ := s.firewallRules()
add := FirewallRule{From: r.URL.Query().Get("from"), To: r.URL.Query().Get("to"), Protocol: "any"}
p.Data = s.firewallData(append(rows, add), nil, nil)
p.Data = s.firewallData(append(rows, add), append(s.portForwards(), WANRule{Protocol: "tcp", NAT: true}), nil, nil)
a.render(w, "firewall", p)
}
@@ -417,19 +422,27 @@ func (a *app) firewall(w http.ResponseWriter, r *http.Request, sess session) {
zf.Members = append(zf.Members, ZoneMember{Kind: v("kind"), Name: v("name"), Device: v("device"), Zone: v("zone")})
}
n, _ := strconv.Atoi(r.PostFormValue("rows"))
nf, _ := strconv.Atoi(r.PostFormValue("forwards"))
var fwds []WANRule
for i := range min(nf, 512) {
v := func(k string) string { return r.PostFormValue(fmt.Sprintf("fwd.%d.%s", i, k)) }
fwds = append(fwds, WANRule{Protocol: v("protocol"), Ports: v("ports"), To: v("to"), Destination: v("destination"),
ToPort: v("toport"), Sources: v("sources"), NAT: v("nat") == "on", Hairpin: v("hairpin") == "on",
Log: v("log") == "on", RateLimit: v("ratelimit"), Remove: v("remove") == "on"})
}
var rows []FirewallRule
for i := range min(n, 512) {
v := func(k string) string { return r.PostFormValue(fmt.Sprintf("rule.%d.%s", i, k)) }
rows = append(rows, FirewallRule{From: v("from"), Sources: v("sources"), SourcePorts: v("sourceports"),
To: v("to"), Destination: v("destination"), Ports: v("ports"), Protocol: v("protocol"), Remove: v("remove") == "on"})
}
if err := s.applyFirewall(zf, rows); err != nil {
if err := s.applyFirewall(zf, rows, fwds); err != nil {
var fe FormErrors
errors.As(err, &fe)
// Show what was entered; the zone lists are those before this attempt.
s, _ = loadSettings(a.sys.settingsPath())
p.Error = "Please correct the marked fields."
d := s.firewallData(rows, zf.Members, fe)
d := s.firewallData(rows, fwds, zf.Members, fe)
d.New = zf.New
p.Data = d
w.WriteHeader(http.StatusUnprocessableEntity)