From a94863e25a65591c0be88efbc7fa86daef9ae627 Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sat, 6 Jun 2026 01:28:03 -0400 Subject: [PATCH] Development --- .../routlin-dash/app/pages/radius/action.py | 17 ++++++++-- .../app/pages/radius/content.json | 17 ++++++++++ docker/routlin-dash/app/pages/radius/view.py | 15 +++++++++ routlin/mod_radius.py | 32 ++++++++++++++++++- 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/docker/routlin-dash/app/pages/radius/action.py b/docker/routlin-dash/app/pages/radius/action.py index 019ec1c..d9e05ec 100644 --- a/docker/routlin-dash/app/pages/radius/action.py +++ b/docker/routlin-dash/app/pages/radius/action.py @@ -69,7 +69,13 @@ def auth_mode_save(): eap_protocol = request.form.get('eap_protocol', 'eap_peap') if eap_protocol not in ('eap_peap', 'eap_ttls', 'eap_md5'): eap_protocol = 'eap_peap' - tunneled_reply = 'tunneled_reply' in request.form + tunneled_reply = 'tunneled_reply' in request.form + inner_protocol = request.form.get('inner_protocol', '') + + _valid_inner = { + 'eap_peap': {'mschapv2', 'md5', 'gtc'}, + 'eap_ttls': {'md5', 'mschapv2', 'gtc'}, + } cfg = load_config() before = copy.deepcopy(cfg.get('radius', {}).get('options', {})) @@ -77,9 +83,14 @@ def auth_mode_save(): if auth_mode == 'eap_password': after['eap_protocol'] = eap_protocol after['tunneled_reply'] = tunneled_reply and eap_protocol in ('eap_peap', 'eap_ttls') + if eap_protocol in _valid_inner and inner_protocol in _valid_inner[eap_protocol]: + after['inner_protocol'] = inner_protocol + else: + after.pop('inner_protocol', None) else: - after.pop('eap_protocol', None) - after.pop('tunneled_reply', None) + after.pop('eap_protocol', None) + after.pop('tunneled_reply', None) + after.pop('inner_protocol', None) cfg.setdefault('radius', {})['options'] = after changes = diff_fields(before, after) diff --git a/docker/routlin-dash/app/pages/radius/content.json b/docker/routlin-dash/app/pages/radius/content.json index fcff3bb..3aadbc4 100644 --- a/docker/routlin-dash/app/pages/radius/content.json +++ b/docker/routlin-dash/app/pages/radius/content.json @@ -238,6 +238,23 @@ "options": "%RADIUS_EAP_PROTOCOL_OPTIONS%", "hint": "_" }, + { + "type": "raw_html", + "html": "
" + }, + { + "type": "field", + "label": "Inner Protocol", + "name": "inner_protocol", + "input_type": "select", + "value": "%RADIUS_INNER_PROTOCOL%", + "options": "%RADIUS_INNER_PROTOCOL_OPTIONS%", + "hint": "_" + }, + { + "type": "raw_html", + "html": "
" + }, { "type": "raw_html", "html": "
" diff --git a/docker/routlin-dash/app/pages/radius/view.py b/docker/routlin-dash/app/pages/radius/view.py index 649b998..66487d9 100644 --- a/docker/routlin-dash/app/pages/radius/view.py +++ b/docker/routlin-dash/app/pages/radius/view.py @@ -75,6 +75,21 @@ def collect_tokens(cfg): {'value': 'eap_ttls', 'label': 'EAP-TTLS'}, {'value': 'eap_md5', 'label': 'EAP-MD5'}, ]) + _eap_proto = fr_opts.get('eap_protocol', 'eap_peap') + _inner_opts_peap = [ + {'value': 'mschapv2', 'label': 'MSCHAPv2 (Default)'}, + {'value': 'md5', 'label': 'MD5'}, + {'value': 'gtc', 'label': 'GTC'}, + ] + _inner_opts_ttls = [ + {'value': 'md5', 'label': 'MD5 (Default)'}, + {'value': 'mschapv2', 'label': 'MSCHAPv2'}, + {'value': 'gtc', 'label': 'GTC'}, + ] + tokens['RADIUS_INNER_PROTOCOL'] = fr_opts.get('inner_protocol', '') + tokens['RADIUS_INNER_PROTOCOL_OPTIONS'] = json.dumps( + _inner_opts_ttls if _eap_proto == 'eap_ttls' else _inner_opts_peap + ) pro_suffix = '' if PRO_LICENSE else ' (PRO REQUIRED)' pro_disabled = not PRO_LICENSE tokens['RADIUS_AUTH_MODE_OPTIONS'] = json.dumps([ diff --git a/routlin/mod_radius.py b/routlin/mod_radius.py index 01011ee..383ebc2 100644 --- a/routlin/mod_radius.py +++ b/routlin/mod_radius.py @@ -247,6 +247,29 @@ def toggle_freeradius_block(content, block_name, enable): return content +def _patch_setting_in_block(content, block_name, key, value): + """Patch `key = value` inside the first occurrence of `block_name { ... }`.""" + lines = content.splitlines(keepends=True) + in_block = False + depth = 0 + for i, line in enumerate(lines): + if not in_block: + if re.match(r'\s*' + re.escape(block_name) + r'\s*\{', line): + in_block = True + depth = 1 + else: + depth += line.count('{') - line.count('}') + if depth <= 0: + break + if re.match(r'\s*' + re.escape(key) + r'\s*=', line): + lines[i] = re.sub( + r'(' + re.escape(key) + r'\s*=\s*)\S+', + rf'\g<1>{value}', line, count=1 + ) + return ''.join(lines) + return content + + def set_freeradius_eap(data): """Patch EAP config for eap_protocol and tunneled_reply settings. Returns True if the file was modified, False if unchanged or not found. @@ -265,10 +288,17 @@ def set_freeradius_eap(data): # Inner blocks (e.g. peap's tunneled default) must not be touched. content3 = re.sub(r'(?m)^(\s*default_eap_type\s*=\s*)\w+', rf'\g<1>{eap_type}', content2, count=1) content4 = toggle_freeradius_block(content3, 'md5', use_md5) + + inner_protocol = opts.get('inner_protocol', '') + _valid_inner = {'eap_peap': {'mschapv2', 'md5', 'gtc'}, 'eap_ttls': {'md5', 'mschapv2', 'gtc'}} + if eap_protocol in _valid_inner and inner_protocol in _valid_inner[eap_protocol]: + inner_block = 'peap' if eap_protocol == 'eap_peap' else 'ttls' + content4 = _patch_setting_in_block(content4, inner_block, 'default_eap_type', inner_protocol) + if content4 == content: return False RADIUS_EAP_FILE.write_text(content4) - print(f"EAP: default_eap_type={eap_type}, tunneled_reply={tr_val}") + print(f"EAP: default_eap_type={eap_type}, inner={inner_protocol or '(default)'}, tunneled_reply={tr_val}") return True