Add IoT+local services ACL template; fix offline DNS resilience guidance
New ACL template: "IoT VLAN — isolated + access local services via FQDN" - Permits DNS to the configured resolver (so FQDNs resolve to LAN IPs) - Permits traffic to the servers VLAN subnet only (NAS, Home Assistant, etc.) - Blocks all other RFC1918 — users, cameras, management stay isolated - Permits internet - serverSubnet param auto-suggested from the vlans list if a VLAN named "Servers" exists; otherwise user enters it Template description explains the full picture: - How FQDN access works through the ACL (DNS → LAN IP → ACL permits it) - Offline resilience: ctrld must be on OPNsense (not management PC) to survive internet outages; split-horizon in DNS tab makes *.lan resolve from local dnsmasq without any internet dependency - Servers VLAN security: lock down servers VLAN inbound ACL by port so IoT can only reach specific service ports, not all server traffic Strict IoT template description tightened — now clearly says "zero LAN access" so users pick the right template for their use case. Modal: serverSubnet field added alongside existing ctrldIp/nvrIp/pbxIp params; preview correctly passes serverSubnet to the build function. https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+94
-9
@@ -1029,8 +1029,8 @@ const ACL_TEMPLATES = [
|
||||
},
|
||||
{
|
||||
id: "iot",
|
||||
label: "IoT / TV / Printer VLAN — internet only, no RFC1918",
|
||||
description: "Blocks all private IP ranges (RFC1918). Devices get internet but cannot reach any other VLAN — servers, NAS, PBX, cameras, management, or any LAN service. Use for IoT, smart TVs, printers, and any device that should never initiate connections to internal resources.",
|
||||
label: "IoT / TV / Printer VLAN — internet only, strict isolation",
|
||||
description: "Blocks ALL private IP ranges (RFC1918). Devices get internet only — cannot reach any other VLAN, server, NAS, PBX, camera, or management network. Use for devices that need zero LAN access and have no local services to reach.",
|
||||
params: ["subnet"],
|
||||
build: ({ subnet, vid }) => ({
|
||||
name: `IOT-VLAN${vid}-POLICY`,
|
||||
@@ -1051,6 +1051,72 @@ const ACL_TEMPLATES = [
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "iot-local-services",
|
||||
label: "IoT VLAN — isolated + access local services via FQDN",
|
||||
description: [
|
||||
"For IoT devices that need to reach local servers (NAS, Home Assistant, etc.)",
|
||||
"by FQDN while remaining isolated from all other VLANs.",
|
||||
"",
|
||||
"What this ACL does:",
|
||||
" • Allows DNS queries to your resolver (so FQDNs resolve to LAN IPs)",
|
||||
" • Allows traffic to your servers VLAN subnet only",
|
||||
" • Blocks all other RFC1918 (user devices, cameras, management, etc.)",
|
||||
" • Allows internet",
|
||||
"",
|
||||
"How FQDN access works:",
|
||||
" Device queries 'nas.lan' → DNS returns 192.168.20.X → ACL permits that IP",
|
||||
" The device never needs to know the IP. Works transparently.",
|
||||
"",
|
||||
"OFFLINE RESILIENCE — critical:",
|
||||
" If ctrld is running on the management PC, DNS fails when that PC is off",
|
||||
" or when internet is down (ctrld requires DoH3 connectivity to Control D).",
|
||||
" → Deploy ctrld on OPNsense (DNS tab → OPNsense mode) so it stays up",
|
||||
" independent of internet and independent of the management PC.",
|
||||
" → Enable split-horizon in DNS tab so *.lan queries go to local dnsmasq,",
|
||||
" which resolves from local-hostnames.json without any internet dependency.",
|
||||
" Result: 'nas.lan' resolves correctly even with internet completely down.",
|
||||
"",
|
||||
"SERVERS VLAN security:",
|
||||
" Granting IoT access to the servers VLAN subnet means ALL servers on that",
|
||||
" subnet are reachable from IoT. Lock down the server VLAN inbound ACL to",
|
||||
" only permit the specific ports each service needs (e.g. TCP 8123 for Home",
|
||||
" Assistant, TCP 443 for internal HTTPS). Apply the ACL on the servers VLAN,",
|
||||
" not here — that way it applies regardless of which VLAN initiates.",
|
||||
].join("\n"),
|
||||
params: ["subnet", "ctrldIp", "serverSubnet"],
|
||||
build: ({ subnet, vid, ctrldIp, serverSubnet }) => {
|
||||
const srvNet = serverSubnet || "192.168.20.0";
|
||||
const dnsDst = ctrldIp || "RESOLVER_IP";
|
||||
return {
|
||||
name: `IOT-SVC-VLAN${vid}-POLICY`,
|
||||
direction: "in",
|
||||
rules: [
|
||||
{ action:"permit", proto:"udp", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:dnsDst, dstMask:"", dstAny:false, port:"53",
|
||||
_comment: "DNS to resolver (UDP) — FQDNs resolve to LAN IPs" },
|
||||
{ action:"permit", proto:"tcp", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:dnsDst, dstMask:"", dstAny:false, port:"53",
|
||||
_comment: "DNS to resolver (TCP)" },
|
||||
{ action:"permit", proto:"ip", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:srvNet, dstMask:"0.0.0.255", dstAny:false, port:"",
|
||||
_comment: `Allow traffic to servers VLAN (${srvNet}/24)` },
|
||||
{ action:"deny", proto:"ip", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:"192.168.0.0", dstMask:"0.255.255.255", dstAny:false, port:"",
|
||||
_comment: "Block all other 192.168.x.x (users, cameras, management)" },
|
||||
{ action:"deny", proto:"ip", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:"10.0.0.0", dstMask:"0.255.255.255", dstAny:false, port:"",
|
||||
_comment: "Block 10.x.x.x" },
|
||||
{ action:"deny", proto:"ip", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:"172.16.0.0", dstMask:"0.15.255.255", dstAny:false, port:"",
|
||||
_comment: "Block 172.16-31.x.x" },
|
||||
{ action:"permit", proto:"ip", src:"", srcMask:"", srcAny:true,
|
||||
dst:"", dstMask:"", dstAny:true, port:"",
|
||||
_comment: "Permit internet" },
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "guest",
|
||||
label: "Guest VLAN — internet only, DNS must work first",
|
||||
@@ -1159,18 +1225,24 @@ const ACL_TEMPLATES = [
|
||||
];
|
||||
|
||||
function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
const [tpl, setTpl] = useState(ACL_TEMPLATES[0].id);
|
||||
const [vid, setVid] = useState(vlans[0]?.id || 1);
|
||||
const [ctrldIp, setCtrldIp] = useState("");
|
||||
const [nvrIp, setNvrIp] = useState("");
|
||||
const [pbxIp, setPbxIp] = useState("");
|
||||
const [tpl, setTpl] = useState(ACL_TEMPLATES[0].id);
|
||||
const [vid, setVid] = useState(vlans[0]?.id || 1);
|
||||
const [ctrldIp, setCtrldIp] = useState("");
|
||||
const [nvrIp, setNvrIp] = useState("");
|
||||
const [pbxIp, setPbxIp] = useState("");
|
||||
const [serverSubnet, setServerSubnet] = useState("");
|
||||
|
||||
const tmpl = ACL_TEMPLATES.find(t => t.id === tpl);
|
||||
const vlan = vlans.find(v => v.id === vid);
|
||||
const subnet = `192.168.${vid}.0`;
|
||||
|
||||
// Auto-suggest servers subnet from vlans list
|
||||
const serversVlan = vlans.find(v => v.name?.toLowerCase().includes("server") && v.id !== 99);
|
||||
const serverSubnetPlaceholder = serversVlan ? `192.168.${serversVlan.id}.0` : "e.g. 192.168.20.0";
|
||||
|
||||
const apply = () => {
|
||||
const acl = tmpl.build({ subnet, vid, ctrldIp, nvrIp, pbxIp });
|
||||
const srvSubnet = serverSubnet || serverSubnetPlaceholder;
|
||||
const acl = tmpl.build({ subnet, vid, ctrldIp, nvrIp, pbxIp, serverSubnet: srvSubnet });
|
||||
// Strip _comment keys — they are just for display here
|
||||
acl.rules = acl.rules.map(({ _comment, ...r }) => r);
|
||||
acl.applyVlan = vid;
|
||||
@@ -1238,6 +1310,19 @@ function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tmpl?.params.includes("serverSubnet") && (
|
||||
<div className="field" style={{margin:"0 0 10px"}}>
|
||||
<label>Servers VLAN subnet</label>
|
||||
<input value={serverSubnet} onChange={e=>setServerSubnet(e.target.value)}
|
||||
placeholder={serverSubnetPlaceholder}
|
||||
style={{fontFamily:"var(--mono)",maxWidth:200}}/>
|
||||
<div style={{fontSize:10,color:"var(--dm)",marginTop:3}}>
|
||||
Network address of the VLAN your servers live on (e.g. NAS, Home Assistant).
|
||||
Leave blank to use <span style={{fontFamily:"var(--mono)"}}>{serverSubnetPlaceholder}</span>.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview */}
|
||||
{tmpl?.params.includes("pbxIp") && (
|
||||
<div className="field" style={{margin:"0 0 10px"}}>
|
||||
@@ -1258,7 +1343,7 @@ function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
fontFamily:"var(--mono)",fontSize:10,color:"#7090a0",
|
||||
lineHeight:1.7,marginBottom:14,maxHeight:180,overflowY:"auto",
|
||||
}}>
|
||||
{tmpl.build({subnet,vid,ctrldIp,nvrIp,pbxIp}).rules.map((r,i)=>(
|
||||
{tmpl.build({subnet,vid,ctrldIp,nvrIp,pbxIp,serverSubnet:serverSubnet||serverSubnetPlaceholder}).rules.map((r,i)=>(
|
||||
<div key={i}>
|
||||
<span style={{color:"#566"}}>{` ${i+1} `}</span>
|
||||
<span style={{color:r.action==="permit"?"#0e7":"#f55"}}>{r.action}</span>
|
||||
|
||||
Reference in New Issue
Block a user