UI and security improvements
This commit is contained in:
parent
9a272ee959
commit
b8c4914a52
13 changed files with 136 additions and 80 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'<span class="tag">{e(str(t))}</span>' 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'<span class="tag" data-tooltip="{e(tooltip)}">'
|
||||
f'<span class="tl-full">{e(s)}</span>'
|
||||
f'<span class="tl-short">{e(short)}</span>'
|
||||
f'<span class="tl-min">{e(mini)}</span>'
|
||||
f'</span>')
|
||||
tags = ''.join(_tag(t) for t in items)
|
||||
return f'{td_open}<div class="tag-list">{tags}</div></td>'
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue