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