diff --git a/docker/router-dash/app/action_apply_blocklists.py b/docker/router-dash/app/action_apply_blocklists.py index ccf9ee8..a5a6c85 100644 --- a/docker/router-dash/app/action_apply_blocklists.py +++ b/docker/router-dash/app/action_apply_blocklists.py @@ -34,8 +34,8 @@ def _save_as_from_name(name): def _parse_fields(): """Parse and validate add/edit form fields. Returns (fields_dict, None) or (None, already_flashed).""" name = sanitize.name(request.form.get('name', '')) - description = sanitize.text(request.form.get('description', '')) - fmt = request.form.get('format', '').strip() + description = sanitize.description(request.form.get('description', '')) + fmt = sanitize.filtervalue(request.form.get('format', ''), validate.VALID_BLOCKLIST_FORMATS) url = sanitize.url(request.form.get('url', '')) if not name: @@ -44,8 +44,8 @@ def _parse_fields(): if not url: flash('The configuration has not been saved because a URL is required.', 'error') return None, True - if fmt not in validate.VALID_BLOCKLIST_FORMATS: - flash(f'The configuration has not been saved because "{fmt}" is not a valid format. ' + if not fmt: + flash(f'The configuration has not been saved because the format is invalid. ' f'Accepted formats: {_VALID_FORMATS_STR}.', 'error') return None, True @@ -75,7 +75,6 @@ def add_blocklist(): 'format': fields['format'], 'url': fields['url'], 'save_as': _save_as_from_name(fields['name']), - 'enabled': True, }) save_core(core) @@ -83,29 +82,6 @@ def add_blocklist(): return redirect(VIEW) -@bp.route('/action/toggle_blocklist', methods=['POST']) -@require_level('administrator') -def toggle_blocklist(): - idx = _row_index() - if idx is None: - flash('Invalid request.', 'error') - return redirect(VIEW) - - if not _hash_ok(): - return redirect(VIEW) - - core = load_core() - items = core.get('blocklists', []) - if idx < 0 or idx >= len(items): - flash('Entry not found.', 'error') - return redirect(VIEW) - - items[idx]['enabled'] = not items[idx].get('enabled', True) - save_core(core) - - flash(apply_msg(), 'success') - return redirect(VIEW) - @bp.route('/action/edit_blocklist', methods=['POST']) @require_level('administrator') @@ -128,13 +104,11 @@ def edit_blocklist(): flash('Entry not found.', 'error') return redirect(VIEW) - enabled = request.form.get('enabled') == 'on' items[idx].update({ 'name': fields['name'], 'description': fields['description'], 'format': fields['format'], 'url': fields['url'], - 'enabled': enabled, }) save_core(core) diff --git a/docker/router-dash/app/action_apply_ddns_providers.py b/docker/router-dash/app/action_apply_ddns_providers.py index c3b2396..50ab5ca 100644 --- a/docker/router-dash/app/action_apply_ddns_providers.py +++ b/docker/router-dash/app/action_apply_ddns_providers.py @@ -1,6 +1,8 @@ from flask import Blueprint, request, redirect, flash from auth import require_level import json +import sanitize +import validate bp = Blueprint('action_apply_ddns_providers', __name__) @@ -10,10 +12,9 @@ DDNS_FILE = '/configs/ddns.json' @bp.route('/action/add_ddns_provider', methods=['POST']) @require_level('administrator') def add_ddns_provider(): - provider_type = request.form.get('provider', '').strip().lower() - description = request.form.get('description', '').strip() - hostnames_raw = request.form.get('hostnames', '') - hostnames = [h.strip() for h in hostnames_raw.splitlines() if h.strip()] + provider_type = sanitize.filtervalue(request.form.get('provider', ''), validate.VALID_DDNS_PROVIDERS) + description = sanitize.description(request.form.get('description', '')) + hostnames = sanitize.domainlist(request.form.get('hostnames', '').splitlines()) if not description: flash('Description is required.', 'error') @@ -21,7 +22,7 @@ def add_ddns_provider(): if not hostnames: flash('At least one hostname is required.', 'error') return redirect('/view/view_ddns') - if provider_type not in ('noip', 'cloudflare', 'duckdns'): + if not provider_type: flash('Unknown provider type.', 'error') return redirect('/view/view_ddns') @@ -64,12 +65,16 @@ def edit_ddns_provider(): flash('Invalid row index.', 'error') return redirect('/view/view_ddns') - provider_type = request.form.get('provider', '').strip().lower() - description = request.form.get('description', '').strip() + provider_type = sanitize.filtervalue(request.form.get('provider', ''), validate.VALID_DDNS_PROVIDERS) + description = sanitize.description(request.form.get('description', '')) hostnames_raw = request.form.get('hostnames', '') enabled = request.form.get('enabled') == 'on' hostnames = [h.strip() for h in hostnames_raw.splitlines() if h.strip()] + if not provider_type: + flash('Unknown provider type.', 'error') + return redirect('/view/view_ddns') + try: with open(DDNS_FILE) as f: data = json.load(f) diff --git a/docker/router-dash/app/action_apply_dhcp_reservations.py b/docker/router-dash/app/action_apply_dhcp_reservations.py index 1cd10b8..eda1f20 100644 --- a/docker/router-dash/app/action_apply_dhcp_reservations.py +++ b/docker/router-dash/app/action_apply_dhcp_reservations.py @@ -51,7 +51,7 @@ def _parse_ip(): def add_dhcp_reservation(): vlan_name = sanitize.name(request.form.get('vlan_name', '')) description = sanitize.text(request.form.get('description', '')) - hostname = sanitize.hostname(request.form.get('hostname', '')) + hostname = sanitize.domainname(request.form.get('hostname', '')) mac = sanitize.mac(request.form.get('mac', '')) ip = _parse_ip() radius_client = 'radius_client' in request.form @@ -125,7 +125,7 @@ def edit_dhcp_reservation(): return redirect(VIEW) description = sanitize.text(request.form.get('description', '')) - hostname = sanitize.hostname(request.form.get('hostname', '')) + hostname = sanitize.domainname(request.form.get('hostname', '')) mac = sanitize.mac(request.form.get('mac', '')) ip = _parse_ip() radius_client = 'radius_client' in request.form diff --git a/docker/router-dash/app/action_apply_host_overrides.py b/docker/router-dash/app/action_apply_host_overrides.py index ca51eab..3401e25 100644 --- a/docker/router-dash/app/action_apply_host_overrides.py +++ b/docker/router-dash/app/action_apply_host_overrides.py @@ -51,7 +51,7 @@ def _hash_ok(): @require_level('administrator') def add_host_override(): description = sanitize.text(request.form.get('description', '')) - host = sanitize.hostname(request.form.get('host', '')) + host = sanitize.domainname(request.form.get('host', '')) ip = sanitize.ip(request.form.get('ip', '')) if not host or not ip: @@ -111,7 +111,7 @@ def edit_host_override(): return redirect(VIEW) description = sanitize.text(request.form.get('description', '')) - host = sanitize.hostname(request.form.get('host', '')) + host = sanitize.domainname(request.form.get('host', '')) ip = sanitize.ip(request.form.get('ip', '')) enabled = request.form.get('enabled') == 'on' diff --git a/docker/router-dash/app/action_apply_inter_vlan.py b/docker/router-dash/app/action_apply_inter_vlan.py index b3d3621..6ec4c51 100644 --- a/docker/router-dash/app/action_apply_inter_vlan.py +++ b/docker/router-dash/app/action_apply_inter_vlan.py @@ -28,13 +28,13 @@ def _hash_ok(): def _parse_entry(): """Parse and validate form fields. Returns (entry_dict, None) or (None, already_flashed).""" description = sanitize.text(request.form.get('description', '')) - protocol = request.form.get('protocol', '').strip() + 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() - if protocol not in validate.VALID_PROTOCOLS: - flash(f'The configuration has not been saved because "{protocol}" is not a valid protocol. ' + if not protocol: + flash(f'The configuration has not been saved because the protocol is invalid. ' f'Accepted values: {_VALID_PROTOS_STR}.', 'error') return None, True diff --git a/docker/router-dash/app/action_apply_mdns.py b/docker/router-dash/app/action_apply_mdns.py index 5008f5e..cb15f36 100644 --- a/docker/router-dash/app/action_apply_mdns.py +++ b/docker/router-dash/app/action_apply_mdns.py @@ -11,13 +11,14 @@ bp = Blueprint('action_apply_mdns', __name__) @require_level('administrator') def apply_mdns(): mdns_enabled = 'mdns_enabled' in request.form - mdns_reflect_vlans = [sanitize.name(v) for v in request.form.getlist('mdns_reflect_vlans') if v.strip()] if not verify_core_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect('/view/view_mdns') core = load_core() + mdns_reflect_vlans = sanitize.filterlist(request.form.getlist('mdns_reflect_vlans'), + {v.get('name') for v in core.get('vlans', [])}) core.setdefault('mdns_reflection', {}).update({ 'enabled': mdns_enabled, 'reflect_vlans': mdns_reflect_vlans, diff --git a/docker/router-dash/app/action_apply_port_forwarding.py b/docker/router-dash/app/action_apply_port_forwarding.py index 6fb2358..178c83f 100644 --- a/docker/router-dash/app/action_apply_port_forwarding.py +++ b/docker/router-dash/app/action_apply_port_forwarding.py @@ -28,13 +28,13 @@ def _hash_ok(): def _parse_entry(): """Parse and validate form fields. Returns (entry_dict, None) or (None, already_flashed).""" description = sanitize.text(request.form.get('description', '')) - protocol = request.form.get('protocol', '').strip() + protocol = sanitize.filtervalue(request.form.get('protocol', ''), validate.VALID_PROTOCOLS) dest_port_raw = request.form.get('dest_port', '').strip() nat_ip_raw = request.form.get('nat_ip', '').strip() nat_port_raw = request.form.get('nat_port', '').strip() - if protocol not in validate.VALID_PROTOCOLS: - flash(f'The configuration has not been saved because "{protocol}" is not a valid protocol. ' + if not protocol: + flash(f'The configuration has not been saved because the protocol is invalid. ' f'Accepted values: {_VALID_PROTOS_STR}.', 'error') return None, True diff --git a/docker/router-dash/app/action_apply_vlans.py b/docker/router-dash/app/action_apply_vlans.py index c6d698f..a2245f2 100644 --- a/docker/router-dash/app/action_apply_vlans.py +++ b/docker/router-dash/app/action_apply_vlans.py @@ -41,12 +41,14 @@ def _derive_vlan_id(subnet, prefix): @bp.route('/action/add_vlan', methods=['POST']) @require_level('administrator') def add_vlan(): - name = sanitize.name(request.form.get('name', '')).lower() + name = sanitize.name(request.form.get('name', '')) is_vpn = 'is_vpn' in request.form subnet = sanitize.ip(request.form.get('subnet', '')) subnet_mask = sanitize.subnet_mask(request.form.get('subnet_mask', '')) radius_default = 'radius_default' in request.form mdns_reflection = 'mdns_reflection' in request.form + use_blocklists = sanitize.filterlist(request.form.getlist('use_blocklists'), + {b.get('name') for b in load_core().get('blocklists', [])}) if not name: flash('Name is required.', 'error') @@ -81,7 +83,7 @@ def add_vlan(): 'is_vpn': is_vpn, 'subnet': subnet, 'subnet_mask': subnet_mask, - 'use_blocklists': [], + 'use_blocklists': use_blocklists, 'radius_default': radius_default, 'mdns_reflection': mdns_reflection, } @@ -104,11 +106,12 @@ def edit_vlan(): flash('Invalid request.', 'error') return redirect(VIEW) - name = sanitize.name(request.form.get('name', '')).lower() + name = sanitize.name(request.form.get('name', '')) subnet = sanitize.ip(request.form.get('subnet', '')) radius_default = 'radius_default' in request.form mdns_reflection = 'mdns_reflection' in request.form - use_blocklists = request.form.getlist('use_blocklists') + use_blocklists = sanitize.filterlist(request.form.getlist('use_blocklists'), + {b.get('name') for b in load_core().get('blocklists', [])}) # subnet_mask is only present when the column is visible (not all edit paths send it). # Validate if submitted; fall back to the stored value otherwise. diff --git a/docker/router-dash/app/action_apply_vpn.py b/docker/router-dash/app/action_apply_vpn.py index 5ff70c9..1f10576 100644 --- a/docker/router-dash/app/action_apply_vpn.py +++ b/docker/router-dash/app/action_apply_vpn.py @@ -122,8 +122,8 @@ def _conf_response(vlan, peer_name, peer_ip, private_key): @require_level('administrator') def apply_vpn(): listen_port_raw = request.form.get('vpn_listen_port', '').strip() - server_endpoint = sanitize.hostname(request.form.get('vpn_server_endpoint', '')) - domain = sanitize.hostname(request.form.get('vpn_domain', '')) + server_endpoint = sanitize.domainname(request.form.get('vpn_server_endpoint', '')) + domain = sanitize.domainname(request.form.get('vpn_domain', '')) dns_raw = request.form.get('vpn_dns_server', '').strip() mtu_raw = request.form.get('vpn_mtu', '').strip() diff --git a/docker/router-dash/app/sanitize.py b/docker/router-dash/app/sanitize.py index 53446bc..29bda90 100644 --- a/docker/router-dash/app/sanitize.py +++ b/docker/router-dash/app/sanitize.py @@ -105,14 +105,33 @@ def text(value, max_len=200): """General description: letters, digits, spaces, basic punctuation. No quotes/braces/brackets/slashes.""" return _strip(value, r'''["'{}\[\]\\/<>;`^~]''', max_len) -def name(value, max_len=64): - """Label/name: letters, digits, spaces, hyphens, underscores, dots.""" - return _strip(value, r'[^A-Za-z0-9 \-_.]', max_len) +def description(value, max_len=200): + """Human-readable description: letters, digits, hyphens, parentheses, commas, forward slashes, spaces. + Whitespace collapsed; no sequential commas or slashes.""" + s = re.sub(r'[^A-Za-z0-9\-()/,\s]', '', str(value)) + s = re.sub(r'\s+', ' ', s) + s = re.sub(r',{2,}', ',', s) + s = re.sub(r'/{2,}', '/', s) + s = re.sub(r'-{2,}', '-', s) + s = re.sub(r'\({2,}', '(', s) + s = re.sub(r'\){2,}', ')', s) + return s.strip()[:max_len] -def hostname(value, max_len=253): +def name(value, max_len=40): + """Identifier: lowercase letters, digits, hyphens only. No sequential hyphens.""" + s = re.sub(r'[\s_]+', '-', str(value).strip().lower()) + s = re.sub(r'[^a-z0-9-]', '', s)[:max_len] + s = re.sub(r'-{2,}', '-', s) + return s.strip('-') + +def domainname(value, max_len=253): """Hostname or domain: letters, digits, hyphens, dots. Lowercased.""" return _strip(value.lower(), r'[^a-z0-9\-.]', max_len) +def domainlist(lines): + """Sanitize a list of domain name strings, returning only non-empty results.""" + return [h for v in lines if (h := domainname(v))] + def ip(value, max_len=45): """IPv4 or IPv6 address. Returns '' if not a valid address.""" cleaned = _strip(value, r'[^0-9a-fA-F.:]', max_len) @@ -169,6 +188,16 @@ def timezone(value): """Timezone string: must be in VALID_TIMEZONES list. Returns '' if not found.""" return value if value in _TIMEZONE_SET else '' +def filterlist(submitted, allowed): + """Filter a list of submitted values to those present in the allowed set, after sanitizing each.""" + allowed = set(allowed) + return [n for v in submitted if (n := name(v)) in allowed] + +def filtervalue(value, allowed): + """Return the sanitized value if it exists in the allowed set, otherwise ''.""" + n = name(value) + return n if n in set(allowed) else '' + _DOTTED_TO_PREFIX = { '255.0.0.0': 8, '255.255.0.0': 16, '255.255.255.0': 24, '255.255.255.128': 25, '255.255.255.192': 26, diff --git a/docker/router-dash/app/validate.py b/docker/router-dash/app/validate.py index 0389999..d20b81e 100644 --- a/docker/router-dash/app/validate.py +++ b/docker/router-dash/app/validate.py @@ -15,9 +15,12 @@ from validation import ( banned_ip, ) +VALID_DDNS_PROVIDERS = ('noip', 'cloudflare', 'duckdns') + __all__ = [ 'VALID_PROTOCOLS', 'VALID_BLOCKLIST_FORMATS', + 'VALID_DDNS_PROVIDERS', 'ip', 'ip_or_cidr', 'port', diff --git a/docker/router-dash/app/view_page.py b/docker/router-dash/app/view_page.py index 80e690a..4489fe1 100644 --- a/docker/router-dash/app/view_page.py +++ b/docker/router-dash/app/view_page.py @@ -2,6 +2,7 @@ from flask import Blueprint, session, redirect, get_flashed_messages from markupsafe import Markup import json, re, subprocess, os, sys, html as html_mod import sanitize +import validate from datetime import datetime, timezone from config_utils import core_hash @@ -211,12 +212,14 @@ def _config_datasource(name): return rows if name == 'vlans': - core = _load_core() + bl_desc = {b['name']: b.get('description', b['name']) for b in core.get('blocklists', []) if 'name' in b} rows = [] for v in sorted(vlans, key=lambda x: x.get('vlan_id', 0)): row = {k: v.get(k) for k in ('vlan_id', 'name', 'subnet', 'subnet_mask', 'radius_default', 'mdns_reflection', 'is_vpn')} row['interface'] = _resolve_iface(v, core) - row['use_blocklists'] = json.dumps(v.get('use_blocklists', [])) + row['use_blocklists'] = json.dumps([ + {'n': bl, 'd': bl_desc.get(bl, bl)} for bl in v.get('use_blocklists', []) + ]) rows.append(row) return rows @@ -475,16 +478,16 @@ def collect_tokens(): tokens['EXISTING_VLAN_NAMES_JSON'] = json.dumps([v.get('name', '') for v in vlans]) tokens['EXISTING_VLAN_INTERFACES_JSON'] = json.dumps([_resolve_iface(v, core) for v in vlans]) tokens['STAT_BANNED_IP_COUNT'] = str(sum(1 for b in core.get('banned_ips', []) if b.get('enabled', True))) - tokens['STAT_BLOCKLIST_COUNT'] = str(sum(1 for b in core.get('blocklists', []) if b.get('enabled', True))) + tokens['STAT_BLOCKLIST_COUNT'] = str(len(core.get('blocklists', []))) ddns = _load_ddns() tokens['DDNS_TIMER_INTERVAL'] = ddns.get('general', {}).get('timer_interval', '-') enabled_p = [p for p in ddns.get('providers', []) if p.get('enabled', True)] tokens['STAT_DDNS_PROVIDER_COUNT'] = str(len(enabled_p)) + _ddns_labels = {'noip': 'No-IP', 'cloudflare': 'Cloudflare', 'duckdns': 'DuckDNS'} tokens['DDNS_PROVIDER_OPTIONS'] = json.dumps([ - {'value': 'noip', 'label': 'No-IP'}, - {'value': 'cloudflare', 'label': 'Cloudflare'}, - {'value': 'duckdns', 'label': 'DuckDNS'}, + {'value': p, 'label': _ddns_labels.get(p, p.title())} + for p in validate.VALID_DDNS_PROVIDERS ]) wg_vlan = next((v for v in vlans if v.get('is_vpn')), {}) @@ -1027,7 +1030,21 @@ def _render_table_cell(value, render_fn, col_class='', field='', row_idx=None, items = json.loads(value) if value.startswith('[') else [s.strip() for s in value.split(',')] except Exception: items = [value] - tags = ''.join(f'{e(str(t))}' for t in items if str(t).strip()) + def _tag(t): + if isinstance(t, dict): + s, tooltip = str(t.get('n', '')).strip(), str(t.get('d', t.get('n', ''))).strip() + else: + s = tooltip = str(t).strip() + if not s: + return '' + short = s.split('-')[0] + mini = s[0] + return (f'' + f'{e(s)}' + f'{e(short)}' + f'{e(mini)}' + f'') + tags = ''.join(_tag(t) for t in items) return f'{td_open}
{tags}
' if render_fn == 'interface_status': @@ -1160,6 +1177,35 @@ function deriveVlanId(subnet, prefix) { return (id >= 0 && id <= 4094) ? id : null; } +function networkBitsMessage(octets, prefix) { + var byteIdx = Math.floor((prefix - 1) / 8); + var hostBitsInActive = (prefix % 8 === 0) ? 0 : (8 - (prefix % 8)); + var activeMask = hostBitsInActive === 0 ? 0xFF : ((0xFF << hostBitsInActive) & 0xFF); + var ordinals = ['1st', '2nd', '3rd', '4th']; + var parts = []; + if (hostBitsInActive > 0 && (octets[byteIdx] & ~activeMask) !== 0) { + var step = 1 << hostBitsInActive; + var vals = []; + for (var v = 0; v < 256; v += step) vals.push(String(v)); + var valStr = vals.length <= 8 + ? vals.slice(0, -1).join(', ') + ' or ' + vals[vals.length - 1] + : 'a multiple of ' + step; + parts.push(ordinals[byteIdx] + ' quartet must be ' + valStr); + } + var badTrailing = []; + for (var i = byteIdx + 1; i < 4; i++) { + if (octets[i] !== 0) badTrailing.push(ordinals[i]); + } + if (badTrailing.length > 0) { + var nameStr = badTrailing.length === 1 + ? badTrailing[0] + : badTrailing.slice(0, -1).join(', ') + ' and ' + badTrailing[badTrailing.length - 1]; + parts.push(nameStr + ' quartet' + (badTrailing.length > 1 ? 's' : '') + ' must be 0'); + } + if (parts.length === 0) return null; + return parts.join('; ') + ' for /' + prefix; +} + function classifySubnet(s) { if (!s) return 'empty'; if (/[^0-9.]/.test(s)) return 'invalid_char'; @@ -1230,8 +1276,12 @@ function updateAddVlanForm(form) { } else if (sClass === 'range') { subnetMsg = 'Quartet out of range'; subnetState = 'error'; } else { - if (id === 0) { - subnetMsg = 'Reserved'; subnetState = 'warning'; + var octetsArr = subnet.split('.').map(Number); + var hostMsg = networkBitsMessage(octetsArr, prefix); + if (hostMsg) { + subnetMsg = hostMsg; subnetState = 'error'; + } else if (id === 0) { + subnetMsg = 'Would compute to VLAN ID 0 (reserved)'; subnetState = 'error'; } else if (id === null || EXISTING_VLAN_IDS.indexOf(id) !== -1) { subnetMsg = id === null ? '' : 'Duplicate'; subnetState = id === null ? 'warning' : 'error'; } else { @@ -1242,7 +1292,7 @@ function updateAddVlanForm(form) { // Interface duplicate/reserved sub-text if (ifacePrev) { - if (id === 0) { + if (id === 0 && !isVpn) { setFieldHint(ifacePrev, 'Reserved', 'error'); } else { var ifaceDupe = ifaceVal.length > 0 && EXISTING_VLAN_INTERFACES.indexOf(ifaceVal) !== -1; diff --git a/docker/router-dash/data/page_content.json b/docker/router-dash/data/page_content.json index 62920c8..74a9f03 100644 --- a/docker/router-dash/data/page_content.json +++ b/docker/router-dash/data/page_content.json @@ -954,11 +954,6 @@ "label": "Source URL", "field": "url", "class": "col-mono" - }, - { - "label": "Status", - "field": "enabled", - "render": "badge_enabled_disabled" } ], "toolbar": { @@ -996,10 +991,6 @@ { "col": "url", "input_type": "text" - }, - { - "col": "enabled", - "input_type": "checkbox" } ] }, @@ -1028,14 +1019,14 @@ "label": "Name", "name": "name", "input_type": "text", - "placeholder": "e.g. StevenBlack" + "placeholder": "e.g. steven-black" }, { "type": "field", "label": "Description", "name": "description", "input_type": "text", - "placeholder": "e.g. Unified ad/malware hosts" + "placeholder": "e.g. Steven Black (ads, malware, trackers)" }, { "type": "field", @@ -1088,7 +1079,7 @@ { "type": "info_bar", "variant": "info", - "text": "VLAN ID is derived automatically from the subnet and prefix using the active-octet rule: for /24 the third octet is used (192.168.10.0/24 → VLAN 10), for /16 the second octet, for /8 the first octet, for /25–/30 the fourth. For a basic flat network with no VLAN segmentation, only use VLAN 1 and delete the others." + "text": "For a basic flat network with no VLAN segmentation, only use VLAN 1 and delete the others." }, { "type": "table",