Development

This commit is contained in:
Matthew Grotke 2026-06-04 15:33:41 -04:00
parent 65c5b61ca7
commit c0ba3e76b7
5 changed files with 226 additions and 13 deletions

View file

@ -1,6 +1,7 @@
import copy
import gzip
import io
import ipaddress
import os
import re
from pathlib import Path
@ -39,17 +40,26 @@ def regenerate():
def options_save():
mac_format = request.form.get('mac_format', 'aabbccddeeff')
apply_to = request.form.get('apply_to', 'all')
ap_ips_raw = request.form.get('ap_ips', '')
if mac_format not in VALID_MAC_FORMATS:
flash('Invalid MAC format.', 'error')
return redirect(f'/{_PAGE}')
if apply_to not in ('all', 'wireless'):
if apply_to not in ('all', 'wireless', 'huntgroup'):
flash('Invalid apply_to value.', 'error')
return redirect(f'/{_PAGE}')
ap_ips = [line.strip() for line in ap_ips_raw.splitlines() if line.strip()]
for ip in ap_ips:
try:
ipaddress.IPv4Address(ip)
except ValueError:
flash(f'Invalid IP address: {ip}', 'error')
return redirect(f'/{_PAGE}')
cfg = load_config()
before = copy.deepcopy(cfg.get('radius', {}).get('options', {}))
after = {'mac_format': mac_format, 'apply_to': apply_to}
after = {'mac_format': mac_format, 'apply_to': apply_to, 'ap_ips': ap_ips}
cfg.setdefault('radius', {})['options'] = after
changes = diff_fields(before, after)
@ -57,6 +67,42 @@ def options_save():
return redirect(f'/{_PAGE}')
@bp.route('/action/radius/default_vlan_save', methods=['POST'])
@require_level('administrator')
def default_vlan_save():
chosen = request.form.get('default_vlan', '').strip()
cfg = load_config()
vlans = cfg.get('vlans', [])
if chosen and not any(v['name'] == chosen for v in vlans):
flash('Invalid VLAN selection.', 'error')
return redirect(f'/{_PAGE}')
old_name = next((v['name'] for v in vlans if v.get('radius_default') is True), '')
for v in vlans:
v['radius_default'] = (v['name'] == chosen) if chosen else False
changes = diff_fields({'radius_default': old_name}, {'radius_default': chosen})
flash(record_group(cfg, 'radius', 'fallback_vlan', chosen or 'none', changes, 'core apply'), 'success')
return redirect(f'/{_PAGE}')
@bp.route('/action/radius/eap_save', methods=['POST'])
@require_level('administrator')
def eap_save():
allow_weak_eap = 'allow_weak_eap' in request.form
tunneled_reply = 'tunneled_reply' in request.form
cfg = load_config()
before = copy.deepcopy(cfg.get('radius', {}).get('eap', {}))
after = {'allow_weak_eap': allow_weak_eap, 'tunneled_reply': tunneled_reply}
cfg.setdefault('radius', {})['eap'] = after
changes = diff_fields(before, after)
flash(record_group(cfg, 'radius.eap', 'setting', 'radius', changes, 'core apply'), 'success')
return redirect(f'/{_PAGE}')
@bp.route('/action/radius/logging_save', methods=['POST'])
@require_level('administrator')
def logging_save():

View file

@ -77,10 +77,21 @@
"input_type": "select",
"value": "%RADIUS_APPLY_TO%",
"options": [
{"value": "all", "label": "All clients"},
{"value": "wireless", "label": "Wireless clients only (NAS-Port-Type = Wireless-802.11)"}
{"value": "all", "label": "All clients"},
{"value": "wireless", "label": "Wireless clients only (NAS-Port-Type = Wireless-802.11)"},
{"value": "huntgroup", "label": "Wireless clients only (AP huntgroup by IP)"}
],
"hint": "Scoping to wireless only prevents the DEFAULT rule from assigning a VLAN to unknown wired switch ports."
"hint": "Scoping to wireless only prevents the DEFAULT rule from assigning a VLAN to unknown wired switch ports. Huntgroup is more precise but requires AP IPs below."
},
{
"type": "field",
"label": "Access Point IPs",
"name": "ap_ips",
"input_type": "textarea",
"value": "%RADIUS_AP_IPS%",
"rows": 4,
"hint": "One IP address per line. Used when Apply DEFAULT Rule To is set to huntgroup.",
"placeholder": "192.168.1.10\n192.168.1.11"
},
{
"type": "button_row",
@ -97,6 +108,87 @@
}
]
},
{
"type": "card",
"label": "Fallback VLAN",
"client_requirement": "client_is_administrator+",
"items": [
{
"type": "p",
"text": "Unknown or unregistered devices are assigned to this VLAN. For wired switch ports, also set the fallback network in the switch configuration."
},
{
"type": "form",
"action": "/action/radius/default_vlan_save",
"method": "post",
"items": [
{
"type": "field",
"label": "Fallback VLAN",
"name": "default_vlan",
"input_type": "select",
"value": "%RADIUS_DEFAULT_VLAN%",
"options": "%RADIUS_DEFAULT_VLAN_OPTIONS%",
"hint": "Devices not in the RADIUS user list will be placed on this VLAN."
},
{
"type": "button_row",
"items": [
{
"type": "button_primary",
"text": "Save"
}
]
}
]
}
]
},
{
"type": "card",
"label": "EAP Settings",
"client_requirement": "client_is_administrator+",
"items": [
{
"type": "p",
"text": "These settings are required for MAC-based 802.1X authentication on managed switches."
},
{
"type": "form",
"action": "/action/radius/eap_save",
"method": "post",
"items": [
{
"type": "field",
"label": "",
"name": "allow_weak_eap",
"input_type": "checkbox",
"checkbox_label": "Allow weak EAP types",
"value": "%RADIUS_ALLOW_WEAK_EAP%",
"hint": "Enables EAP-MD5. Required for switch port MAC-based 802.1X authentication."
},
{
"type": "field",
"label": "",
"name": "tunneled_reply",
"input_type": "checkbox",
"checkbox_label": "Use tunneled reply (EAP-TTLS / EAP-PEAP)",
"value": "%RADIUS_TUNNELED_REPLY%",
"hint": "Sets use_tunneled_reply = yes in EAP-TTLS and EAP-PEAP modules. Required for switch MAC authentication."
},
{
"type": "button_row",
"items": [
{
"type": "button_primary",
"text": "Save"
}
]
}
]
}
]
},
{
"type": "card",
"label": "Logging",

View file

@ -1,3 +1,4 @@
import json
import os
from config_utils import collect_layout_tokens, CONFIGS_DIR
@ -65,8 +66,24 @@ def collect_tokens(cfg):
fr_gen = fr.get('general', {})
tokens['RADIUS_MAC_FORMAT'] = fr_opts.get('mac_format', 'aabbccddeeff')
tokens['RADIUS_APPLY_TO'] = fr_opts.get('apply_to', 'all')
tokens['RADIUS_AP_IPS'] = '\n'.join(fr_opts.get('ap_ips', []))
tokens['RADIUS_LOGGING'] = 'true' if fr_gen.get('logging', False) else ''
tokens['RADIUS_LOGGING_HINT'] = 'Unchecking will clear logs.' if fr_gen.get('logging', False) else ''
tokens['RADIUS_GEN_LOG_MAX_KB'] = str(fr_gen.get('log_max_kb', 1024))
fr_eap = fr.get('eap', {})
tokens['RADIUS_ALLOW_WEAK_EAP'] = 'true' if fr_eap.get('allow_weak_eap', False) else ''
tokens['RADIUS_TUNNELED_REPLY'] = 'true' if fr_eap.get('tunneled_reply', False) else ''
vlans = cfg.get('vlans', [])
default_vlan = next((v['name'] for v in vlans if v.get('radius_default') is True), '')
vlan_options = [{'value': '', 'label': 'None (reject unknown devices)'}]
vlan_options += [
{'value': v['name'], 'label': f"{v['name']} (VLAN {v.get('vlan_id', '?')})"}
for v in vlans
]
tokens['RADIUS_DEFAULT_VLAN'] = default_vlan
tokens['RADIUS_DEFAULT_VLAN_OPTIONS'] = json.dumps(vlan_options)
tokens['RADIUS_LOG_TAIL'], tokens['RADIUS_LOG_SUMMARY'] = radius_log_tail(cfg)
return tokens