Development
This commit is contained in:
parent
6c3abca58c
commit
96f6e32c8f
9 changed files with 294 additions and 166 deletions
|
|
@ -105,6 +105,13 @@ def blocklists_edit():
|
|||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
|
||||
# Blocklist name must be unique - it is the lookup key for VLAN use_blocklists references
|
||||
err = validate.check_blocklist_name_unique(items, fields['name'], exclude_idx=idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
items[idx].update({
|
||||
'name': fields['name'],
|
||||
'description': fields['description'],
|
||||
|
|
@ -134,8 +141,10 @@ def addblocklist_add():
|
|||
cfg = load_config()
|
||||
blocklists = cfg.setdefault('dns_blocking', {}).setdefault('blocklists', [])
|
||||
|
||||
if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists):
|
||||
flash('The configuration has not been saved because a blocklist with that name already exists.', 'error')
|
||||
# Blocklist name must be unique - it is the lookup key for VLAN use_blocklists references
|
||||
err = validate.check_blocklist_name_unique(blocklists, fields['name'])
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ def _parse_entry():
|
|||
protocol = sanitize.filtervalue(request.form.get('protocol', ''), validate.VALID_PROTOCOLS)
|
||||
src_raw = request.form.get('src_ip_or_subnet', '').strip()
|
||||
dst_raw = request.form.get('dst_ip_or_subnet', '').strip()
|
||||
dst_port_raw = request.form.get('dst_port', '').strip()
|
||||
dst_port_min_raw = request.form.get('dst_port_min', '').strip()
|
||||
dst_port_max_raw = request.form.get('dst_port_max', '').strip()
|
||||
|
||||
if not protocol:
|
||||
flash(f'The configuration has not been saved because the protocol is invalid. '
|
||||
|
|
@ -56,19 +57,31 @@ def _parse_entry():
|
|||
flash(f'The configuration has not been saved because "{dst_raw}" is not a valid IP address or subnet.', 'error')
|
||||
return None, True
|
||||
|
||||
dst_port = ''
|
||||
if dst_port_raw:
|
||||
dst_port = validate.port(dst_port_raw)
|
||||
if not dst_port:
|
||||
flash(f'The configuration has not been saved because "{dst_port_raw}" is not a valid port number (1-65535).', 'error')
|
||||
dst_port_min = ''
|
||||
if dst_port_min_raw:
|
||||
dst_port_min = validate.port(dst_port_min_raw)
|
||||
if not dst_port_min:
|
||||
flash(f'The configuration has not been saved because "{dst_port_min_raw}" is not a valid port number (1-65535).', 'error')
|
||||
return None, True
|
||||
|
||||
dst_port_max = ''
|
||||
if dst_port_max_raw:
|
||||
dst_port_max = validate.port(dst_port_max_raw)
|
||||
if not dst_port_max:
|
||||
flash(f'The configuration has not been saved because "{dst_port_max_raw}" is not a valid port number (1-65535).', 'error')
|
||||
return None, True
|
||||
|
||||
if dst_port_min and dst_port_max and int(dst_port_min) > int(dst_port_max):
|
||||
flash('Port range min must not be greater than max.', 'error')
|
||||
return None, True
|
||||
|
||||
return {
|
||||
'description': description,
|
||||
'protocol': protocol,
|
||||
'src_ip_or_subnet': src,
|
||||
'dst_ip_or_subnet': dst,
|
||||
'dst_port': dst_port,
|
||||
'dst_port_min': dst_port_min,
|
||||
'dst_port_max': dst_port_max,
|
||||
'enabled': True,
|
||||
}, None
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
{
|
||||
"label": "Protocol",
|
||||
"field": "protocol",
|
||||
"class": "col-mono"
|
||||
"class": "col-mono col-narrow"
|
||||
},
|
||||
{
|
||||
"label": "Source",
|
||||
|
|
@ -39,9 +39,14 @@
|
|||
"class": "col-mono"
|
||||
},
|
||||
{
|
||||
"label": "Dest Port",
|
||||
"field": "dst_port",
|
||||
"class": "col-mono"
|
||||
"label": "Port Min",
|
||||
"field": "dst_port_min",
|
||||
"class": "col-mono col-narrow"
|
||||
},
|
||||
{
|
||||
"label": "Port Max",
|
||||
"field": "dst_port_max",
|
||||
"class": "col-mono col-narrow"
|
||||
},
|
||||
{
|
||||
"label": "Status",
|
||||
|
|
@ -75,8 +80,12 @@
|
|||
"input_type": "text"
|
||||
},
|
||||
{
|
||||
"col": "dst_port",
|
||||
"input_type": "text"
|
||||
"col": "dst_port_min",
|
||||
"input_type": "number"
|
||||
},
|
||||
{
|
||||
"col": "dst_port_max",
|
||||
"input_type": "number"
|
||||
},
|
||||
{
|
||||
"col": "enabled",
|
||||
|
|
@ -112,36 +121,53 @@
|
|||
"input_type": "text",
|
||||
"placeholder": "e.g. Allow Chromecast"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Protocol",
|
||||
"name": "protocol",
|
||||
"input_type": "select",
|
||||
"options": "%PROTOCOL_OPTIONS%"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Source",
|
||||
"name": "src_ip_or_subnet",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_IPV4_CIDR",
|
||||
"placeholder": "e.g. 192.168.20.0/24"
|
||||
"validate": "VALIDATION_IPV4_FORMAT|VALIDATION_IPV4_CIDR",
|
||||
"placeholder": "e.g. 192.168.20.100 or 192.168.20.0/24",
|
||||
"hint": "You may allow either a single device IP or an entire subnet to contact dest."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Destination",
|
||||
"name": "dst_ip_or_subnet",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_IPV4_FORMAT",
|
||||
"placeholder": "e.g. 192.168.10.100"
|
||||
"validate": "VALIDATION_IPV4_FORMAT|VALIDATION_IPV4_CIDR",
|
||||
"placeholder": "e.g. 192.168.10.200 or 192.168.10.0/24",
|
||||
"hint": "You may allow either a single device IP or an entire subnet to be reached by source."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Dest Port",
|
||||
"name": "dst_port",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_PORT",
|
||||
"placeholder": "e.g. 8009"
|
||||
"type": "field_row",
|
||||
"cols": 3,
|
||||
"items": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Protocol",
|
||||
"name": "protocol",
|
||||
"input_type": "select",
|
||||
"options": "%PROTOCOL_OPTIONS%"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Port Min",
|
||||
"name": "dst_port_min",
|
||||
"input_type": "number",
|
||||
"min": 1,
|
||||
"max": 65535,
|
||||
"hint": "This exception only applies to traffic over this port range and protocol."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Port Max",
|
||||
"name": "dst_port_max",
|
||||
"input_type": "number",
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
|
|
@ -163,4 +189,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,25 +218,37 @@ def vlans_addedit():
|
|||
existing = vlans[edit_idx]
|
||||
is_vpn = existing.get('is_vpn', False)
|
||||
|
||||
if is_vpn and mdns_reflection:
|
||||
flash('mDNS reflection is not supported on VPN VLANs.', 'error')
|
||||
# VPN VLANs do not support mDNS reflection
|
||||
err = validate.check_mdns_vpn(is_vpn, mdns_reflection)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
# VLAN 1 maps to the physical LAN interface; its ID is fixed
|
||||
current_id = existing.get('vlan_id')
|
||||
if current_id == 1 and vlan_id != 1:
|
||||
flash('VLAN 1 is the physical interface and cannot change its ID.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if vlan_id != current_id and any(
|
||||
v.get('vlan_id') == vlan_id for i, v in enumerate(vlans) if i != edit_idx
|
||||
):
|
||||
flash(f'VLAN ID {vlan_id} is already in use.', 'error')
|
||||
# VLAN ID must be unique across all VLANs (used as 802.1Q tag and interface name)
|
||||
err = validate.check_vlan_id_unique(vlans, vlan_id, exclude_idx=edit_idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if radius_default and any(i != edit_idx and v.get('radius_default') for i, v in enumerate(vlans)):
|
||||
flash('Only one VLAN can be the RADIUS default.', 'error')
|
||||
# VLAN name must be unique - it is used as the change-history lookup key
|
||||
err = validate.check_vlan_name_unique(vlans, name, exclude_idx=edit_idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
# Only one VLAN may be the RADIUS default (used for dynamic VLAN assignment)
|
||||
if radius_default:
|
||||
err = validate.check_radius_default_unique(vlans, exclude_idx=edit_idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(existing)
|
||||
existing.update({
|
||||
'name': name,
|
||||
|
|
@ -269,18 +281,31 @@ def vlans_addedit():
|
|||
else:
|
||||
is_vpn = 'is_vpn' in request.form
|
||||
|
||||
if is_vpn and mdns_reflection:
|
||||
flash('mDNS reflection is not supported on VPN VLANs.', 'error')
|
||||
# VPN VLANs do not support mDNS reflection
|
||||
err = validate.check_mdns_vpn(is_vpn, mdns_reflection)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if any(v.get('vlan_id') == vlan_id for v in vlans):
|
||||
flash(f'VLAN ID {vlan_id} is already in use.', 'error')
|
||||
# VLAN ID must be unique across all VLANs (used as 802.1Q tag and interface name)
|
||||
err = validate.check_vlan_id_unique(vlans, vlan_id)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if radius_default and any(v.get('radius_default') for v in vlans):
|
||||
flash('Only one VLAN can be the RADIUS default.', 'error')
|
||||
# VLAN name must be unique - it is used as the change-history lookup key
|
||||
err = validate.check_vlan_name_unique(vlans, name)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
# Only one VLAN may be the RADIUS default (used for dynamic VLAN assignment)
|
||||
if radius_default:
|
||||
err = validate.check_radius_default_unique(vlans)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
'name': name,
|
||||
'vlan_id': vlan_id,
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@
|
|||
"name": "name",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_DASH_NAME",
|
||||
"existing_ids": "%EXISTING_VLAN_NAMES_JSON%",
|
||||
"hint": "Lowercase letters, digits, hyphens. E.g. iot"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ def physicalinterface_save():
|
|||
flash('Both WAN and LAN interfaces are required.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if wan == lan:
|
||||
flash('WAN and LAN interfaces must be different.', 'error')
|
||||
# WAN and LAN must be distinct physical interfaces
|
||||
err = validate.check_wan_lan_unique(wan, lan)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
|
|
@ -51,9 +53,11 @@ def physicalinterface_save():
|
|||
return redirect(f'/{_PAGE}')
|
||||
|
||||
available = _get_system_interfaces()
|
||||
# Interfaces must exist on this system (checked against physical-only interface list)
|
||||
for iface in (wan, lan):
|
||||
if available and iface not in available:
|
||||
flash(f"Interface '{iface}' does not exist on this system.", 'error')
|
||||
err = validate.check_interface_exists(iface, available)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue