Files
nilsandClaude Sonnet 5.5 586274af4e Add the appliance web UI look, interface names, MCP and REST API, local zone
Web UI: dark/light themes (appliance layout, sidebar, switch checkboxes),
theme preference on the Advanced page, Changes page with the pending diff.

Interface names: router.interfaces.<name>.mac, lans.<l>.deviceName and
pppoe.vlanName; a rename is staged for the next reboot by router-apply.

Remote access: an MCP server (POST /mcp) and an OpenAPI REST API
(/api/v1, /api/openapi.json) with the same tokens, scopes, limits and audit
log; tools to read status, settings, leases, logs (get_logs), and to edit
and check the saved settings (never applied). Tokens on the API access page.

Firewall: a reserved `local` zone (the router itself) as the destination of
rules, so the web UI or SSH can be allowed from the WAN.

PLAN.md lists what is still left.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
2026-11-01 16:56:59 +01:00

128 lines
8.6 KiB
HTML

{{define "content"}}
{{with .Data}}
{{$e := .Errors}}
<p class="hint">Every LAN and tunnel belongs to a zone. Traffic inside a zone is allowed; between zones it is denied
unless a rule allows it. A rule works in one direction (replies are always allowed), so letting two zones talk
to each other takes two rules.</p>
<form method="post" action="/firewall">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<fieldset>
<legend>Zones</legend>
<input type="hidden" name="members" value="{{len .Members}}">
<table>
<tr><th>Interface</th><th>Device</th><th>Zone</th></tr>
{{range $i, $m := .Members}}
<tr>
<td>{{if eq $m.Kind "lan"}}LAN{{else}}Tunnel{{end}} <strong>{{$m.Name}}</strong>
<input type="hidden" name="member.{{$i}}.kind" value="{{$m.Kind}}">
<input type="hidden" name="member.{{$i}}.name" value="{{$m.Name}}">
<input type="hidden" name="member.{{$i}}.device" value="{{$m.Device}}"></td>
<td><code>{{$m.Device}}</code></td>
<td><select name="member.{{$i}}.zone">
{{range $.Data.Zones}}<option value="{{.}}" {{if eq . $m.Zone}}selected{{end}}>{{.}}</option>{{end}}
</select></td>
</tr>
{{with index $e (printf "member[%d]" $i)}}<tr><td colspan="3" class="field-error">{{.}}</td></tr>{{end}}
{{end}}
</table>
<p class="hint">Several interfaces can share a zone. The uplink is the zone <code>wan</code>; rules from it publish services.</p>
{{range .Empty}}
<p>Zone <strong>{{.}}</strong> has no interface. <label class="inline"><input type="checkbox" name="rmzone" value="{{.}}"> remove</label>
{{with index $e (printf "zone.%s" .)}}<span class="field-error">{{.}}</span>{{end}}</p>
{{end}}
<label>New zones <input name="newzones" value="{{.New}}" size="24" placeholder="guests, iot"></label>
{{with index $e "newzone"}}<span class="field-error">{{.}}</span>{{end}}
<p class="hint">A new zone can be picked above (and in the rules) after saving.</p>
</fieldset>
<fieldset>
<legend>Rules</legend>
<input type="hidden" name="rows" value="{{len .Rows}}">
<div class="scroll">
<table>
<tr><th>From zone</th><th>From address</th><th>From port</th><th>To zone</th><th>To address</th><th>To port</th><th>Protocol</th><th title="only connections that a port forward translated">Forwarded only</th><th>Log</th><th>Rate limit</th><th>Remove</th></tr>
{{range $i, $r := .Rows}}
<tr>
<td><select name="rule.{{$i}}.from"><option value=""></option>
{{range $.Data.FromZones}}<option value="{{.}}" {{if eq . $r.From}}selected{{end}}>{{.}}</option>{{end}}
</select></td>
<td><input name="rule.{{$i}}.sources" value="{{$r.Sources}}" size="16" spellcheck="false" placeholder="any"></td>
<td><input name="rule.{{$i}}.sourceports" value="{{$r.SourcePorts}}" size="8" placeholder="any"></td>
<td><select name="rule.{{$i}}.to"><option value=""></option>
{{range $.Data.ToZones}}<option value="{{.}}" {{if eq . $r.To}}selected{{end}}>{{.}}</option>{{end}}
</select></td>
<td><input name="rule.{{$i}}.destination" value="{{$r.Destination}}" size="16" spellcheck="false" placeholder="whole zone"></td>
<td><input name="rule.{{$i}}.ports" value="{{$r.Ports}}" size="10" placeholder="all"></td>
<td><select name="rule.{{$i}}.protocol">
{{range $p := list "any" "tcp" "udp" "icmp"}}<option value="{{$p}}" {{if eq $p $r.Protocol}}selected{{end}}>{{$p}}</option>{{end}}
</select></td>
<td><input type="checkbox" name="rule.{{$i}}.natonly" {{if $r.NATOnly}}checked{{end}}></td>
<td><input type="checkbox" name="rule.{{$i}}.log" {{if $r.Log}}checked{{end}}></td>
<td><input name="rule.{{$i}}.ratelimit" value="{{$r.RateLimit}}" size="9" placeholder="none"></td>
<td>{{if or $r.From $r.To}}<input type="checkbox" name="rule.{{$i}}.remove">{{end}}</td>
</tr>
{{range $f := list "from" "sources" "sourcePorts" "to" "destination" "ports" "protocol" "ratelimit"}}{{with index $e (printf "rule[%d].%s" $i $f)}}<tr><td colspan="11" class="field-error">{{$f}}: {{.}}</td></tr>{{end}}{{end}}
{{end}}
</table>
</div>
<p class="hint">Fill in the last row to add a rule. Addresses (several separated by commas) are IPv4 or IPv6, single or CIDR;
to address is one address or network. Ports (tcp and udp only): <code>22, 443, 5000-5100</code>.
From <code>wan</code> (the internet) a rule must name the address inside, the protocol and the ports, and only lets
traffic through; a port forward is set up below. <strong>Forwarded only</strong> limits a rule to connections that a
port forward translated. To zone <code>local</code> is the router itself (SSH, the web UI): no address needed, but protocol and ports are. A port forward on the same port wins. Rate limit caps new connections per sender, like <code>10/minute</code>.</p>
</fieldset>
<fieldset>
<legend>Address translation (NAT)</legend>
<input type="hidden" name="nats" value="{{len .NATs}}">
<div class="scroll">
<table>
<tr><th>Type</th><th>From zone</th><th>From address</th><th>Sent to</th><th>Protocol</th><th>Port</th><th>To zone</th><th>Translate to</th><th>To port</th><th>Hairpin</th><th>Also allow</th><th>Remove</th></tr>
{{range $i, $r := .NATs}}
<tr>
<td><select name="nat.{{$i}}.type">
{{range $p := list "dnat" "snat" "masquerade"}}<option value="{{$p}}" {{if eq $p $r.Type}}selected{{end}}>{{if eq $p "dnat"}}port forward{{else if eq $p "snat"}}source NAT{{else}}masquerade{{end}}</option>{{end}}
</select></td>
<td><select name="nat.{{$i}}.from"><option value="">any</option>
{{range $.Data.FromZones}}<option value="{{.}}" {{if eq . $r.From}}selected{{end}}>{{.}}</option>{{end}}
</select></td>
<td><input name="nat.{{$i}}.sources" value="{{$r.Sources}}" size="14" spellcheck="false" placeholder="any"></td>
<td><input name="nat.{{$i}}.destination" value="{{$r.Destination}}" size="14" spellcheck="false" placeholder="the router"></td>
<td><select name="nat.{{$i}}.protocol">
{{range $p := list "any" "tcp" "udp" "icmp"}}<option value="{{$p}}" {{if eq $p $r.Protocol}}selected{{end}}>{{$p}}</option>{{end}}
</select></td>
<td><input name="nat.{{$i}}.ports" value="{{$r.Ports}}" size="10" placeholder="443"></td>
<td><select name="nat.{{$i}}.to"><option value=""></option>
{{range $.Data.FromZones}}<option value="{{.}}" {{if eq . $r.To}}selected{{end}}>{{.}}</option>{{end}}
</select></td>
<td><input name="nat.{{$i}}.toaddress" value="{{$r.ToAddress}}" size="14" spellcheck="false" placeholder="192.168.10.5"></td>
<td><input name="nat.{{$i}}.toport" value="{{$r.ToPort}}" size="6" placeholder="same"></td>
<td><input type="checkbox" name="nat.{{$i}}.hairpin" {{if $r.Hairpin}}checked{{end}}></td>
<td>{{if or $r.ToAddress $r.Ports}}{{if $r.Allow}}<input type="checkbox" name="nat.{{$i}}.allow" checked>{{else}}<input type="checkbox" name="nat.{{$i}}.allow">{{end}}{{end}}</td>
<td>{{if or $r.ToAddress $r.Ports $r.To}}<input type="checkbox" name="nat.{{$i}}.remove">{{end}}</td>
</tr>
{{range $f := list "type" "from" "sources" "destination" "protocol" "ports" "to" "toaddress" "toport" "hairpin"}}{{with index $e (printf "nat[%d].%s" $i $f)}}<tr><td colspan="12" class="field-error">{{$f}}: {{.}}</td></tr>{{end}}{{end}}
{{end}}
</table>
</div>
<p class="hint"><strong>Port forward:</strong> traffic from the zone (<code>wan</code> is the internet) addressed to the router
(or to the address in <em>Sent to</em>) on these ports goes to the host in <em>Translate to</em>, in zone <em>To zone</em>;
the sender's address is kept. <em>Also allow</em> adds the filter rule that lets it through (without it the firewall still
blocks it). <em>Hairpin</em> lets your LANs use a forward from the internet through the router's public address.<br>
<strong>Masquerade</strong> and <strong>source NAT</strong> rewrite where traffic from the zone (or any zone) leaves through
<em>To zone</em>: masquerade with the outgoing interface's address, source NAT with the address in <em>Translate to</em>.
Internet-enabled LANs are already masqueraded out the uplink. Fill in the last row to add one.</p>
</fieldset>
<button>Save</button>
</form>
{{if .Other}}
<section>
<h2>Other rules</h2>
<p class="muted">Rules with options this page does not edit are kept as they are; change them in <code>settings.json</code>.</p>
<ul>{{range .Other}}<li>{{.}}</li>{{end}}</ul>
</section>
{{end}}
{{end}}
{{end}}