Files
nixos-router/pkgs/router-webui/templates/firewall.html
T
nilsandClaude Sonnet 5.5 a58600823f Add a generic NAT table: port forward, source NAT, masquerade
router.firewall.nat replaces the dnat/toPort/hairpin fields of the filter
rules. Filter rules gain natOnly, and may use wan with log and rate limit
in the web UI. The Firewall page edits the NAT table and can add the
matching allow rule. Settings schema 1 (migrations/1.jq) converts old
forwards.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
2026-10-30 15:54:29 +01:00

128 lines
8.5 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.Zones}}<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. 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}}