Development
This commit is contained in:
parent
cb0fb0bdaf
commit
82df24f294
5 changed files with 56 additions and 3 deletions
|
|
@ -40,8 +40,18 @@ VALIDATION_FLAGS = {
|
||||||
'VALIDATION_RANGE_INT': 1 << 11,
|
'VALIDATION_RANGE_INT': 1 << 11,
|
||||||
'VALIDATION_ENDPOINT': 1 << 12,
|
'VALIDATION_ENDPOINT': 1 << 12,
|
||||||
'VALIDATION_IPV4_CIDR': 1 << 13,
|
'VALIDATION_IPV4_CIDR': 1 << 13,
|
||||||
|
'VALIDATION_UNRESTRICTED': 1 << 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _restricted_vlan_subnets():
|
||||||
|
"""Return list of 'subnet/prefix' strings for all restricted VLANs."""
|
||||||
|
vlans = load_config().get('vlans', [])
|
||||||
|
result = []
|
||||||
|
for v in vlans:
|
||||||
|
if v.get('restricted_vlan') and v.get('subnet') and v.get('subnet_mask') is not None:
|
||||||
|
result.append(f"{v['subnet']}/{v['subnet_mask']}")
|
||||||
|
return result
|
||||||
|
|
||||||
# File / shell helpers ================================================
|
# File / shell helpers ================================================
|
||||||
|
|
||||||
def load_json(path):
|
def load_json(path):
|
||||||
|
|
@ -192,6 +202,7 @@ function _checkLine(s){
|
||||||
if(validation&2048){t=_acc(function(){if(s===''||s===null||s===undefined)return _par('');if(/[^0-9]/.test(s))return _err('Digits only');var n=parseInt(s,10);var mn=(arg1!==''&&arg1!=null)?parseInt(arg1,10):0;var mx=(arg2!==''&&arg2!=null)?parseInt(arg2,10):null;if(n<mn||(mx!==null&&n>mx)){if(mn!=null&&mx!==null)return _err('Must be between '+mn+' and '+mx);return mn!=null?_err('Must be >= '+mn):_err('Must be <= '+mx);}return _ok();}());if(t)return t;}
|
if(validation&2048){t=_acc(function(){if(s===''||s===null||s===undefined)return _par('');if(/[^0-9]/.test(s))return _err('Digits only');var n=parseInt(s,10);var mn=(arg1!==''&&arg1!=null)?parseInt(arg1,10):0;var mx=(arg2!==''&&arg2!=null)?parseInt(arg2,10):null;if(n<mn||(mx!==null&&n>mx)){if(mn!=null&&mx!==null)return _err('Must be between '+mn+' and '+mx);return mn!=null?_err('Must be >= '+mn):_err('Must be <= '+mx);}return _ok();}());if(t)return t;}
|
||||||
if(validation&4096){t=_acc(function(){if(!s)return _par('');if(/^[0-9.]+$/.test(s)){var rv=_ipv4(s);return rv==='ok'?_ok():(rv==='partial'||rv==='empty')?_par(''):_err('Invalid character');}if(s.indexOf(':')!==-1){var cc=(s.match(/:/g)||[]).length;if(cc>1){if(/:::/.test(s)||(s.match(/::/g)||[]).length>1)return _err('Invalid hostname or IP');if(/[^0-9a-fA-F:.]/.test(s))return _err('Invalid character');var col=s.replace(/[^:]/g,'').length;return(s.indexOf('::')!==-1||col===7)?_ok():_par('');}return _checkDomain(s.slice(0,s.lastIndexOf(':')));}return _checkDomain(s);}());if(t)return t;}
|
if(validation&4096){t=_acc(function(){if(!s)return _par('');if(/^[0-9.]+$/.test(s)){var rv=_ipv4(s);return rv==='ok'?_ok():(rv==='partial'||rv==='empty')?_par(''):_err('Invalid character');}if(s.indexOf(':')!==-1){var cc=(s.match(/:/g)||[]).length;if(cc>1){if(/:::/.test(s)||(s.match(/::/g)||[]).length>1)return _err('Invalid hostname or IP');if(/[^0-9a-fA-F:.]/.test(s))return _err('Invalid character');var col=s.replace(/[^:]/g,'').length;return(s.indexOf('::')!==-1||col===7)?_ok():_par('');}return _checkDomain(s.slice(0,s.lastIndexOf(':')));}return _checkDomain(s);}());if(t)return t;}
|
||||||
if(validation&8192){t=_acc(function(){if(!s)return _par('');var slash=s.indexOf('/');if(slash===-1){var rv=_ipv4(s);return(rv==='ok'||rv==='partial'||rv==='empty')?_par(''):(rv==='badchar'?_err('Invalid character'):rv==='badrange'?_err('Octet out of range'):_err('Invalid format'));}var rv=_ipv4(s.slice(0,slash));if(rv!=='ok')return rv==='badchar'?_err('Invalid character'):rv==='badrange'?_err('Octet out of range'):_par('');var pfx=s.slice(slash+1);if(!pfx)return _par('');if(/[^0-9]/.test(pfx))return _err('Invalid character');var n=parseInt(pfx,10);return(n>=0&&n<=32)?_ok():_err('Prefix must be 0-32');}());if(t)return t;}
|
if(validation&8192){t=_acc(function(){if(!s)return _par('');var slash=s.indexOf('/');if(slash===-1){var rv=_ipv4(s);return(rv==='ok'||rv==='partial'||rv==='empty')?_par(''):(rv==='badchar'?_err('Invalid character'):rv==='badrange'?_err('Octet out of range'):_err('Invalid format'));}var rv=_ipv4(s.slice(0,slash));if(rv!=='ok')return rv==='badchar'?_err('Invalid character'):rv==='badrange'?_err('Octet out of range'):_par('');var pfx=s.slice(slash+1);if(!pfx)return _par('');if(/[^0-9]/.test(pfx))return _err('Invalid character');var n=parseInt(pfx,10);return(n>=0&&n<=32)?_ok():_err('Prefix must be 0-32');}());if(t)return t;}
|
||||||
|
if(validation&16384){t=_acc(function(){if(!s)return _par('');var rv=_ipv4(s);if(rv!=='ok')return _par('');if(!collisions||!collisions.length)return _ok();var ip=s.split('.').map(Number);var ipN=((ip[0]<<24)|(ip[1]<<16)|(ip[2]<<8)|ip[3])>>>0;for(var i=0;i<collisions.length;i++){var sp=String(collisions[i]).split('/');if(sp.length!==2)continue;var np=sp[0].split('.').map(Number);if(np.length!==4)continue;var netN=((np[0]<<24)|(np[1]<<16)|(np[2]<<8)|np[3])>>>0;var pfx=parseInt(sp[1],10);var mB=pfx===0?0:((0xFFFFFFFF<<(32-pfx))>>>0);if((ipN&mB)===(netN&mB))return _err('IP is on a restricted VLAN');}return _ok();}());if(t)return t;}
|
||||||
return anyPartial?_par(''):_err(firstMsg||'Invalid');
|
return anyPartial?_par(''):_err(firstMsg||'Invalid');
|
||||||
}
|
}
|
||||||
var lines=value.split('\n'),hasPartial=false,seen={},hasContent=false;
|
var lines=value.split('\n'),hasPartial=false,seen={},hasContent=false;
|
||||||
|
|
@ -841,6 +852,10 @@ def build_field(item, tokens):
|
||||||
optional_attr = ' data-optional="1"' if item.get('optional') else ''
|
optional_attr = ' data-optional="1"' if item.get('optional') else ''
|
||||||
existing_ids = apply_tokens(item.get('existing_ids', ''), tokens)
|
existing_ids = apply_tokens(item.get('existing_ids', ''), tokens)
|
||||||
existing_attr = f' data-existing-ids="{e(existing_ids)}"' if existing_ids else ''
|
existing_attr = f' data-existing-ids="{e(existing_ids)}"' if existing_ids else ''
|
||||||
|
if not existing_ids and (_vmask & VALIDATION_FLAGS.get('VALIDATION_UNRESTRICTED', 0)):
|
||||||
|
_rsubnets = _restricted_vlan_subnets()
|
||||||
|
if _rsubnets:
|
||||||
|
existing_attr = f' data-existing-ids="{e(json.dumps(_rsubnets))}"'
|
||||||
if _vmask:
|
if _vmask:
|
||||||
return (
|
return (
|
||||||
f'<div class="form-group"><label class="form-label">{label}</label>'
|
f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,10 @@
|
||||||
"type": "raw_html",
|
"type": "raw_html",
|
||||||
"html": "<p class=\"form-hint\" style=\"margin-top:-1rem\">This exception only applies to traffic matching the selected protocol and destination port range.</p>"
|
"html": "<p class=\"form-hint\" style=\"margin-top:-1rem\">This exception only applies to traffic matching the selected protocol and destination port range.</p>"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "<br>"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "field",
|
"type": "field",
|
||||||
"label": "Enabled",
|
"label": "Enabled",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,10 @@ def addrule_add():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
err = validate.check_portfwd_restricted_vlan(entry['nat_ip'], cfg.get('vlans', []))
|
||||||
|
if err:
|
||||||
|
flash(err, 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
cfg.setdefault('port_forwarding', []).append(entry)
|
cfg.setdefault('port_forwarding', []).append(entry)
|
||||||
for desc in validate.disable_portfwd_on_restricted_vlans(cfg):
|
for desc in validate.disable_portfwd_on_restricted_vlans(cfg):
|
||||||
flash(f"Port forwarding rule '{desc}' was disabled because its destination is on a restricted VLAN.", 'info')
|
flash(f"Port forwarding rule '{desc}' was disabled because its destination is on a restricted VLAN.", 'info')
|
||||||
|
|
@ -115,9 +119,15 @@ def rules_toggle():
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
old_enabled = items[idx].get('enabled', True)
|
old_enabled = items[idx].get('enabled', True)
|
||||||
|
new_enabled = not old_enabled
|
||||||
|
if new_enabled:
|
||||||
|
err = validate.check_portfwd_restricted_vlan(items[idx].get('nat_ip', ''), cfg.get('vlans', []))
|
||||||
|
if err:
|
||||||
|
flash(err, 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
before = copy.deepcopy(items[idx])
|
before = copy.deepcopy(items[idx])
|
||||||
items[idx]['enabled'] = not old_enabled
|
items[idx]['enabled'] = new_enabled
|
||||||
for desc in validate.disable_portfwd_on_restricted_vlans(cfg):
|
for desc in validate.disable_portfwd_on_restricted_vlans(cfg):
|
||||||
flash(f"Port forwarding rule '{desc}' was disabled because its destination is on a restricted VLAN.", 'info')
|
flash(f"Port forwarding rule '{desc}' was disabled because its destination is on a restricted VLAN.", 'info')
|
||||||
errors = validate.validate_config(cfg)
|
errors = validate.validate_config(cfg)
|
||||||
|
|
@ -152,6 +162,11 @@ def rules_edit():
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
if request.form.get('enabled') == 'on':
|
||||||
|
err = validate.check_portfwd_restricted_vlan(entry['nat_ip'], cfg.get('vlans', []))
|
||||||
|
if err:
|
||||||
|
flash(err, 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
before = copy.deepcopy(items[idx])
|
before = copy.deepcopy(items[idx])
|
||||||
items[idx] = entry
|
items[idx] = entry
|
||||||
items[idx]['enabled'] = request.form.get('enabled') == 'on'
|
items[idx]['enabled'] = request.form.get('enabled') == 'on'
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
"label": "NAT IP",
|
"label": "NAT IP",
|
||||||
"name": "nat_ip",
|
"name": "nat_ip",
|
||||||
"input_type": "text",
|
"input_type": "text",
|
||||||
"validate": "VALIDATION_IPV4_FORMAT",
|
"validate": "VALIDATION_IPV4_FORMAT|VALIDATION_UNRESTRICTED",
|
||||||
"placeholder": "e.g. 192.168.1.50"
|
"placeholder": "e.g. 192.168.1.50"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -952,6 +952,25 @@ def validate_config(data):
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def check_portfwd_restricted_vlan(nat_ip, vlans):
|
||||||
|
"""Return an error string if nat_ip falls within a restricted VLAN's subnet, else None."""
|
||||||
|
try:
|
||||||
|
addr = ipaddress.IPv4Address(nat_ip)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for v in vlans:
|
||||||
|
if not v.get('restricted_vlan'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
net = ipaddress.IPv4Network(f"{v['subnet']}/{v['subnet_mask']}", strict=False)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if addr in net:
|
||||||
|
return (f"NAT IP '{nat_ip}' is on restricted VLAN '{v['name']}'. "
|
||||||
|
f"Port forwarding to restricted VLANs is not permitted.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def disable_portfwd_on_restricted_vlans(data):
|
def disable_portfwd_on_restricted_vlans(data):
|
||||||
"""Auto-disable enabled port forwarding rules whose nat_ip falls within a restricted VLAN's subnet.
|
"""Auto-disable enabled port forwarding rules whose nat_ip falls within a restricted VLAN's subnet.
|
||||||
Mutates data in place. Returns list of descriptions of rules that were disabled."""
|
Mutates data in place. Returns list of descriptions of rules that were disabled."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue