UI and security improvements

This commit is contained in:
Matthew Grotke 2026-05-18 20:02:22 -04:00
parent 9a272ee959
commit b8c4914a52
13 changed files with 136 additions and 80 deletions

View file

@ -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;