Add VoIP/SIP ACL template for Asterisk PBX, port range support
ACL backend: - AclRule gains optional port_end field; build_acl generates "range X Y" when both port and port_end are set (needed for RTP) New ACL template — "SIP Phone VLAN — Asterisk / FreePBX access": - Permits SIP signaling UDP/TCP 5060 to PBX IP - Permits SIP/TLS TCP 5061 to PBX IP - Permits RTP audio UDP range 10000-20000 to PBX IP (uses new range syntax) - Blocks management VLAN 99 - Permits internet and all other traffic - Requires entering the Asterisk server IP (restricts SIP/RTP to that exact host, not the whole VLAN subnet) Template description explains: - Why OPNsense firewall rules are also needed (inter-VLAN routing) - Exactly which OPNsense rules to add (including return RTP) - Remote access options: WebRTC via Caddy reverse proxy (recommended) and SIP/TLS with fail2ban for traditional SIP clients Template modal: - New pbxIp param field shown for VoIP template - Description box scrollable for longer template descriptions - Preview renders "range X Y" for port range rules https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
This commit is contained in:
+2
-2
File diff suppressed because one or more lines are too long
+68
-5
@@ -1099,6 +1099,51 @@ const ACL_TEMPLATES = [
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "voip",
|
||||
label: "SIP Phone VLAN — Asterisk / FreePBX access",
|
||||
description: [
|
||||
"Permits SIP phones to reach an Asterisk PBX on another VLAN.",
|
||||
"Allows SIP signaling (UDP/TCP 5060), SIP/TLS (TCP 5061), and RTP audio (UDP 10000–20000) to the PBX IP.",
|
||||
"Blocks access to the management VLAN (99). All other traffic (internet, etc.) is permitted.",
|
||||
"",
|
||||
"NOTE — inter-VLAN routing is enforced by OPNsense, not the switch.",
|
||||
"Also add these rules in OPNsense Firewall → Rules → VLAN interfaces:",
|
||||
" • Pass: src=phone VLAN dst=PBX IP proto=UDP/TCP port=5060,5061",
|
||||
" • Pass: src=phone VLAN dst=PBX IP proto=UDP port=10000-20000",
|
||||
" • Pass: src=PBX IP dst=phone VLAN proto=UDP port=10000-20000 (return RTP)",
|
||||
" • Block: src=phone VLAN dst=any other LAN VLAN (optional — defence in depth)",
|
||||
"",
|
||||
"For remote Asterisk access without VPN:",
|
||||
" WebRTC (recommended) — add a Caddy reverse proxy for pbx.yourdomain.com → PBX:8089 (WSS)",
|
||||
" SIP/TLS — NAT port 5061 to PBX, install fail2ban, use a TLS-capable SIP client",
|
||||
].join("\n"),
|
||||
params: ["subnet", "pbxIp"],
|
||||
build: ({ subnet, vid, pbxIp }) => ({
|
||||
name: `VOIP-VLAN${vid}-POLICY`,
|
||||
direction: "in",
|
||||
rules: [
|
||||
{ action:"permit", proto:"udp", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:pbxIp||"PBX_IP", dstMask:"", dstAny:false, port:"5060", portEnd:"",
|
||||
_comment: "SIP signaling to PBX (UDP)" },
|
||||
{ action:"permit", proto:"tcp", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:pbxIp||"PBX_IP", dstMask:"", dstAny:false, port:"5060", portEnd:"",
|
||||
_comment: "SIP signaling to PBX (TCP)" },
|
||||
{ action:"permit", proto:"tcp", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:pbxIp||"PBX_IP", dstMask:"", dstAny:false, port:"5061", portEnd:"",
|
||||
_comment: "SIP/TLS to PBX (TCP)" },
|
||||
{ action:"permit", proto:"udp", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:pbxIp||"PBX_IP", dstMask:"", dstAny:false, port:"10000", portEnd:"20000",
|
||||
_comment: "RTP audio to PBX (UDP 10000-20000)" },
|
||||
{ action:"deny", proto:"ip", src:subnet, srcMask:"0.0.0.255", srcAny:false,
|
||||
dst:"192.168.99.0", dstMask:"0.0.0.255", dstAny:false, port:"",
|
||||
_comment: "Block management VLAN 99" },
|
||||
{ action:"permit", proto:"ip", src:"", srcMask:"", srcAny:true,
|
||||
dst:"", dstMask:"", dstAny:true, port:"",
|
||||
_comment: "Permit internet and all other traffic" },
|
||||
],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
@@ -1106,13 +1151,14 @@ function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
const [vid, setVid] = useState(vlans[0]?.id || 1);
|
||||
const [ctrldIp, setCtrldIp] = useState("");
|
||||
const [nvrIp, setNvrIp] = useState("");
|
||||
const [pbxIp, setPbxIp] = useState("");
|
||||
|
||||
const tmpl = ACL_TEMPLATES.find(t => t.id === tpl);
|
||||
const vlan = vlans.find(v => v.id === vid);
|
||||
const subnet = `192.168.${vid}.0`;
|
||||
|
||||
const apply = () => {
|
||||
const acl = tmpl.build({ subnet, vid, ctrldIp, nvrIp });
|
||||
const acl = tmpl.build({ subnet, vid, ctrldIp, nvrIp, pbxIp });
|
||||
// Strip _comment keys — they are just for display here
|
||||
acl.rules = acl.rules.map(({ _comment, ...r }) => r);
|
||||
acl.applyVlan = vid;
|
||||
@@ -1145,7 +1191,7 @@ function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
{tmpl && (
|
||||
<div style={{fontSize:11,color:"var(--dm)",lineHeight:1.7,marginBottom:12,
|
||||
padding:"8px 12px",background:"var(--bg)",borderRadius:4,
|
||||
border:"1px solid var(--b1)"}}>
|
||||
border:"1px solid var(--b1)",whiteSpace:"pre-wrap",maxHeight:140,overflowY:"auto"}}>
|
||||
{tmpl.description}
|
||||
</div>
|
||||
)}
|
||||
@@ -1180,14 +1226,27 @@ function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview */}
|
||||
{tmpl?.params.includes("pbxIp") && (
|
||||
<div className="field" style={{margin:"0 0 10px"}}>
|
||||
<label>Asterisk / PBX IP address</label>
|
||||
<input value={pbxIp} onChange={e=>setPbxIp(e.target.value)}
|
||||
placeholder="e.g. 192.168.20.10"
|
||||
style={{fontFamily:"var(--mono)",maxWidth:200}}/>
|
||||
<div style={{fontSize:10,color:"var(--dm)",marginTop:3}}>
|
||||
IP of your Asterisk server. SIP and RTP will only be permitted to this exact IP.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview */}
|
||||
{tmpl && (
|
||||
<div style={{
|
||||
background:"#060809",borderRadius:4,padding:"10px 12px",
|
||||
fontFamily:"var(--mono)",fontSize:10,color:"#7090a0",
|
||||
lineHeight:1.7,marginBottom:14,maxHeight:160,overflowY:"auto",
|
||||
lineHeight:1.7,marginBottom:14,maxHeight:180,overflowY:"auto",
|
||||
}}>
|
||||
{tmpl.build({subnet,vid,ctrldIp,nvrIp}).rules.map((r,i)=>(
|
||||
{tmpl.build({subnet,vid,ctrldIp,nvrIp,pbxIp}).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>
|
||||
@@ -1199,7 +1258,11 @@ function AclTemplateModal({ vlans, onApply, onClose }) {
|
||||
<span style={{color:"#a0c0d0"}}>
|
||||
{r.dstAny?"any":`${r.dst||"?"} ${r.dstMask||""}`}
|
||||
</span>
|
||||
{r.port?<span style={{color:"#fa0"}}>{` eq ${r.port}`}</span>:null}
|
||||
{r.portEnd
|
||||
? <span style={{color:"#fa0"}}>{` range ${r.port} ${r.portEnd}`}</span>
|
||||
: r.port
|
||||
? <span style={{color:"#fa0"}}>{` eq ${r.port}`}</span>
|
||||
: null}
|
||||
{r._comment && <span style={{color:"#445"}}>{` # ${r._comment}`}</span>}
|
||||
</div>
|
||||
))}
|
||||
|
||||
+7
-1
@@ -550,6 +550,7 @@ class AclRule(BaseModel):
|
||||
dst_mask: Optional[str] = "0.0.0.255"
|
||||
dst_any: Optional[bool] = True
|
||||
port: Optional[str] = ""
|
||||
port_end: Optional[str] = "" # when set, generates "range port port_end"
|
||||
@field_validator("action")
|
||||
@classmethod
|
||||
def ca(cls, v): return _san(v, _RE_ACTION, "action")
|
||||
@@ -640,7 +641,12 @@ def build_acl(acl: AclCreate) -> list[str]:
|
||||
for i, r in enumerate(acl.rules):
|
||||
src = "any" if r.src_any else f"{r.src} {r.src_mask}"
|
||||
dst = "any" if r.dst_any else f"{r.dst} {r.dst_mask}"
|
||||
port_str = f" eq {r.port}" if r.port else ""
|
||||
if r.port and r.port_end:
|
||||
port_str = f" range {r.port} {r.port_end}"
|
||||
elif r.port:
|
||||
port_str = f" eq {r.port}"
|
||||
else:
|
||||
port_str = ""
|
||||
cmds.append(f" {i+1} {r.action} {r.proto} {src} {dst}{port_str}")
|
||||
vid = san_vid(acl.apply_vlan, "apply_vlan")
|
||||
cmds += [f"interface vlan {vid}",
|
||||
|
||||
Reference in New Issue
Block a user