diff --git a/docker/routlin-dash/app/pages/networklayout/action.py b/docker/routlin-dash/app/pages/networklayout/action.py index 8084ad5..90a8204 100644 --- a/docker/routlin-dash/app/pages/networklayout/action.py +++ b/docker/routlin-dash/app/pages/networklayout/action.py @@ -96,6 +96,47 @@ def addvlan_add(): ident['hostname'] = clean_hostname new_identities.append(ident) + identity_ips = [ident['ip'] for ident in new_identities] + + gateway_raw = sanitize.ip(request.form.get('gateway', '')) + if gateway_raw and gateway_raw not in identity_ips: + flash(f"Gateway '{gateway_raw}' must match one of the server identity IPs.", 'error') + return redirect(f'/{_PAGE}') + inferred_gw = (min(identity_ips, key=lambda ip: int(ip.split('.')[-1])) if identity_ips else '') + new_stored_gw = gateway_raw if (gateway_raw and gateway_raw != inferred_gw) else '' + + dns_override = 'dns_server_override' in request.form + dns_ips = [] + for _line in request.form.get('dns_server', '').splitlines(): + _line = _line.strip() + if not _line: + continue + _clean = sanitize.ip(_line) + if not _clean: + flash(f"'{_line}' is not a valid DNS server IP.", 'error') + return redirect(f'/{_PAGE}') + dns_ips.append(_clean) + new_stored_dns = dns_ips if dns_override else [] + + ntp_override = 'ntp_server_override' in request.form + ntp_ips = [] + for _line in request.form.get('ntp_server', '').splitlines(): + _line = _line.strip() + if not _line: + continue + _clean = sanitize.ip(_line) + if not _clean: + flash(f"'{_line}' is not a valid NTP server IP.", 'error') + return redirect(f'/{_PAGE}') + ntp_ips.append(_clean) + new_stored_ntp = ntp_ips if ntp_override else [] + + dhcp_domain_raw = request.form.get('dhcp_domain', '').strip() + dhcp_domain = sanitize.hostname(dhcp_domain_raw) if dhcp_domain_raw else 'local' + if dhcp_domain_raw and dhcp_domain is None: + flash(f"'{dhcp_domain_raw}' is not a valid domain name.", 'error') + return redirect(f'/{_PAGE}') + cfg = load_config() vlans = cfg.setdefault('vlans', []) @@ -106,6 +147,19 @@ def addvlan_add(): flash('Only one VLAN can be the RADIUS default.', 'error') return redirect(f'/{_PAGE}') + dhcp_info = {} + if dhcp_domain and dhcp_domain != 'local': + dhcp_info['domain'] = dhcp_domain + dhcp_overrides = {} + if new_stored_gw: + dhcp_overrides['gateway'] = new_stored_gw + if new_stored_dns: + dhcp_overrides['dns_server'] = new_stored_dns + if new_stored_ntp: + dhcp_overrides['ntp_server'] = new_stored_ntp + if dhcp_overrides: + dhcp_info['explicit_overrides'] = dhcp_overrides + entry = { 'name': name, 'vlan_id': vlan_id, @@ -118,6 +172,8 @@ def addvlan_add(): 'mdns_reflection': mdns_reflection, 'server_identities': new_identities, } + if dhcp_info: + entry['dhcp_information'] = dhcp_info if is_vpn: entry['peers'] = [] else: diff --git a/docker/routlin-dash/app/pages/networklayout/content.json b/docker/routlin-dash/app/pages/networklayout/content.json index 99dabab..bbfa519 100644 --- a/docker/routlin-dash/app/pages/networklayout/content.json +++ b/docker/routlin-dash/app/pages/networklayout/content.json @@ -254,6 +254,37 @@ "type": "p", "text": "Servers provided to network clients via DHCP" }, + { + "type": "field_row", + "cols": 4, + "items": [ + { + "type": "dhcp_gateway_select", + "label": "Gateway", + "name": "gateway" + }, + { + "type": "dhcp_override_textarea", + "label": "DNS Server(s)", + "name": "dns_server", + "override_name": "dns_server_override" + }, + { + "type": "dhcp_override_textarea", + "label": "NTP Server(s)", + "name": "ntp_server", + "override_name": "ntp_server_override" + }, + { + "type": "field", + "label": "Domain", + "name": "dhcp_domain", + "input_type": "text", + "validate": "networkname", + "value": "local" + } + ] + }, { "type": "hr" }, diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index b0b0d86..c18b262 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -1276,6 +1276,34 @@ def _render_item(item, tokens, inherited_req=None): f'' ) + if t == 'dhcp_gateway_select': + label = e(item.get('label', 'Gateway')) + name = e(item.get('name', 'gateway')) + return ( + f'