Development
This commit is contained in:
parent
d0cfffac52
commit
adcfe55c7c
24 changed files with 405 additions and 359 deletions
|
|
@ -2,7 +2,7 @@ import copy
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -47,17 +47,17 @@ def add_banned_ip():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
entry = {'description': description, 'ip': ip, 'enabled': True}
|
entry = {'description': description, 'ip': ip, 'enabled': True}
|
||||||
core.setdefault('banned_ips', []).append(entry)
|
cfg.setdefault('banned_ips', []).append(entry)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='banned_ips', key=ip, operation='add',
|
path='banned_ips', key=ip, operation='add',
|
||||||
before=None, after=entry,
|
before=None, after=entry,
|
||||||
description=f'Added banned IP: {ip}',
|
description=f'Added banned IP: {ip}',
|
||||||
|
|
@ -75,23 +75,23 @@ def toggle_banned_ip():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('banned_ips', [])
|
items = cfg.get('banned_ips', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
old_enabled = items[idx].get('enabled', True)
|
old_enabled = items[idx].get('enabled', True)
|
||||||
items[idx]['enabled'] = not old_enabled
|
items[idx]['enabled'] = not old_enabled
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='banned_ips', key=items[idx]['ip'], operation='toggle',
|
path='banned_ips', key=items[idx]['ip'], operation='toggle',
|
||||||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||||
description=f'{action} banned IP: {items[idx]["ip"]}',
|
description=f'{action} banned IP: {items[idx]["ip"]}',
|
||||||
|
|
@ -116,22 +116,22 @@ def edit_banned_ip():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('banned_ips', [])
|
items = cfg.get('banned_ips', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
before = copy.deepcopy(items[idx])
|
before = copy.deepcopy(items[idx])
|
||||||
items[idx].update({'description': description, 'ip': ip, 'enabled': enabled})
|
items[idx].update({'description': description, 'ip': ip, 'enabled': enabled})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='banned_ips', key=ip, operation='edit',
|
path='banned_ips', key=ip, operation='edit',
|
||||||
before=before, after=copy.deepcopy(items[idx]),
|
before=before, after=copy.deepcopy(items[idx]),
|
||||||
description=f'Edited banned IP: {ip}',
|
description=f'Edited banned IP: {ip}',
|
||||||
|
|
@ -149,21 +149,21 @@ def delete_banned_ip():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('banned_ips', [])
|
items = cfg.get('banned_ips', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
removed = items.pop(idx)
|
removed = items.pop(idx)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='banned_ips', key=removed['ip'], operation='delete',
|
path='banned_ips', key=removed['ip'], operation='delete',
|
||||||
before=removed, after=None,
|
before=removed, after=None,
|
||||||
description=f'Deleted banned IP: {removed["ip"]}',
|
description=f'Deleted banned IP: {removed["ip"]}',
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import ipaddress
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -86,8 +86,8 @@ def add_dhcp_reservation():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
vlan = next((v for v in vlans if v.get('name') == vlan_name), None)
|
vlan = next((v for v in vlans if v.get('name') == vlan_name), None)
|
||||||
if vlan is None:
|
if vlan is None:
|
||||||
flash(f'The configuration has not been saved because VLAN "{vlan_name}" was not found.', 'error')
|
flash(f'The configuration has not been saved because VLAN "{vlan_name}" was not found.', 'error')
|
||||||
|
|
@ -107,14 +107,14 @@ def add_dhcp_reservation():
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
}
|
}
|
||||||
vlan.setdefault('reservations', []).append(entry)
|
vlan.setdefault('reservations', []).append(entry)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.reservations', key=mac, operation='add',
|
path=f'vlans.{vlan_name}.reservations', key=mac, operation='add',
|
||||||
before=None, after=entry,
|
before=None, after=entry,
|
||||||
description=f'Added DHCP reservation: {hostname or mac} ({ip})',
|
description=f'Added DHCP reservation: {hostname or mac} ({ip})',
|
||||||
|
|
@ -132,8 +132,8 @@ def toggle_dhcp_reservation():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
||||||
if vi is None:
|
if vi is None:
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
|
|
@ -142,7 +142,7 @@ def toggle_dhcp_reservation():
|
||||||
res = vlans[vi]['reservations'][ri]
|
res = vlans[vi]['reservations'][ri]
|
||||||
old_enabled = res.get('enabled', True)
|
old_enabled = res.get('enabled', True)
|
||||||
res['enabled'] = not old_enabled
|
res['enabled'] = not old_enabled
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
|
|
@ -150,8 +150,8 @@ def toggle_dhcp_reservation():
|
||||||
|
|
||||||
vlan_name = vlans[vi]['name']
|
vlan_name = vlans[vi]['name']
|
||||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.reservations', key=res['mac'], operation='toggle',
|
path=f'vlans.{vlan_name}.reservations', key=res['mac'], operation='toggle',
|
||||||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||||
description=f'{action} DHCP reservation: {res.get("hostname") or res["mac"]}',
|
description=f'{action} DHCP reservation: {res.get("hostname") or res["mac"]}',
|
||||||
|
|
@ -181,8 +181,8 @@ def edit_dhcp_reservation():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
||||||
if vi is None:
|
if vi is None:
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
|
|
@ -203,15 +203,15 @@ def edit_dhcp_reservation():
|
||||||
'radius_client': radius_client,
|
'radius_client': radius_client,
|
||||||
'enabled': 'enabled' in request.form,
|
'enabled': 'enabled' in request.form,
|
||||||
})
|
})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
vlan_name = vlans[vi]['name']
|
vlan_name = vlans[vi]['name']
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.reservations', key=mac, operation='edit',
|
path=f'vlans.{vlan_name}.reservations', key=mac, operation='edit',
|
||||||
before=before, after=copy.deepcopy(res),
|
before=before, after=copy.deepcopy(res),
|
||||||
description=f'Edited DHCP reservation: {hostname or mac} ({ip})',
|
description=f'Edited DHCP reservation: {hostname or mac} ({ip})',
|
||||||
|
|
@ -229,8 +229,8 @@ def delete_dhcp_reservation():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
||||||
if vi is None:
|
if vi is None:
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
|
|
@ -238,14 +238,14 @@ def delete_dhcp_reservation():
|
||||||
|
|
||||||
vlan_name = vlans[vi]['name']
|
vlan_name = vlans[vi]['name']
|
||||||
removed = vlans[vi]['reservations'].pop(ri)
|
removed = vlans[vi]['reservations'].pop(ri)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.reservations', key=removed['mac'], operation='delete',
|
path=f'vlans.{vlan_name}.reservations', key=removed['mac'], operation='delete',
|
||||||
before=removed, after=None,
|
before=removed, after=None,
|
||||||
description=f'Deleted DHCP reservation: {removed.get("hostname") or removed["mac"]}',
|
description=f'Deleted DHCP reservation: {removed.get("hostname") or removed["mac"]}',
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import ipaddress
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -12,9 +12,9 @@ bp = Blueprint('action_apply_host_overrides', __name__)
|
||||||
VIEW = '/view/view_host_overrides'
|
VIEW = '/view/view_host_overrides'
|
||||||
|
|
||||||
|
|
||||||
def _vlan_networks(core):
|
def _vlan_networks(cfg):
|
||||||
nets = []
|
nets = []
|
||||||
for v in core.get('vlans', []):
|
for v in cfg.get('vlans', []):
|
||||||
subnet = v.get('subnet', '')
|
subnet = v.get('subnet', '')
|
||||||
mask = v.get('subnet_mask', '')
|
mask = v.get('subnet_mask', '')
|
||||||
if subnet and mask:
|
if subnet and mask:
|
||||||
|
|
@ -25,12 +25,12 @@ def _vlan_networks(core):
|
||||||
return nets
|
return nets
|
||||||
|
|
||||||
|
|
||||||
def _ip_in_vlan(ip_str, core):
|
def _ip_in_vlan(ip_str, cfg):
|
||||||
try:
|
try:
|
||||||
addr = ipaddress.IPv4Address(ip_str)
|
addr = ipaddress.IPv4Address(ip_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
nets = _vlan_networks(core)
|
nets = _vlan_networks(cfg)
|
||||||
return not nets or any(addr in net for net in nets)
|
return not nets or any(addr in net for net in nets)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -61,21 +61,21 @@ def add_host_override():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
if not _ip_in_vlan(ip, core):
|
if not _ip_in_vlan(ip, cfg):
|
||||||
flash('IP address does not fall within any configured VLAN subnet.', 'error')
|
flash('IP address does not fall within any configured VLAN subnet.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
entry = {'description': description, 'host': host, 'ip': ip, 'enabled': True}
|
entry = {'description': description, 'host': host, 'ip': ip, 'enabled': True}
|
||||||
core.setdefault('host_overrides', []).append(entry)
|
cfg.setdefault('host_overrides', []).append(entry)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='host_overrides', key=host, operation='add',
|
path='host_overrides', key=host, operation='add',
|
||||||
before=None, after=entry,
|
before=None, after=entry,
|
||||||
description=f'Added host override: {host} → {ip}',
|
description=f'Added host override: {host} → {ip}',
|
||||||
|
|
@ -93,23 +93,23 @@ def toggle_host_override():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('host_overrides', [])
|
items = cfg.get('host_overrides', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
old_enabled = items[idx].get('enabled', True)
|
old_enabled = items[idx].get('enabled', True)
|
||||||
items[idx]['enabled'] = not old_enabled
|
items[idx]['enabled'] = not old_enabled
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='host_overrides', key=items[idx]['host'], operation='toggle',
|
path='host_overrides', key=items[idx]['host'], operation='toggle',
|
||||||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||||
description=f'{action} host override: {items[idx]["host"]}',
|
description=f'{action} host override: {items[idx]["host"]}',
|
||||||
|
|
@ -136,26 +136,26 @@ def edit_host_override():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
if not _ip_in_vlan(ip, core):
|
if not _ip_in_vlan(ip, cfg):
|
||||||
flash('IP address does not fall within any configured VLAN subnet.', 'error')
|
flash('IP address does not fall within any configured VLAN subnet.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
items = core.get('host_overrides', [])
|
items = cfg.get('host_overrides', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
before = copy.deepcopy(items[idx])
|
before = copy.deepcopy(items[idx])
|
||||||
items[idx].update({'description': description, 'host': host, 'ip': ip, 'enabled': enabled})
|
items[idx].update({'description': description, 'host': host, 'ip': ip, 'enabled': enabled})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='host_overrides', key=host, operation='edit',
|
path='host_overrides', key=host, operation='edit',
|
||||||
before=before, after=copy.deepcopy(items[idx]),
|
before=before, after=copy.deepcopy(items[idx]),
|
||||||
description=f'Edited host override: {host} → {ip}',
|
description=f'Edited host override: {host} → {ip}',
|
||||||
|
|
@ -173,21 +173,21 @@ def delete_host_override():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('host_overrides', [])
|
items = cfg.get('host_overrides', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
removed = items.pop(idx)
|
removed = items.pop(idx)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='host_overrides', key=removed['host'], operation='delete',
|
path='host_overrides', key=removed['host'], operation='delete',
|
||||||
before=removed, after=None,
|
before=removed, after=None,
|
||||||
description=f'Deleted host override: {removed["host"]}',
|
description=f'Deleted host override: {removed["host"]}',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import copy
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -86,17 +86,17 @@ def add_inter_vlan():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
core.setdefault('inter_vlan_exceptions', []).append(entry)
|
cfg.setdefault('inter_vlan_exceptions', []).append(entry)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
key = _entry_key(entry)
|
key = _entry_key(entry)
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='inter_vlan_exceptions', key=key, operation='add',
|
path='inter_vlan_exceptions', key=key, operation='add',
|
||||||
before=None, after=entry,
|
before=None, after=entry,
|
||||||
description=f'Added inter-VLAN rule: {key}',
|
description=f'Added inter-VLAN rule: {key}',
|
||||||
|
|
@ -114,15 +114,15 @@ def toggle_inter_vlan():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('inter_vlan_exceptions', [])
|
items = cfg.get('inter_vlan_exceptions', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
old_enabled = items[idx].get('enabled', True)
|
old_enabled = items[idx].get('enabled', True)
|
||||||
items[idx]['enabled'] = not old_enabled
|
items[idx]['enabled'] = not old_enabled
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
|
|
@ -130,8 +130,8 @@ def toggle_inter_vlan():
|
||||||
|
|
||||||
key = _entry_key(items[idx])
|
key = _entry_key(items[idx])
|
||||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='inter_vlan_exceptions', key=key, operation='toggle',
|
path='inter_vlan_exceptions', key=key, operation='toggle',
|
||||||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||||
description=f'{action} inter-VLAN rule: {key}',
|
description=f'{action} inter-VLAN rule: {key}',
|
||||||
|
|
@ -153,8 +153,8 @@ def edit_inter_vlan():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('inter_vlan_exceptions', [])
|
items = cfg.get('inter_vlan_exceptions', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -162,15 +162,15 @@ def edit_inter_vlan():
|
||||||
before = copy.deepcopy(items[idx])
|
before = copy.deepcopy(items[idx])
|
||||||
items[idx] = entry
|
items[idx] = entry
|
||||||
items[idx]['enabled'] = request.form.get('enabled') == 'on'
|
items[idx]['enabled'] = request.form.get('enabled') == 'on'
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
key = _entry_key(entry)
|
key = _entry_key(entry)
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='inter_vlan_exceptions', key=key, operation='edit',
|
path='inter_vlan_exceptions', key=key, operation='edit',
|
||||||
before=before, after=copy.deepcopy(items[idx]),
|
before=before, after=copy.deepcopy(items[idx]),
|
||||||
description=f'Edited inter-VLAN rule: {key}',
|
description=f'Edited inter-VLAN rule: {key}',
|
||||||
|
|
@ -188,22 +188,22 @@ def delete_inter_vlan():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('inter_vlan_exceptions', [])
|
items = cfg.get('inter_vlan_exceptions', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
removed = items.pop(idx)
|
removed = items.pop(idx)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
key = _entry_key(removed)
|
key = _entry_key(removed)
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='inter_vlan_exceptions', key=key, operation='delete',
|
path='inter_vlan_exceptions', key=key, operation='delete',
|
||||||
before=removed, after=None,
|
before=removed, after=None,
|
||||||
description=f'Deleted inter-VLAN rule: {key}',
|
description=f'Deleted inter-VLAN rule: {key}',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import copy
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -14,31 +14,31 @@ bp = Blueprint('action_apply_mdns', __name__)
|
||||||
def apply_mdns():
|
def apply_mdns():
|
||||||
mdns_enabled = 'mdns_enabled' in request.form
|
mdns_enabled = 'mdns_enabled' in request.form
|
||||||
|
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect('/view/view_mdns')
|
return redirect('/view/view_mdns')
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
mdns_reflect_vlans = sanitize.filterlist(
|
mdns_reflect_vlans = sanitize.filterlist(
|
||||||
request.form.getlist('mdns_reflect_vlans'),
|
request.form.getlist('mdns_reflect_vlans'),
|
||||||
{v.get('name') for v in core.get('vlans', [])},
|
{v.get('name') for v in cfg.get('vlans', [])},
|
||||||
)
|
)
|
||||||
|
|
||||||
before = copy.deepcopy(core.get('mdns_reflection', {}))
|
before = copy.deepcopy(cfg.get('mdns_reflection', {}))
|
||||||
core.setdefault('mdns_reflection', {}).update({
|
cfg.setdefault('mdns_reflection', {}).update({
|
||||||
'enabled': mdns_enabled,
|
'enabled': mdns_enabled,
|
||||||
'reflect_vlans': mdns_reflect_vlans,
|
'reflect_vlans': mdns_reflect_vlans,
|
||||||
})
|
})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect('/view/view_mdns')
|
return redirect('/view/view_mdns')
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='mdns_reflection', key='global', operation='edit',
|
path='mdns_reflection', key='global', operation='edit',
|
||||||
before=before or None, after=copy.deepcopy(core['mdns_reflection']),
|
before=before or None, after=copy.deepcopy(cfg['mdns_reflection']),
|
||||||
description='Updated mDNS reflection settings',
|
description='Updated mDNS reflection settings',
|
||||||
), 'success')
|
), 'success')
|
||||||
return redirect('/view/view_mdns')
|
return redirect('/view/view_mdns')
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import copy
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -82,17 +82,17 @@ def add_port_forward():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
core.setdefault('port_forwarding', []).append(entry)
|
cfg.setdefault('port_forwarding', []).append(entry)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
key = f'{entry["protocol"]}:{entry["dest_port"]}'
|
key = f'{entry["protocol"]}:{entry["dest_port"]}'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='port_forwarding', key=key, operation='add',
|
path='port_forwarding', key=key, operation='add',
|
||||||
before=None, after=entry,
|
before=None, after=entry,
|
||||||
description=f'Added port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}',
|
description=f'Added port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}',
|
||||||
|
|
@ -110,15 +110,15 @@ def toggle_port_forward():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('port_forwarding', [])
|
items = cfg.get('port_forwarding', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
old_enabled = items[idx].get('enabled', True)
|
old_enabled = items[idx].get('enabled', True)
|
||||||
items[idx]['enabled'] = not old_enabled
|
items[idx]['enabled'] = not old_enabled
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
|
|
@ -126,8 +126,8 @@ def toggle_port_forward():
|
||||||
|
|
||||||
key = f'{items[idx]["protocol"]}:{items[idx]["dest_port"]}'
|
key = f'{items[idx]["protocol"]}:{items[idx]["dest_port"]}'
|
||||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='port_forwarding', key=key, operation='toggle',
|
path='port_forwarding', key=key, operation='toggle',
|
||||||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||||
description=f'{action} port forward: {key}',
|
description=f'{action} port forward: {key}',
|
||||||
|
|
@ -149,8 +149,8 @@ def edit_port_forward():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('port_forwarding', [])
|
items = cfg.get('port_forwarding', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -158,15 +158,15 @@ def edit_port_forward():
|
||||||
before = copy.deepcopy(items[idx])
|
before = copy.deepcopy(items[idx])
|
||||||
items[idx] = entry
|
items[idx] = entry
|
||||||
items[idx]['enabled'] = request.form.get('enabled') == 'on'
|
items[idx]['enabled'] = request.form.get('enabled') == 'on'
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
key = f'{entry["protocol"]}:{entry["dest_port"]}'
|
key = f'{entry["protocol"]}:{entry["dest_port"]}'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='port_forwarding', key=key, operation='edit',
|
path='port_forwarding', key=key, operation='edit',
|
||||||
before=before, after=copy.deepcopy(items[idx]),
|
before=before, after=copy.deepcopy(items[idx]),
|
||||||
description=f'Edited port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}',
|
description=f'Edited port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}',
|
||||||
|
|
@ -184,22 +184,22 @@ def delete_port_forward():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('port_forwarding', [])
|
items = cfg.get('port_forwarding', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
removed = items.pop(idx)
|
removed = items.pop(idx)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
key = f'{removed["protocol"]}:{removed["dest_port"]}'
|
key = f'{removed["protocol"]}:{removed["dest_port"]}'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='port_forwarding', key=key, operation='delete',
|
path='port_forwarding', key=key, operation='delete',
|
||||||
before=removed, after=None,
|
before=removed, after=None,
|
||||||
description=f'Deleted port forward: {key}',
|
description=f'Deleted port forward: {key}',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import copy
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -40,7 +40,7 @@ def add_vlan():
|
||||||
dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form
|
dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form
|
||||||
use_blocklists = sanitize.filterlist(
|
use_blocklists = sanitize.filterlist(
|
||||||
request.form.getlist('use_blocklists'),
|
request.form.getlist('use_blocklists'),
|
||||||
{b.get('name') for b in load_core().get('dns_blocking', {}).get('blocklists', [])},
|
{b.get('name') for b in load_config().get('dns_blocking', {}).get('blocklists', [])},
|
||||||
)
|
)
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
|
|
@ -61,8 +61,8 @@ def add_vlan():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlans = core.setdefault('vlans', [])
|
vlans = cfg.setdefault('vlans', [])
|
||||||
|
|
||||||
if any(validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) == vlan_id for v in vlans):
|
if any(validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) == vlan_id for v in vlans):
|
||||||
flash(f'VLAN {vlan_id} (derived from subnet) already exists.', 'error')
|
flash(f'VLAN {vlan_id} (derived from subnet) already exists.', 'error')
|
||||||
|
|
@ -86,14 +86,14 @@ def add_vlan():
|
||||||
else:
|
else:
|
||||||
entry['reservations'] = []
|
entry['reservations'] = []
|
||||||
vlans.append(entry)
|
vlans.append(entry)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='vlans', key=name, operation='add',
|
path='vlans', key=name, operation='add',
|
||||||
before=None, after={k: entry[k] for k in _VLAN_FIELDS if k in entry},
|
before=None, after={k: entry[k] for k in _VLAN_FIELDS if k in entry},
|
||||||
description=f'Added VLAN: {name} ({subnet}/{subnet_mask})',
|
description=f'Added VLAN: {name} ({subnet}/{subnet_mask})',
|
||||||
|
|
@ -116,7 +116,7 @@ def edit_vlan():
|
||||||
dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form
|
dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form
|
||||||
use_blocklists = sanitize.filterlist(
|
use_blocklists = sanitize.filterlist(
|
||||||
request.form.getlist('use_blocklists'),
|
request.form.getlist('use_blocklists'),
|
||||||
{b.get('name') for b in load_core().get('dns_blocking', {}).get('blocklists', [])},
|
{b.get('name') for b in load_config().get('dns_blocking', {}).get('blocklists', [])},
|
||||||
)
|
)
|
||||||
|
|
||||||
subnet_mask_raw = request.form.get('subnet_mask')
|
subnet_mask_raw = request.form.get('subnet_mask')
|
||||||
|
|
@ -137,8 +137,8 @@ def edit_vlan():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
if idx < 0 or idx >= len(vlans):
|
if idx < 0 or idx >= len(vlans):
|
||||||
flash('VLAN not found.', 'error')
|
flash('VLAN not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -179,14 +179,14 @@ def edit_vlan():
|
||||||
'mdns_reflection': mdns_reflection,
|
'mdns_reflection': mdns_reflection,
|
||||||
'use_blocklists': use_blocklists,
|
'use_blocklists': use_blocklists,
|
||||||
})
|
})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='vlans', key=name, operation='edit',
|
path='vlans', key=name, operation='edit',
|
||||||
before=before, after={k: existing.get(k) for k in _VLAN_FIELDS},
|
before=before, after={k: existing.get(k) for k in _VLAN_FIELDS},
|
||||||
description=f'Edited VLAN: {name}',
|
description=f'Edited VLAN: {name}',
|
||||||
|
|
@ -204,21 +204,21 @@ def delete_vlan():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
if idx < 0 or idx >= len(vlans):
|
if idx < 0 or idx >= len(vlans):
|
||||||
flash('VLAN not found.', 'error')
|
flash('VLAN not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
removed = vlans.pop(idx)
|
removed = vlans.pop(idx)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path='vlans', key=removed['name'], operation='delete',
|
path='vlans', key=removed['name'], operation='delete',
|
||||||
before={k: removed.get(k) for k in _VLAN_FIELDS},
|
before={k: removed.get(k) for k in _VLAN_FIELDS},
|
||||||
after=None,
|
after=None,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import re
|
||||||
|
|
||||||
from flask import Blueprint, make_response, redirect, flash, request
|
from flask import Blueprint, make_response, redirect, flash, request
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core_with_snapshot, verify_core_hash, CONFIGS_DIR, WEB_APP_DISPLAY_NAME
|
from config_utils import load_config, save_config_with_snapshot, verify_config_hash, CONFIGS_DIR, WEB_APP_DISPLAY_NAME
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -16,17 +16,17 @@ _MTU_MIN = 576
|
||||||
_MTU_MAX = 9000
|
_MTU_MAX = 9000
|
||||||
|
|
||||||
|
|
||||||
def _wg_vlan(core):
|
def _wg_vlan(cfg):
|
||||||
return next((v for v in core.get('vlans', []) if v.get('is_vpn')), None)
|
return next((v for v in cfg.get('vlans', []) if v.get('is_vpn')), None)
|
||||||
|
|
||||||
|
|
||||||
def _wg_vlan_by_name(core, name):
|
def _wg_vlan_by_name(cfg, name):
|
||||||
return next((v for v in core.get('vlans', []) if v.get('is_vpn') and v.get('name') == name), None)
|
return next((v for v in cfg.get('vlans', []) if v.get('is_vpn') and v.get('name') == name), None)
|
||||||
|
|
||||||
|
|
||||||
def _find_peer_by_flat_idx(core, flat_idx):
|
def _find_peer_by_flat_idx(cfg, flat_idx):
|
||||||
i = 0
|
i = 0
|
||||||
for vlan in core.get('vlans', []):
|
for vlan in cfg.get('vlans', []):
|
||||||
if not vlan.get('is_vpn'):
|
if not vlan.get('is_vpn'):
|
||||||
continue
|
continue
|
||||||
peers = vlan.get('peers', [])
|
peers = vlan.get('peers', [])
|
||||||
|
|
@ -37,8 +37,8 @@ def _find_peer_by_flat_idx(core, flat_idx):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def _wg_iface(vlan, core):
|
def _wg_iface(vlan, cfg):
|
||||||
wg_vlans = [v for v in core.get('vlans', []) if v.get('is_vpn')]
|
wg_vlans = [v for v in cfg.get('vlans', []) if v.get('is_vpn')]
|
||||||
idx = next((i for i, v in enumerate(wg_vlans) if v is vlan), 0)
|
idx = next((i for i, v in enumerate(wg_vlans) if v is vlan), 0)
|
||||||
return f'wg{idx}'
|
return f'wg{idx}'
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -115,8 +115,8 @@ def _build_client_conf(vlan, peer_name, peer_ip, private_key, server_pubkey):
|
||||||
|
|
||||||
|
|
||||||
def _conf_response(vlan, peer_name, peer_ip, private_key):
|
def _conf_response(vlan, peer_name, peer_ip, private_key):
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
iface = _wg_iface(vlan, core)
|
iface = _wg_iface(vlan, cfg)
|
||||||
server_pub = _server_pubkey(iface)
|
server_pub = _server_pubkey(iface)
|
||||||
if not server_pub:
|
if not server_pub:
|
||||||
flash('Peer saved. Run sudo python3 ~/routlin/core.py --apply to generate the server '
|
flash('Peer saved. Run sudo python3 ~/routlin/core.py --apply to generate the server '
|
||||||
|
|
@ -164,13 +164,13 @@ def apply_vpn():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vpn_vlan = _wg_vlan(core)
|
vpn_vlan = _wg_vlan(cfg)
|
||||||
if vpn_vlan is None:
|
if vpn_vlan is None:
|
||||||
flash('No WireGuard VLAN found in configuration.', 'error')
|
flash('No WireGuard VLAN found in configuration.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
for v in core.get('vlans', []):
|
for v in cfg.get('vlans', []):
|
||||||
if v.get('is_vpn') and v is not vpn_vlan and v.get('vpn_information', {}).get('listen_port') == listen_port:
|
if v.get('is_vpn') and v is not vpn_vlan and v.get('vpn_information', {}).get('listen_port') == listen_port:
|
||||||
flash(f'Listen port {listen_port} is already used by another VPN VLAN.', 'error')
|
flash(f'Listen port {listen_port} is already used by another VPN VLAN.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
@ -191,15 +191,15 @@ def apply_vpn():
|
||||||
else:
|
else:
|
||||||
overrides.pop('mtu', None)
|
overrides.pop('mtu', None)
|
||||||
|
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
vlan_name = vpn_vlan['name']
|
vlan_name = vpn_vlan['name']
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.vpn_information', key=vlan_name, operation='edit',
|
path=f'vlans.{vlan_name}.vpn_information', key=vlan_name, operation='edit',
|
||||||
before=before_info or None, after=copy.deepcopy(info),
|
before=before_info or None, after=copy.deepcopy(info),
|
||||||
description=f'Updated VPN configuration for {vlan_name}',
|
description=f'Updated VPN configuration for {vlan_name}',
|
||||||
|
|
@ -229,8 +229,8 @@ def add_vpn_peer():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vpn_vlan = _wg_vlan_by_name(core, peer_vlan_nm)
|
vpn_vlan = _wg_vlan_by_name(cfg, peer_vlan_nm)
|
||||||
if vpn_vlan is None:
|
if vpn_vlan is None:
|
||||||
flash(f'VPN VLAN "{peer_vlan_nm}" not found.', 'error')
|
flash(f'VPN VLAN "{peer_vlan_nm}" not found.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
@ -247,7 +247,7 @@ def add_vpn_peer():
|
||||||
if any(p.get('name') == peer_name for p in peers):
|
if any(p.get('name') == peer_name for p in peers):
|
||||||
flash(f'A peer named "{peer_name}" already exists.', 'error')
|
flash(f'A peer named "{peer_name}" already exists.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
for v in core.get('vlans', []):
|
for v in cfg.get('vlans', []):
|
||||||
if not v.get('is_vpn'):
|
if not v.get('is_vpn'):
|
||||||
continue
|
continue
|
||||||
if any(p.get('ip') == peer_ip for p in v.get('peers', [])):
|
if any(p.get('ip') == peer_ip for p in v.get('peers', [])):
|
||||||
|
|
@ -263,14 +263,14 @@ def add_vpn_peer():
|
||||||
'enabled': enabled,
|
'enabled': enabled,
|
||||||
}
|
}
|
||||||
peers.append(entry)
|
peers.append(entry)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
save_core_with_snapshot(
|
save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{peer_vlan_nm}.peers', key=peer_name, operation='add',
|
path=f'vlans.{peer_vlan_nm}.peers', key=peer_name, operation='add',
|
||||||
before=None, after={k: v for k, v in entry.items() if k != 'public_key'},
|
before=None, after={k: v for k, v in entry.items() if k != 'public_key'},
|
||||||
description=f'Added VPN peer: {peer_name} ({peer_ip})',
|
description=f'Added VPN peer: {peer_name} ({peer_ip})',
|
||||||
|
|
@ -297,8 +297,8 @@ def edit_vpn_peer():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx)
|
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||||
if vlan is None:
|
if vlan is None:
|
||||||
flash('Peer not found.', 'error')
|
flash('Peer not found.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
@ -310,15 +310,15 @@ def edit_vpn_peer():
|
||||||
|
|
||||||
before = copy.deepcopy({k: peers[peer_idx].get(k) for k in ('name', 'split_tunnel', 'enabled')})
|
before = copy.deepcopy({k: peers[peer_idx].get(k) for k in ('name', 'split_tunnel', 'enabled')})
|
||||||
peers[peer_idx].update({'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled})
|
peers[peer_idx].update({'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
vlan_name = vlan['name']
|
vlan_name = vlan['name']
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.peers', key=peer_name, operation='edit',
|
path=f'vlans.{vlan_name}.peers', key=peer_name, operation='edit',
|
||||||
before=before, after={'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled},
|
before=before, after={'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled},
|
||||||
description=f'Edited VPN peer: {peer_name}',
|
description=f'Edited VPN peer: {peer_name}',
|
||||||
|
|
@ -336,8 +336,8 @@ def toggle_vpn_peer():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx)
|
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||||
if vlan is None:
|
if vlan is None:
|
||||||
flash('Peer not found.', 'error')
|
flash('Peer not found.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
@ -345,7 +345,7 @@ def toggle_vpn_peer():
|
||||||
peers = vlan.get('peers', [])
|
peers = vlan.get('peers', [])
|
||||||
old_enabled = peers[peer_idx].get('enabled', True)
|
old_enabled = peers[peer_idx].get('enabled', True)
|
||||||
peers[peer_idx]['enabled'] = not old_enabled
|
peers[peer_idx]['enabled'] = not old_enabled
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
|
|
@ -354,8 +354,8 @@ def toggle_vpn_peer():
|
||||||
peer_name = peers[peer_idx]['name']
|
peer_name = peers[peer_idx]['name']
|
||||||
vlan_name = vlan['name']
|
vlan_name = vlan['name']
|
||||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.peers', key=peer_name, operation='toggle',
|
path=f'vlans.{vlan_name}.peers', key=peer_name, operation='toggle',
|
||||||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||||
description=f'{action} VPN peer: {peer_name}',
|
description=f'{action} VPN peer: {peer_name}',
|
||||||
|
|
@ -373,23 +373,23 @@ def delete_vpn_peer():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx)
|
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||||
if vlan is None:
|
if vlan is None:
|
||||||
flash('Peer not found.', 'error')
|
flash('Peer not found.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
peers = vlan.get('peers', [])
|
peers = vlan.get('peers', [])
|
||||||
removed = peers.pop(peer_idx)
|
removed = peers.pop(peer_idx)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
vlan_name = vlan['name']
|
vlan_name = vlan['name']
|
||||||
flash(save_core_with_snapshot(
|
flash(save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.peers', key=removed['name'], operation='delete',
|
path=f'vlans.{vlan_name}.peers', key=removed['name'], operation='delete',
|
||||||
before={k: removed.get(k) for k in ('name', 'ip', 'split_tunnel', 'enabled')},
|
before={k: removed.get(k) for k in ('name', 'ip', 'split_tunnel', 'enabled')},
|
||||||
after=None,
|
after=None,
|
||||||
|
|
@ -408,8 +408,8 @@ def regenerate_vpn_peer():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx)
|
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||||
if vlan is None:
|
if vlan is None:
|
||||||
flash('Peer not found.', 'error')
|
flash('Peer not found.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
@ -418,15 +418,15 @@ def regenerate_vpn_peer():
|
||||||
peer = vlan['peers'][peer_idx]
|
peer = vlan['peers'][peer_idx]
|
||||||
old_pub_key = peer.get('public_key', '')
|
old_pub_key = peer.get('public_key', '')
|
||||||
peer['public_key'] = public_key
|
peer['public_key'] = public_key
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
vlan_name = vlan['name']
|
vlan_name = vlan['name']
|
||||||
save_core_with_snapshot(
|
save_config_with_snapshot(
|
||||||
core,
|
cfg,
|
||||||
path=f'vlans.{vlan_name}.peers', key=peer['name'], operation='regenerate',
|
path=f'vlans.{vlan_name}.peers', key=peer['name'], operation='regenerate',
|
||||||
before={'public_key': old_pub_key}, after={'public_key': public_key},
|
before={'public_key': old_pub_key}, after={'public_key': public_key},
|
||||||
description=f'Regenerated keypair for VPN peer: {peer["name"]}',
|
description=f'Regenerated keypair for VPN peer: {peer["name"]}',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import copy
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from flask import Blueprint, request, redirect, flash, send_file, abort
|
from flask import Blueprint, request, redirect, flash, send_file, abort
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core, verify_core_hash, queued_msg, CONFIGS_DIR
|
from config_utils import load_config, verify_config_hash, save_config_with_snapshot, CONFIGS_DIR
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -29,6 +29,10 @@ def ddns_cardaddaccount_add():
|
||||||
flash('Unknown provider type.', 'error')
|
flash('Unknown provider type.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
|
return redirect(VIEW)
|
||||||
|
|
||||||
entry = {
|
entry = {
|
||||||
'description': description,
|
'description': description,
|
||||||
'provider': provider_type,
|
'provider': provider_type,
|
||||||
|
|
@ -41,10 +45,14 @@ def ddns_cardaddaccount_add():
|
||||||
else:
|
else:
|
||||||
entry['api_token'] = request.form.get('api_token', '').strip()
|
entry['api_token'] = request.form.get('api_token', '').strip()
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
core.setdefault('ddns', {}).setdefault('providers', []).append(entry)
|
cfg.setdefault('ddns', {}).setdefault('providers', []).append(entry)
|
||||||
save_core(core)
|
flash(save_config_with_snapshot(
|
||||||
flash(f'DDNS provider "{description}" added.', 'success')
|
cfg, path='ddns', key=description, operation='add',
|
||||||
|
before=None, after=copy.deepcopy(entry),
|
||||||
|
description=f'Added DDNS provider: {description}',
|
||||||
|
cmd='ddns update',
|
||||||
|
), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -66,12 +74,17 @@ def ddns_tableaccounts_rowedit():
|
||||||
flash('Unknown provider type.', 'error')
|
flash('Unknown provider type.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
providers = core.setdefault('ddns', {}).setdefault('providers', [])
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
cfg = load_config()
|
||||||
|
providers = cfg.setdefault('ddns', {}).setdefault('providers', [])
|
||||||
if row_index < 0 or row_index >= len(providers):
|
if row_index < 0 or row_index >= len(providers):
|
||||||
flash('Invalid provider index.', 'error')
|
flash('Invalid provider index.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
before = copy.deepcopy(providers[row_index])
|
||||||
entry = {
|
entry = {
|
||||||
'description': description,
|
'description': description,
|
||||||
'provider': provider_type,
|
'provider': provider_type,
|
||||||
|
|
@ -85,8 +98,12 @@ def ddns_tableaccounts_rowedit():
|
||||||
entry['api_token'] = request.form.get('api_token', '').strip()
|
entry['api_token'] = request.form.get('api_token', '').strip()
|
||||||
|
|
||||||
providers[row_index] = entry
|
providers[row_index] = entry
|
||||||
save_core(core)
|
flash(save_config_with_snapshot(
|
||||||
flash('DDNS provider updated.', 'success')
|
cfg, path='ddns', key=description, operation='edit',
|
||||||
|
before=before, after=copy.deepcopy(entry),
|
||||||
|
description=f'Edited DDNS provider: {description}',
|
||||||
|
cmd='ddns update',
|
||||||
|
), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -99,15 +116,25 @@ def ddns_tableaccounts_rowdelete():
|
||||||
flash('Invalid row index.', 'error')
|
flash('Invalid row index.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
providers = core.setdefault('ddns', {}).setdefault('providers', [])
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
cfg = load_config()
|
||||||
|
providers = cfg.setdefault('ddns', {}).setdefault('providers', [])
|
||||||
if row_index < 0 or row_index >= len(providers):
|
if row_index < 0 or row_index >= len(providers):
|
||||||
flash('Invalid provider index.', 'error')
|
flash('Invalid provider index.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
before = copy.deepcopy(providers[row_index])
|
||||||
|
description = before.get('description', str(row_index))
|
||||||
del providers[row_index]
|
del providers[row_index]
|
||||||
save_core(core)
|
flash(save_config_with_snapshot(
|
||||||
flash('DDNS provider deleted.', 'success')
|
cfg, path='ddns', key=description, operation='delete',
|
||||||
|
before=before, after=None,
|
||||||
|
description=f'Deleted DDNS provider: {description}',
|
||||||
|
cmd='ddns update',
|
||||||
|
), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -123,20 +150,27 @@ def ddns_cardipcheckinterval_save():
|
||||||
flash('Interval must be a whole number of minutes >= 1.', 'error')
|
flash('Interval must be a whole number of minutes >= 1.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
timer_interval = f'{mins}m'
|
timer_interval = f'{mins}m'
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
|
||||||
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
core = load_core()
|
|
||||||
core.setdefault('ddns', {}).setdefault('general', {})['timer_interval'] = timer_interval
|
cfg = load_config()
|
||||||
save_core(core)
|
before = copy.deepcopy(cfg.get('ddns', {}).get('general', {}))
|
||||||
flash(queued_msg('core apply'), 'success')
|
cfg.setdefault('ddns', {}).setdefault('general', {})['timer_interval'] = timer_interval
|
||||||
|
flash(save_config_with_snapshot(
|
||||||
|
cfg, path='ddns', key='general', operation='edit',
|
||||||
|
before=before, after=copy.deepcopy(cfg['ddns']['general']),
|
||||||
|
description='Updated DDNS check interval',
|
||||||
|
cmd='core apply',
|
||||||
|
), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/ddns_cardipcheckservices_save', methods=['POST'])
|
@bp.route('/action/ddns_cardipcheckservices_save', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def ddns_cardipcheckservices_save():
|
def ddns_cardipcheckservices_save():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
@ -147,13 +181,17 @@ def ddns_cardipcheckservices_save():
|
||||||
flash('At least one IP check service is required.', 'error')
|
flash('At least one IP check service is required.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
services = [{'type': 'http', 'url': u} for u in http_services]
|
cfg = load_config()
|
||||||
|
before = copy.deepcopy(cfg.get('ddns', {}).get('ip_check_services', []))
|
||||||
|
services = [{'type': 'http', 'url': u} for u in http_services]
|
||||||
services += [{'type': 'dig', 'url': u} for u in dig_services]
|
services += [{'type': 'dig', 'url': u} for u in dig_services]
|
||||||
|
cfg.setdefault('ddns', {})['ip_check_services'] = services
|
||||||
core = load_core()
|
flash(save_config_with_snapshot(
|
||||||
core.setdefault('ddns', {})['ip_check_services'] = services
|
cfg, path='ddns', key='ip_check_services', operation='edit',
|
||||||
save_core(core)
|
before=before, after=copy.deepcopy(services),
|
||||||
flash('IP check services saved.', 'success')
|
description='Updated DDNS IP check services',
|
||||||
|
cmd='ddns update',
|
||||||
|
), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -165,16 +203,23 @@ def ddns_cardlogging_save():
|
||||||
flash('Max Log Size must be a number >= 64.', 'error')
|
flash('Max Log Size must be a number >= 64.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
log_errors_only = 'log_errors_only' in request.form
|
log_errors_only = 'log_errors_only' in request.form
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
|
||||||
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
core = load_core()
|
|
||||||
core.setdefault('ddns', {}).setdefault('general', {}).update({
|
cfg = load_config()
|
||||||
|
before = copy.deepcopy(cfg.get('ddns', {}).get('general', {}))
|
||||||
|
cfg.setdefault('ddns', {}).setdefault('general', {}).update({
|
||||||
'log_max_kb': log_max_kb,
|
'log_max_kb': log_max_kb,
|
||||||
'log_errors_only': log_errors_only,
|
'log_errors_only': log_errors_only,
|
||||||
})
|
})
|
||||||
save_core(core)
|
flash(save_config_with_snapshot(
|
||||||
flash('DDNS log settings saved.', 'success')
|
cfg, path='ddns', key='general', operation='edit',
|
||||||
|
before=before, after=copy.deepcopy(cfg['ddns']['general']),
|
||||||
|
description='Updated DDNS logging settings',
|
||||||
|
cmd='ddns update',
|
||||||
|
), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core, verify_core_hash, queued_msg
|
from config_utils import load_config, save_config, verify_config_hash, queued_msg
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ def _row_index():
|
||||||
|
|
||||||
|
|
||||||
def _hash_ok():
|
def _hash_ok():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
@ -62,19 +62,19 @@ def dnsblocking_tableblocklists_rowdelete():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('dns_blocking', {}).get('blocklists', [])
|
items = cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
items.pop(idx)
|
items.pop(idx)
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
|
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -95,8 +95,8 @@ def dnsblocking_tableblocklists_rowedit():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
items = core.get('dns_blocking', {}).get('blocklists', [])
|
items = cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||||
if idx < 0 or idx >= len(items):
|
if idx < 0 or idx >= len(items):
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -107,12 +107,12 @@ def dnsblocking_tableblocklists_rowedit():
|
||||||
'format': fields['format'],
|
'format': fields['format'],
|
||||||
'url': fields['url'],
|
'url': fields['url'],
|
||||||
})
|
})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
|
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -128,8 +128,8 @@ def dnsblocking_cardaddblocklist_add():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
blocklists = core.setdefault('dns_blocking', {}).setdefault('blocklists', [])
|
blocklists = cfg.setdefault('dns_blocking', {}).setdefault('blocklists', [])
|
||||||
|
|
||||||
if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists):
|
if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists):
|
||||||
flash('The configuration has not been saved because a blocklist with that name already exists.', 'error')
|
flash('The configuration has not been saved because a blocklist with that name already exists.', 'error')
|
||||||
|
|
@ -142,12 +142,12 @@ def dnsblocking_cardaddblocklist_add():
|
||||||
'url': fields['url'],
|
'url': fields['url'],
|
||||||
'save_as': _save_as_from_name(fields['name']),
|
'save_as': _save_as_from_name(fields['name']),
|
||||||
})
|
})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
|
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -162,13 +162,13 @@ def dnsblocking_cardblocklistrefresh_save():
|
||||||
flash('Daily Refresh Time must be a valid 24-hour time (e.g. 02:30).', 'error')
|
flash('Daily Refresh Time must be a valid 24-hour time (e.g. 02:30).', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
core.setdefault('dns_blocking', {}).setdefault('general', {})['daily_execute_time_24hr_local'] = daily_execute_time
|
cfg.setdefault('dns_blocking', {}).setdefault('general', {})['daily_execute_time_24hr_local'] = daily_execute_time
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
|
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
@ -192,21 +192,21 @@ def dnsblocking_cardlogging_save():
|
||||||
flash('Max Log Size must be a number >= 64.', 'error')
|
flash('Max Log Size must be a number >= 64.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
core.setdefault('dns_blocking', {}).setdefault('general', {}).update({
|
cfg.setdefault('dns_blocking', {}).setdefault('general', {}).update({
|
||||||
'log_max_kb': log_max_kb,
|
'log_max_kb': log_max_kb,
|
||||||
'log_errors_only': log_errors_only,
|
'log_errors_only': log_errors_only,
|
||||||
})
|
})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
|
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import os
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core, verify_core_hash, queued_msg, queue_command
|
from config_utils import load_config, save_config, verify_config_hash, queued_msg, queue_command
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ def networkinterfaces_cardnetworkinterface_save():
|
||||||
flash('WAN and LAN interfaces must be different.', 'error')
|
flash('WAN and LAN interfaces must be different.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
|
|
@ -54,16 +54,16 @@ def networkinterfaces_cardnetworkinterface_save():
|
||||||
flash(f"Interface '{iface}' does not exist on this system.", 'error')
|
flash(f"Interface '{iface}' does not exist on this system.", 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
gen = core.setdefault('network_interfaces', {})
|
gen = cfg.setdefault('network_interfaces', {})
|
||||||
gen['wan_interface'] = wan
|
gen['wan_interface'] = wan
|
||||||
gen['lan_interface'] = lan
|
gen['lan_interface'] = lan
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
|
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
@ -72,7 +72,7 @@ def networkinterfaces_cardnetworkinterface_save():
|
||||||
@bp.route('/action/networkinterfaces_cardinterfaceconfiguration_apply', methods=['POST'])
|
@bp.route('/action/networkinterfaces_cardinterfaceconfiguration_apply', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def networkinterfaces_cardinterfaceconfiguration_apply():
|
def networkinterfaces_cardinterfaceconfiguration_apply():
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from flask import Blueprint, request, redirect, flash
|
from flask import Blueprint, request, redirect, flash
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import load_core, save_core, verify_core_hash, queued_msg
|
from config_utils import load_config, save_config, verify_config_hash, queued_msg
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
|
|
@ -28,27 +28,27 @@ def upstreamdns_cardupstreamdns_save():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
upstream_servers.append(clean)
|
upstream_servers.append(clean)
|
||||||
|
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
current = core.get('upstream_dns', {})
|
current = cfg.get('upstream_dns', {})
|
||||||
if (strict_order == bool(current.get('strict_order', False)) and
|
if (strict_order == bool(current.get('strict_order', False)) and
|
||||||
upstream_servers == current.get('upstream_servers', [])):
|
upstream_servers == current.get('upstream_servers', [])):
|
||||||
flash('No changes detected.', 'info')
|
flash('No changes detected.', 'info')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core.setdefault('upstream_dns', {}).update({
|
cfg.setdefault('upstream_dns', {}).update({
|
||||||
'strict_order': strict_order,
|
'strict_order': strict_order,
|
||||||
'upstream_servers': upstream_servers,
|
'upstream_servers': upstream_servers,
|
||||||
})
|
})
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
|
|
@ -61,22 +61,22 @@ def upstreamdns_cardforwardingdnsservice_save():
|
||||||
flash('Cache Size must be a non-negative integer.', 'error')
|
flash('Cache Size must be a non-negative integer.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
if not verify_core_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core = load_core()
|
cfg = load_config()
|
||||||
current = core.get('upstream_dns', {})
|
current = cfg.get('upstream_dns', {})
|
||||||
if cache_size == int(current.get('cache_size', 0)):
|
if cache_size == int(current.get('cache_size', 0)):
|
||||||
flash('No changes detected.', 'info')
|
flash('No changes detected.', 'info')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
core.setdefault('upstream_dns', {})['cache_size'] = cache_size
|
cfg.setdefault('upstream_dns', {})['cache_size'] = cache_size
|
||||||
errors = validate.validate_config(core)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
save_core(core)
|
save_config(cfg)
|
||||||
flash(queued_msg('core apply'), 'success')
|
flash(queued_msg('core apply'), 'success')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from flask import session
|
||||||
CONFIGS_DIR = '/routlin_location'
|
CONFIGS_DIR = '/routlin_location'
|
||||||
DATA_DIR = '/data'
|
DATA_DIR = '/data'
|
||||||
ACCOUNTS_FILE = f'{DATA_DIR}/authorized_accounts.json'
|
ACCOUNTS_FILE = f'{DATA_DIR}/authorized_accounts.json'
|
||||||
CORE_FILE = f'{CONFIGS_DIR}/core.json'
|
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
||||||
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
||||||
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
||||||
DASHBOARD_LAST_RUN = f'{CONFIGS_DIR}/.dashboard-last-run'
|
DASHBOARD_LAST_RUN = f'{CONFIGS_DIR}/.dashboard-last-run'
|
||||||
|
|
@ -21,31 +21,31 @@ DASHB_INTERVAL_SECS = 60
|
||||||
QUEUE_MAX_LINES = 50
|
QUEUE_MAX_LINES = 50
|
||||||
|
|
||||||
|
|
||||||
def load_core():
|
def load_config():
|
||||||
try:
|
try:
|
||||||
with open(CORE_FILE) as f:
|
with open(CONFIG_FILE) as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def save_core(data):
|
def save_config(data):
|
||||||
with open(CORE_FILE, 'w') as f:
|
with open(CONFIG_FILE, 'w') as f:
|
||||||
json.dump(data, f, indent=2)
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def core_hash():
|
def config_hash():
|
||||||
try:
|
try:
|
||||||
with open(CORE_FILE, 'rb') as f:
|
with open(CONFIG_FILE, 'rb') as f:
|
||||||
return hashlib.md5(f.read()).hexdigest()
|
return hashlib.md5(f.read()).hexdigest()
|
||||||
except Exception:
|
except Exception:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def verify_core_hash(submitted):
|
def verify_config_hash(submitted):
|
||||||
if not submitted:
|
if not submitted:
|
||||||
return True
|
return True
|
||||||
return submitted == core_hash()
|
return submitted == config_hash()
|
||||||
|
|
||||||
|
|
||||||
def _load_done_set():
|
def _load_done_set():
|
||||||
|
|
@ -358,7 +358,7 @@ def _items_match(item, ref):
|
||||||
|
|
||||||
|
|
||||||
def revert_snapshot_to_core(entry_uuid):
|
def revert_snapshot_to_core(entry_uuid):
|
||||||
"""Apply the inverse of a snapshot to core.json and queue a new pending change.
|
"""Apply the inverse of a snapshot to config.json and queue a new pending change.
|
||||||
|
|
||||||
Returns (flash_message, success_bool).
|
Returns (flash_message, success_bool).
|
||||||
"""
|
"""
|
||||||
|
|
@ -375,7 +375,7 @@ def revert_snapshot_to_core(entry_uuid):
|
||||||
if operation == 'revert':
|
if operation == 'revert':
|
||||||
return 'This change is already a revert; cannot revert again.', False
|
return 'This change is already a revert; cannot revert again.', False
|
||||||
|
|
||||||
core = load_core()
|
core = load_config()
|
||||||
|
|
||||||
if key == 'global':
|
if key == 'global':
|
||||||
if before is None:
|
if before is None:
|
||||||
|
|
@ -396,7 +396,7 @@ def revert_snapshot_to_core(entry_uuid):
|
||||||
items[i] = before
|
items[i] = before
|
||||||
break
|
break
|
||||||
|
|
||||||
msg = save_core_with_snapshot(
|
msg = save_config_with_snapshot(
|
||||||
core, path=path, key=key, operation='revert',
|
core, path=path, key=key, operation='revert',
|
||||||
before=after, after=before,
|
before=after, after=before,
|
||||||
description=f"Reverted: {snap.get('description', '')}",
|
description=f"Reverted: {snap.get('description', '')}",
|
||||||
|
|
@ -417,7 +417,7 @@ def load_snapshot_for_uuid(entry_uuid):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def save_core_with_snapshot(new_core, path, key, operation, before, after,
|
def save_config_with_snapshot(new_core, path, key, operation, before, after,
|
||||||
description='', cmd='core apply', queue=True):
|
description='', cmd='core apply', queue=True):
|
||||||
"""
|
"""
|
||||||
Write a .snapshots/{ts}-{uuid}.json file, save new_core to disk, and
|
Write a .snapshots/{ts}-{uuid}.json file, save new_core to disk, and
|
||||||
|
|
@ -447,7 +447,7 @@ def save_core_with_snapshot(new_core, path, key, operation, before, after,
|
||||||
with open(os.path.join(SNAPSHOTS_DIR, f'{entry_ts}-{entry_uuid}.json'), 'w') as f:
|
with open(os.path.join(SNAPSHOTS_DIR, f'{entry_ts}-{entry_uuid}.json'), 'w') as f:
|
||||||
json.dump(snapshot, f, indent=2)
|
json.dump(snapshot, f, indent=2)
|
||||||
|
|
||||||
save_core(new_core)
|
save_config(new_core)
|
||||||
|
|
||||||
if not queue:
|
if not queue:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import json, re, subprocess, os, sys, html as html_mod
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from config_utils import core_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR
|
from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR
|
||||||
|
|
||||||
bp = Blueprint('view_page', __name__)
|
bp = Blueprint('view_page', __name__)
|
||||||
|
|
||||||
|
|
@ -44,8 +44,8 @@ def _load_json(path):
|
||||||
print(f'[view_page] ERROR loading {path}: {ex}', file=sys.stderr)
|
print(f'[view_page] ERROR loading {path}: {ex}', file=sys.stderr)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _load_core(): return _load_json(f'{CONFIGS_DIR}/core.json')
|
def _load_config(): return _load_json(f'{CONFIGS_DIR}/config.json')
|
||||||
def _load_ddns(): return _load_core().get('ddns', {})
|
def _load_ddns(): return _load_config().get('ddns', {})
|
||||||
def _load_accounts(): return _load_json(f'{DATA_DIR}/authorized_accounts.json')
|
def _load_accounts(): return _load_json(f'{DATA_DIR}/authorized_accounts.json')
|
||||||
|
|
||||||
def _load_css():
|
def _load_css():
|
||||||
|
|
@ -149,17 +149,17 @@ def _iface_status(iface):
|
||||||
return 'INVALID'
|
return 'INVALID'
|
||||||
|
|
||||||
|
|
||||||
def _resolve_iface(vlan, core):
|
def _resolve_iface(vlan, cfg):
|
||||||
"""Compute interface name from is_vpn + derived vlan_id + general.lan_interface."""
|
"""Compute interface name from is_vpn + derived vlan_id + general.lan_interface."""
|
||||||
if vlan.get('is_vpn'):
|
if vlan.get('is_vpn'):
|
||||||
wg_vlans = [v for v in core.get('vlans', []) if v.get('is_vpn')]
|
wg_vlans = [v for v in cfg.get('vlans', []) if v.get('is_vpn')]
|
||||||
wg_sorted = sorted(wg_vlans, key=lambda v: (
|
wg_sorted = sorted(wg_vlans, key=lambda v: (
|
||||||
validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) is None,
|
validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) is None,
|
||||||
validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) or 0,
|
validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) or 0,
|
||||||
))
|
))
|
||||||
idx = next((i for i, v in enumerate(wg_sorted) if v is vlan), 0)
|
idx = next((i for i, v in enumerate(wg_sorted) if v is vlan), 0)
|
||||||
return f'wg{idx}'
|
return f'wg{idx}'
|
||||||
lan = core.get('network_interfaces', {}).get('lan_interface', 'eth0')
|
lan = cfg.get('network_interfaces', {}).get('lan_interface', 'eth0')
|
||||||
vid = validate.derive_vlan_id(vlan.get('subnet', ''), vlan.get('subnet_mask', 24)) or 1
|
vid = validate.derive_vlan_id(vlan.get('subnet', ''), vlan.get('subnet_mask', 24)) or 1
|
||||||
return lan if vid == 1 else f'{lan}.{vid}'
|
return lan if vid == 1 else f'{lan}.{vid}'
|
||||||
|
|
||||||
|
|
@ -187,7 +187,7 @@ def _live_dhcp_leases():
|
||||||
|
|
||||||
def _vlan_name_for_ip(ip):
|
def _vlan_name_for_ip(ip):
|
||||||
import ipaddress
|
import ipaddress
|
||||||
for vlan in _load_core().get('vlans', []):
|
for vlan in _load_config().get('vlans', []):
|
||||||
subnet = vlan.get('subnet', '')
|
subnet = vlan.get('subnet', '')
|
||||||
mask = vlan.get('subnet_mask', 24)
|
mask = vlan.get('subnet_mask', 24)
|
||||||
if not subnet:
|
if not subnet:
|
||||||
|
|
@ -254,11 +254,11 @@ def _fmt_bytes(n):
|
||||||
# Config data loaders ===============================================
|
# Config data loaders ===============================================
|
||||||
|
|
||||||
def _config_datasource(name):
|
def _config_datasource(name):
|
||||||
core = _load_core()
|
cfg = _load_config()
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
|
|
||||||
if name == 'interfaces':
|
if name == 'interfaces':
|
||||||
gen = core.get('network_interfaces', {})
|
gen = cfg.get('network_interfaces', {})
|
||||||
wan = gen.get('wan_interface', '')
|
wan = gen.get('wan_interface', '')
|
||||||
lan = gen.get('lan_interface', '')
|
lan = gen.get('lan_interface', '')
|
||||||
return [
|
return [
|
||||||
|
|
@ -267,14 +267,14 @@ def _config_datasource(name):
|
||||||
]
|
]
|
||||||
|
|
||||||
if name == 'banned_ips':
|
if name == 'banned_ips':
|
||||||
return core.get('banned_ips', [])
|
return cfg.get('banned_ips', [])
|
||||||
|
|
||||||
if name == 'host_overrides':
|
if name == 'host_overrides':
|
||||||
return core.get('host_overrides', [])
|
return cfg.get('host_overrides', [])
|
||||||
|
|
||||||
if name == 'blocklists':
|
if name == 'blocklists':
|
||||||
rows = []
|
rows = []
|
||||||
for bl in core.get('dns_blocking', {}).get('blocklists', []):
|
for bl in cfg.get('dns_blocking', {}).get('blocklists', []):
|
||||||
row = dict(bl)
|
row = dict(bl)
|
||||||
bl_path = f'{CONFIGS_DIR}/blocklists/{bl.get("save_as", "")}'
|
bl_path = f'{CONFIGS_DIR}/blocklists/{bl.get("save_as", "")}'
|
||||||
try:
|
try:
|
||||||
|
|
@ -288,12 +288,12 @@ def _config_datasource(name):
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
if name == 'vlans':
|
if name == 'vlans':
|
||||||
bl_desc = {b['name']: b.get('description', b['name']) for b in core.get('dns_blocking', {}).get('blocklists', []) if 'name' in b}
|
bl_desc = {b['name']: b.get('description', b['name']) for b in cfg.get('dns_blocking', {}).get('blocklists', []) if 'name' in b}
|
||||||
rows = []
|
rows = []
|
||||||
for v in sorted(vlans, key=lambda x: validate.derive_vlan_id(x.get('subnet', ''), x.get('subnet_mask', 24)) or 0):
|
for v in sorted(vlans, key=lambda x: validate.derive_vlan_id(x.get('subnet', ''), x.get('subnet_mask', 24)) or 0):
|
||||||
row = {k: v.get(k) for k in ('name', 'subnet', 'subnet_mask', 'radius_default', 'mdns_reflection', 'is_vpn', 'dnsmasq_log_queries')}
|
row = {k: v.get(k) for k in ('name', 'subnet', 'subnet_mask', 'radius_default', 'mdns_reflection', 'is_vpn', 'dnsmasq_log_queries')}
|
||||||
row['vlan_id'] = validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24))
|
row['vlan_id'] = validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24))
|
||||||
row['interface'] = _resolve_iface(v, core)
|
row['interface'] = _resolve_iface(v, cfg)
|
||||||
row['use_blocklists'] = json.dumps([
|
row['use_blocklists'] = json.dumps([
|
||||||
{'n': bl, 'd': bl_desc.get(bl, bl)} for bl in v.get('use_blocklists', [])
|
{'n': bl, 'd': bl_desc.get(bl, bl)} for bl in v.get('use_blocklists', [])
|
||||||
])
|
])
|
||||||
|
|
@ -301,10 +301,10 @@ def _config_datasource(name):
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
if name == 'inter_vlan_exceptions':
|
if name == 'inter_vlan_exceptions':
|
||||||
return core.get('inter_vlan_exceptions', [])
|
return cfg.get('inter_vlan_exceptions', [])
|
||||||
|
|
||||||
if name == 'port_forwarding':
|
if name == 'port_forwarding':
|
||||||
return core.get('port_forwarding', [])
|
return cfg.get('port_forwarding', [])
|
||||||
|
|
||||||
if name == 'dhcp_reservations':
|
if name == 'dhcp_reservations':
|
||||||
rows = []
|
rows = []
|
||||||
|
|
@ -418,10 +418,10 @@ def _bl_last_update():
|
||||||
except Exception:
|
except Exception:
|
||||||
return '-'
|
return '-'
|
||||||
|
|
||||||
def _blocklist_stats_html(core):
|
def _blocklist_stats_html(cfg):
|
||||||
bl_dir = f'{CONFIGS_DIR}/blocklists'
|
bl_dir = f'{CONFIGS_DIR}/blocklists'
|
||||||
rows = ''
|
rows = ''
|
||||||
for bl in core.get('dns_blocking', {}).get('blocklists', []):
|
for bl in cfg.get('dns_blocking', {}).get('blocklists', []):
|
||||||
name = e(bl.get('name', ''))
|
name = e(bl.get('name', ''))
|
||||||
save_as = bl.get('save_as', '')
|
save_as = bl.get('save_as', '')
|
||||||
bl_path = f'{bl_dir}/{save_as}' if save_as else ''
|
bl_path = f'{bl_dir}/{save_as}' if save_as else ''
|
||||||
|
|
@ -546,7 +546,7 @@ def _ddns_last_checked():
|
||||||
return 'Last checked: ---'
|
return 'Last checked: ---'
|
||||||
|
|
||||||
def _vpn_info():
|
def _vpn_info():
|
||||||
for vlan in _load_core().get('vlans', []):
|
for vlan in _load_config().get('vlans', []):
|
||||||
if 'vpn_information' in vlan:
|
if 'vpn_information' in vlan:
|
||||||
return vlan['vpn_information']
|
return vlan['vpn_information']
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -556,11 +556,11 @@ def _vpn_info():
|
||||||
|
|
||||||
def collect_tokens():
|
def collect_tokens():
|
||||||
tokens = {}
|
tokens = {}
|
||||||
core = _load_core()
|
cfg = _load_config()
|
||||||
net = core.get('network_interfaces', {})
|
net = cfg.get('network_interfaces', {})
|
||||||
dns_blk_gen = core.get('dns_blocking', {}).get('general', {})
|
dns_blk_gen = cfg.get('dns_blocking', {}).get('general', {})
|
||||||
dns = core.get('upstream_dns', {})
|
dns = cfg.get('upstream_dns', {})
|
||||||
vlans = core.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
tokens['GENERAL_WAN_INTERFACE'] = str(net.get('wan_interface', '-'))
|
tokens['GENERAL_WAN_INTERFACE'] = str(net.get('wan_interface', '-'))
|
||||||
tokens['GENERAL_LAN_INTERFACE'] = str(net.get('lan_interface', '-'))
|
tokens['GENERAL_LAN_INTERFACE'] = str(net.get('lan_interface', '-'))
|
||||||
tokens['GENERAL_WAN_STATUS'] = _iface_status(net.get('wan_interface', ''))
|
tokens['GENERAL_WAN_STATUS'] = _iface_status(net.get('wan_interface', ''))
|
||||||
|
|
@ -693,10 +693,10 @@ def collect_tokens():
|
||||||
tokens['VPN_VLAN_COUNT'] = str(sum(1 for v in vlans if v.get('is_vpn')))
|
tokens['VPN_VLAN_COUNT'] = str(sum(1 for v in vlans if v.get('is_vpn')))
|
||||||
tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) for v in vlans])
|
tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) for v in vlans])
|
||||||
tokens['EXISTING_VLAN_NAMES_JSON'] = json.dumps([v.get('name', '') for v in vlans])
|
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['EXISTING_VLAN_INTERFACES_JSON'] = json.dumps([_resolve_iface(v, cfg) 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_BANNED_IP_COUNT'] = str(sum(1 for b in cfg.get('banned_ips', []) if b.get('enabled', True)))
|
||||||
tokens['STAT_BLOCKLIST_COUNT'] = str(len(core.get('dns_blocking', {}).get('blocklists', [])))
|
tokens['STAT_BLOCKLIST_COUNT'] = str(len(cfg.get('dns_blocking', {}).get('blocklists', [])))
|
||||||
tokens['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(core)
|
tokens['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(cfg)
|
||||||
|
|
||||||
ddns = _load_ddns()
|
ddns = _load_ddns()
|
||||||
ddns_gen = ddns.get('general', {})
|
ddns_gen = ddns.get('general', {})
|
||||||
|
|
@ -795,7 +795,7 @@ def collect_tokens():
|
||||||
|
|
||||||
tokens['BLOCKLIST_NAME_OPTIONS'] = json.dumps([
|
tokens['BLOCKLIST_NAME_OPTIONS'] = json.dumps([
|
||||||
{'value': bl.get('name', ''), 'label': bl.get('description', bl.get('name', ''))}
|
{'value': bl.get('name', ''), 'label': bl.get('description', bl.get('name', ''))}
|
||||||
for bl in core.get('dns_blocking', {}).get('blocklists', [])
|
for bl in cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||||
])
|
])
|
||||||
|
|
||||||
tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([
|
tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([
|
||||||
|
|
@ -984,7 +984,7 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
f'<button type="button" class="btn btn-ghost btn-sm stat-card-edit-btn">Edit</button>'
|
f'<button type="button" class="btn btn-ghost btn-sm stat-card-edit-btn">Edit</button>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
f'<form class="stat-card-edit-form" style="display:none" action="{e(edit_action)}" method="post">'
|
f'<form class="stat-card-edit-form" style="display:none" action="{e(edit_action)}" method="post">'
|
||||||
f'<input type="hidden" name="config_hash" value="{e(core_hash())}"/>'
|
f'<input type="hidden" name="config_hash" value="{e(config_hash())}"/>'
|
||||||
f'{input_wrap}'
|
f'{input_wrap}'
|
||||||
f'<div style="margin-top:0.5em;display:flex;gap:0.5em">'
|
f'<div style="margin-top:0.5em;display:flex;gap:0.5em">'
|
||||||
f'<button type="submit" class="btn btn-primary btn-sm" disabled>Save</button>'
|
f'<button type="submit" class="btn btn-primary btn-sm" disabled>Save</button>'
|
||||||
|
|
@ -1066,7 +1066,7 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
action = e(apply_tokens(item.get('action', ''), tokens))
|
action = e(apply_tokens(item.get('action', ''), tokens))
|
||||||
method = e(item.get('method', 'post'))
|
method = e(item.get('method', 'post'))
|
||||||
inner = render_items(item.get('items', []), tokens, req)
|
inner = render_items(item.get('items', []), tokens, req)
|
||||||
hash_field = f'<input type="hidden" name="config_hash" value="{e(core_hash())}"/>'
|
hash_field = f'<input type="hidden" name="config_hash" value="{e(config_hash())}"/>'
|
||||||
originals = _collect_form_originals(item.get('items', []), tokens)
|
originals = _collect_form_originals(item.get('items', []), tokens)
|
||||||
orig_field = (f'<input type="hidden" name="original_values" value="{e(json.dumps(originals))}"/>'
|
orig_field = (f'<input type="hidden" name="original_values" value="{e(json.dumps(originals))}"/>'
|
||||||
if originals else '')
|
if originals else '')
|
||||||
|
|
@ -1406,7 +1406,7 @@ def _render_table(item, tokens, inherited_req=None):
|
||||||
rows = _load_datasource(item.get('datasource', ''))
|
rows = _load_datasource(item.get('datasource', ''))
|
||||||
empty = e(item.get('empty_message', 'No data.'))
|
empty = e(item.get('empty_message', 'No data.'))
|
||||||
row_actions = item.get('row_actions', [])
|
row_actions = item.get('row_actions', [])
|
||||||
hash_val = core_hash()
|
hash_val = config_hash()
|
||||||
|
|
||||||
toolbar_html = ''
|
toolbar_html = ''
|
||||||
toolbar = item.get('toolbar')
|
toolbar = item.get('toolbar')
|
||||||
|
|
@ -1594,7 +1594,7 @@ def render_layout(view_id, content_html, tokens):
|
||||||
navbar_html = _render_navbar(view_id, level, tokens)
|
navbar_html = _render_navbar(view_id, level, tokens)
|
||||||
footer_html = f'<footer class="footer">{WEB_APP_DISPLAY_NAME}</footer>'
|
footer_html = f'<footer class="footer">{WEB_APP_DISPLAY_NAME}</footer>'
|
||||||
|
|
||||||
page_hash = core_hash()
|
page_hash = config_hash()
|
||||||
lan_iface = e(tokens.get('GENERAL_LAN_INTERFACE', ''))
|
lan_iface = e(tokens.get('GENERAL_LAN_INTERFACE', ''))
|
||||||
vpn_count = tokens.get('VPN_VLAN_COUNT', '0')
|
vpn_count = tokens.get('VPN_VLAN_COUNT', '0')
|
||||||
existing_ids = tokens.get('EXISTING_VLAN_IDS_JSON', '[]')
|
existing_ids = tokens.get('EXISTING_VLAN_IDS_JSON', '[]')
|
||||||
|
|
|
||||||
|
|
@ -627,7 +627,8 @@
|
||||||
{
|
{
|
||||||
"type": "button_secondary",
|
"type": "button_secondary",
|
||||||
"formaction": "/action/actions_cardpending_revertselected",
|
"formaction": "/action/actions_cardpending_revertselected",
|
||||||
"text": "Revert Selected"
|
"text": "Revert Selected",
|
||||||
|
"disabled": "%NO_PENDING%"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ All configuration lives in two JSON files. Edit these to match your network befo
|
||||||
|
|
||||||
| File | Controls |
|
| File | Controls |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `core.json` | VLANs, subnets, gateways, dynamic pools, static/dynamic reservations, RADIUS client flags, mDNS reflection scope, WireGuard interface settings and peers, upstream DNS servers, blocklist sources, per-VLAN blocklist assignments, host overrides, banned IPs, WAN interface, port forwarding rules, port wrangling, inter-VLAN exceptions |
|
| `config.json` | VLANs, subnets, gateways, dynamic pools, static/dynamic reservations, RADIUS client flags, mDNS reflection scope, WireGuard interface settings and peers, upstream DNS servers, blocklist sources, per-VLAN blocklist assignments, host overrides, banned IPs, WAN interface, port forwarding rules, port wrangling, inter-VLAN exceptions |
|
||||||
| `ddns.json` | DDNS provider credentials, hostnames/subdomains, update interval, IP-check services |
|
| `ddns.json` | DDNS provider credentials, hostnames/subdomains, update interval, IP-check services |
|
||||||
|
|
||||||
### Dotfiles (auto-generated, do not edit)
|
### Dotfiles (auto-generated, do not edit)
|
||||||
|
|
@ -33,7 +33,7 @@ All configuration lives in two JSON files. Edit these to match your network befo
|
||||||
|
|
||||||
## Initial Configuration
|
## Initial Configuration
|
||||||
|
|
||||||
### 1. Edit Core Configuration (`core.json`)
|
### 1. Edit Core Configuration (`config.json`)
|
||||||
|
|
||||||
Edit the top-level `network_interfaces` block:
|
Edit the top-level `network_interfaces` block:
|
||||||
|
|
||||||
|
|
@ -149,7 +149,7 @@ mDNS (Multicast DNS) is the protocol devices use to advertise and discover servi
|
||||||
|
|
||||||
**Multi-VLAN networks:** A device on the IoT VLAN (e.g. a network printer) advertising via mDNS is invisible to devices on the Kids or Trusted VLANs, because the multicast packets never leave the IoT subnet. The `mdns_reflection` feature solves this by running `avahi-daemon` as an mDNS proxy on the router, which has an interface on every VLAN. Avahi listens for mDNS announcements arriving on any of the designated reflection interfaces and re-broadcasts them on all the others, making services discoverable across VLANs without requiring any changes on the devices themselves.
|
**Multi-VLAN networks:** A device on the IoT VLAN (e.g. a network printer) advertising via mDNS is invisible to devices on the Kids or Trusted VLANs, because the multicast packets never leave the IoT subnet. The `mdns_reflection` feature solves this by running `avahi-daemon` as an mDNS proxy on the router, which has an interface on every VLAN. Avahi listens for mDNS announcements arriving on any of the designated reflection interfaces and re-broadcasts them on all the others, making services discoverable across VLANs without requiring any changes on the devices themselves.
|
||||||
|
|
||||||
Configure mDNS reflection with the top-level `mdns_reflection` block in `core.json`:
|
Configure mDNS reflection with the top-level `mdns_reflection` block in `config.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"mdns_reflection": {
|
"mdns_reflection": {
|
||||||
|
|
@ -190,7 +190,7 @@ sudo python3 ddns.py --start # Run an immediate IP update and install t
|
||||||
|
|
||||||
Optional (if WireGuard VPN is desired):
|
Optional (if WireGuard VPN is desired):
|
||||||
|
|
||||||
1. Add a WireGuard VLAN to `core.json` with `is_vpn: true` (see configuration example above)
|
1. Add a WireGuard VLAN to `config.json` with `is_vpn: true` (see configuration example above)
|
||||||
2. Run `sudo python3 core.py --apply` - this generates the server keypair, writes `/etc/wireguard/wg0.conf`, and brings the interface up
|
2. Run `sudo python3 core.py --apply` - this generates the server keypair, writes `/etc/wireguard/wg0.conf`, and brings the interface up
|
||||||
3. Add peers using `create_vpn_peer.py` (see below), then run `sudo python3 core.py --apply` again to sync them to the live interface
|
3. Add peers using `create_vpn_peer.py` (see below), then run `sudo python3 core.py --apply` again to sync them to the live interface
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ python3 create_vpn_peer.py --name phone --ip 192.168.40.3 --split-tunnel
|
||||||
python3 create_vpn_peer.py --name tablet --ip 192.168.40.4 --output ~/tablet.conf
|
python3 create_vpn_peer.py --name tablet --ip 192.168.40.4 --output ~/tablet.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
The script reads the specified WireGuard VLAN from `core.json`, validates the IP against the VLAN subnet, generates a keypair, appends the peer to `core.json`, and writes the client `.conf` file. If the config has exactly one WireGuard VLAN, `--iface` is optional. Transfer the `.conf` to the peer device by secure means, then delete it from the server.
|
The script reads the specified WireGuard VLAN from `config.json`, validates the IP against the VLAN subnet, generates a keypair, appends the peer to `config.json`, and writes the client `.conf` file. If the config has exactly one WireGuard VLAN, `--iface` is optional. Transfer the `.conf` to the peer device by secure means, then delete it from the server.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -266,7 +266,7 @@ Only `--start` and `--disable` require `sudo` as they install/remove systemd tim
|
||||||
sudo python3 ddns.py --start # Run update and install systemd timer
|
sudo python3 ddns.py --start # Run update and install systemd timer
|
||||||
sudo python3 ddns.py --disable # Stop updates and remove systemd timer
|
sudo python3 ddns.py --disable # Stop updates and remove systemd timer
|
||||||
|
|
||||||
python3 ddns.py --apply # Run one immediate DDNS update (used by timer)
|
python3 ddns.py --update # Run one immediate DDNS update (used by timer)
|
||||||
python3 ddns.py --force # Force update regardless of cached IP
|
python3 ddns.py --force # Force update regardless of cached IP
|
||||||
python3 ddns.py --status # Timer/service status
|
python3 ddns.py --status # Timer/service status
|
||||||
python3 ddns.py --getip # Print current public IP and exit
|
python3 ddns.py --getip # Print current public IP and exit
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
core.py -- Apply core.json to systemd-networkd, per-VLAN dnsmasq instances, and nftables.
|
core.py -- Apply config.json to systemd-networkd, per-VLAN dnsmasq instances, and nftables.
|
||||||
|
|
||||||
Each VLAN defined in core.json gets its own dnsmasq instance that handles
|
Each VLAN defined in config.json gets its own dnsmasq instance that handles
|
||||||
both DHCP and DNS for that VLAN. WireGuard VLANs get a DNS-only instance
|
both DHCP and DNS for that VLAN. WireGuard VLANs get a DNS-only instance
|
||||||
(no DHCP, since peers have statically assigned IPs).
|
(no DHCP, since peers have statically assigned IPs).
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ from validation import (
|
||||||
PRODUCT_NAME = "routlin"
|
PRODUCT_NAME = "routlin"
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).parent
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
CONFIG_FILE = SCRIPT_DIR / "core.json"
|
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||||
BLOCKLIST_DIR = SCRIPT_DIR / "blocklists"
|
BLOCKLIST_DIR = SCRIPT_DIR / "blocklists"
|
||||||
METRICS_FILE = SCRIPT_DIR / ".dns-metrics"
|
METRICS_FILE = SCRIPT_DIR / ".dns-metrics"
|
||||||
DNSMASQ_CONF_DIR = Path(f"/etc/dnsmasq-{PRODUCT_NAME}")
|
DNSMASQ_CONF_DIR = Path(f"/etc/dnsmasq-{PRODUCT_NAME}")
|
||||||
|
|
@ -260,7 +260,7 @@ def load_config():
|
||||||
with open(CONFIG_FILE) as f:
|
with open(CONFIG_FILE) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
if not data.get("vlans"):
|
if not data.get("vlans"):
|
||||||
die("No vlans defined in core.json.")
|
die("No vlans defined in config.json.")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
@ -270,7 +270,7 @@ def load_config():
|
||||||
def build_netdev(vlan, vid, iface):
|
def build_netdev(vlan, vid, iface):
|
||||||
return "\n".join([
|
return "\n".join([
|
||||||
"# Generated by core.py -- do not edit manually.",
|
"# Generated by core.py -- do not edit manually.",
|
||||||
"# Edit core.json and re-run: sudo python3 core.py --apply",
|
"# Edit config.json and re-run: sudo python3 core.py --apply",
|
||||||
"",
|
"",
|
||||||
"[NetDev]",
|
"[NetDev]",
|
||||||
f"Name={iface}",
|
f"Name={iface}",
|
||||||
|
|
@ -286,7 +286,7 @@ def build_network(vlan, vid, iface, all_vlan_ids):
|
||||||
prefix = network.prefixlen
|
prefix = network.prefixlen
|
||||||
lines = [
|
lines = [
|
||||||
"# Generated by core.py -- do not edit manually.",
|
"# Generated by core.py -- do not edit manually.",
|
||||||
"# Edit core.json and re-run: sudo python3 core.py --apply",
|
"# Edit config.json and re-run: sudo python3 core.py --apply",
|
||||||
"",
|
"",
|
||||||
"[Match]",
|
"[Match]",
|
||||||
f"Name={iface}",
|
f"Name={iface}",
|
||||||
|
|
@ -452,7 +452,7 @@ def build_vlan_dnsmasq_conf(vlan, data, iface):
|
||||||
L.append(s)
|
L.append(s)
|
||||||
|
|
||||||
line("# Generated by core.py -- do not edit manually.")
|
line("# Generated by core.py -- do not edit manually.")
|
||||||
line("# Edit core.json and re-run: sudo python3 core.py --apply")
|
line("# Edit config.json and re-run: sudo python3 core.py --apply")
|
||||||
line(f"# VLAN: {name} (vlan_id={derive_vlan_id(vlan.get('subnet', ''), vlan.get('subnet_mask', 24))})")
|
line(f"# VLAN: {name} (vlan_id={derive_vlan_id(vlan.get('subnet', ''), vlan.get('subnet_mask', 24))})")
|
||||||
line()
|
line()
|
||||||
line(f"pid-file={vlan_pid_file(vlan)}")
|
line(f"pid-file={vlan_pid_file(vlan)}")
|
||||||
|
|
@ -772,7 +772,7 @@ def generate_wg_server_key(iface):
|
||||||
return private
|
return private
|
||||||
|
|
||||||
def build_wg_server_conf(vlan, server_private_key, iface):
|
def build_wg_server_conf(vlan, server_private_key, iface):
|
||||||
"""Build the /etc/wireguard/<iface>.conf content from core.json peers."""
|
"""Build the /etc/wireguard/<iface>.conf content from config.json peers."""
|
||||||
info = vlan["vpn_information"]
|
info = vlan["vpn_information"]
|
||||||
gateway = resolve_vlan_options(vlan)["gateway"]
|
gateway = resolve_vlan_options(vlan)["gateway"]
|
||||||
network = network_for(vlan)
|
network = network_for(vlan)
|
||||||
|
|
@ -1158,7 +1158,7 @@ def install_ddns_timer(data):
|
||||||
"",
|
"",
|
||||||
"[Service]",
|
"[Service]",
|
||||||
"Type=oneshot",
|
"Type=oneshot",
|
||||||
f"ExecStart=/usr/bin/python3 {script_path} --apply",
|
f"ExecStart=/usr/bin/python3 {script_path} --update",
|
||||||
"",
|
"",
|
||||||
])
|
])
|
||||||
timer_content = "\n".join([
|
timer_content = "\n".join([
|
||||||
|
|
@ -1387,7 +1387,7 @@ def build_nft_config(data, dry_run=False):
|
||||||
L.append(s)
|
L.append(s)
|
||||||
|
|
||||||
line("# Generated by core.py -- do not edit manually.")
|
line("# Generated by core.py -- do not edit manually.")
|
||||||
line("# Edit core.json and re-run: sudo python3 core.py --apply")
|
line("# Edit config.json and re-run: sudo python3 core.py --apply")
|
||||||
line()
|
line()
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
|
|
@ -1829,7 +1829,7 @@ def build_radius_clients_conf(data, secret):
|
||||||
"""Generate freeradius clients.conf from reservations with radius_client: true."""
|
"""Generate freeradius clients.conf from reservations with radius_client: true."""
|
||||||
lines = [
|
lines = [
|
||||||
"# Generated by core.py -- do not edit manually.",
|
"# Generated by core.py -- do not edit manually.",
|
||||||
"# Edit core.json and re-run: sudo python3 core.py --apply",
|
"# Edit config.json and re-run: sudo python3 core.py --apply",
|
||||||
"",
|
"",
|
||||||
"# localhost (required)",
|
"# localhost (required)",
|
||||||
"client localhost {",
|
"client localhost {",
|
||||||
|
|
@ -1867,7 +1867,7 @@ def build_radius_users(data):
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"# Generated by core.py -- do not edit manually.",
|
"# Generated by core.py -- do not edit manually.",
|
||||||
"# Edit core.json and re-run: sudo python3 core.py --apply",
|
"# Edit config.json and re-run: sudo python3 core.py --apply",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -3028,7 +3028,7 @@ def cmd_apply(data, dry_run=False):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Apply core.json to systemd-networkd, per-VLAN dnsmasq instances, and nftables",
|
description="Apply config.json to systemd-networkd, per-VLAN dnsmasq instances, and nftables",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog=(
|
epilog=(
|
||||||
"examples:\n"
|
"examples:\n"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
create_vpn_peer.py -- Add a WireGuard peer to core.json and write the client .conf file.
|
create_vpn_peer.py -- Add a WireGuard peer to config.json and write the client .conf file.
|
||||||
|
|
||||||
Generates a fresh keypair, appends the peer to the specified WireGuard VLAN in core.json,
|
Generates a fresh keypair, appends the peer to the specified WireGuard VLAN in config.json,
|
||||||
and saves a ready-to-import client config file.
|
and saves a ready-to-import client config file.
|
||||||
|
|
||||||
Use --iface or --vlan-id to select the target VLAN. If the config contains exactly one
|
Use --iface or --vlan-id to select the target VLAN. If the config contains exactly one
|
||||||
|
|
@ -26,7 +26,7 @@ import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).parent
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
CONFIG_FILE = SCRIPT_DIR / "core.json"
|
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||||
|
|
||||||
|
|
||||||
def die(msg):
|
def die(msg):
|
||||||
|
|
@ -61,7 +61,7 @@ def find_wg_vlan(data, iface=None, vlan_id=None):
|
||||||
vlan = next((v for v in wg_vlans if resolve_wg_iface(v, data) == iface), None)
|
vlan = next((v for v in wg_vlans if resolve_wg_iface(v, data) == iface), None)
|
||||||
if vlan is None:
|
if vlan is None:
|
||||||
known = ", ".join(resolve_wg_iface(v, data) for v in wg_vlans) or "none"
|
known = ", ".join(resolve_wg_iface(v, data) for v in wg_vlans) or "none"
|
||||||
die(f"No WireGuard VLAN with interface '{iface}' found in core.json. "
|
die(f"No WireGuard VLAN with interface '{iface}' found in config.json. "
|
||||||
f"Known WireGuard interfaces: {known}.")
|
f"Known WireGuard interfaces: {known}.")
|
||||||
return vlan
|
return vlan
|
||||||
|
|
||||||
|
|
@ -71,12 +71,12 @@ def find_wg_vlan(data, iface=None, vlan_id=None):
|
||||||
known = ", ".join(
|
known = ", ".join(
|
||||||
f"{v['vlan_id']} ({resolve_wg_iface(v, data)})" for v in wg_vlans
|
f"{v['vlan_id']} ({resolve_wg_iface(v, data)})" for v in wg_vlans
|
||||||
) or "none"
|
) or "none"
|
||||||
die(f"No WireGuard VLAN with vlan_id {vlan_id} found in core.json. "
|
die(f"No WireGuard VLAN with vlan_id {vlan_id} found in config.json. "
|
||||||
f"Known WireGuard VLANs: {known}.")
|
f"Known WireGuard VLANs: {known}.")
|
||||||
return vlan
|
return vlan
|
||||||
|
|
||||||
if not wg_vlans:
|
if not wg_vlans:
|
||||||
die("No WireGuard VLANs found in core.json. "
|
die("No WireGuard VLANs found in config.json. "
|
||||||
"Add a VLAN with is_vpn set to true.")
|
"Add a VLAN with is_vpn set to true.")
|
||||||
if len(wg_vlans) > 1:
|
if len(wg_vlans) > 1:
|
||||||
options = " " + "\n ".join(
|
options = " " + "\n ".join(
|
||||||
|
|
@ -149,7 +149,7 @@ def build_client_conf(vlan, peer_ip, private_key, server_pub, split_tunnel):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Add a WireGuard peer to core.json and write the client .conf file."
|
description="Add a WireGuard peer to config.json and write the client .conf file."
|
||||||
)
|
)
|
||||||
parser.add_argument("--name", required=True, help="Peer name (e.g. laptop)")
|
parser.add_argument("--name", required=True, help="Peer name (e.g. laptop)")
|
||||||
parser.add_argument("--ip", required=True, help="Peer IP within the VPN subnet (e.g. 192.168.40.2)")
|
parser.add_argument("--ip", required=True, help="Peer IP within the VPN subnet (e.g. 192.168.40.2)")
|
||||||
|
|
@ -198,7 +198,7 @@ def main():
|
||||||
private_key, public_key = generate_keypair()
|
private_key, public_key = generate_keypair()
|
||||||
srv_pub = server_pubkey(iface)
|
srv_pub = server_pubkey(iface)
|
||||||
|
|
||||||
# -- Update core.json ------------------------------------------------------
|
# -- Update config.json ------------------------------------------------------
|
||||||
peers.append({
|
peers.append({
|
||||||
"name": args.name,
|
"name": args.name,
|
||||||
"ip": peer_ip,
|
"ip": peer_ip,
|
||||||
|
|
@ -207,7 +207,7 @@ def main():
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
})
|
})
|
||||||
save_config(data)
|
save_config(data)
|
||||||
print(f"Added peer '{args.name}' to core.json.")
|
print(f"Added peer '{args.name}' to config.json.")
|
||||||
|
|
||||||
# -- Write client conf -----------------------------------------------------
|
# -- Write client conf -----------------------------------------------------
|
||||||
conf_content = build_client_conf(vlan, peer_ip, private_key, srv_pub, args.split_tunnel)
|
conf_content = build_client_conf(vlan, peer_ip, private_key, srv_pub, args.split_tunnel)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"""
|
"""
|
||||||
ddns.py -- Update DDNS provider(s) with current public IP.
|
ddns.py -- Update DDNS provider(s) with current public IP.
|
||||||
|
|
||||||
Reads the ddns block from core.json, fetches the current public IP,
|
Reads the ddns block from config.json, fetches the current public IP,
|
||||||
and updates each enabled provider block only if the IP has changed
|
and updates each enabled provider block only if the IP has changed
|
||||||
since the last successful update for that provider.
|
since the last successful update for that provider.
|
||||||
Designed to be run on a systemd timer managed by core.py --apply.
|
Designed to be run on a systemd timer managed by core.py --apply.
|
||||||
|
|
@ -16,7 +16,7 @@ Logs to ddns.log in the same directory as this script.
|
||||||
Log is cleared when it exceeds general.log_max_kb from config.
|
Log is cleared when it exceeds general.log_max_kb from config.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 ddns.py --apply Run update once (used by timer)
|
python3 ddns.py --update Run update once (used by timer)
|
||||||
python3 ddns.py --force Force update regardless of cached IP
|
python3 ddns.py --force Force update regardless of cached IP
|
||||||
python3 ddns.py --getip Print current public IP and exit
|
python3 ddns.py --getip Print current public IP and exit
|
||||||
"""
|
"""
|
||||||
|
|
@ -32,7 +32,7 @@ import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).parent
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
CONFIG_FILE = SCRIPT_DIR / "core.json"
|
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||||
CACHE_SERVICE_FILE = SCRIPT_DIR / ".ddns-last-service"
|
CACHE_SERVICE_FILE = SCRIPT_DIR / ".ddns-last-service"
|
||||||
LOG_FILE = SCRIPT_DIR / "ddns.log"
|
LOG_FILE = SCRIPT_DIR / "ddns.log"
|
||||||
|
|
||||||
|
|
@ -512,18 +512,18 @@ def main():
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog=(
|
epilog=(
|
||||||
"examples:\n"
|
"examples:\n"
|
||||||
" python3 ddns.py --apply Run update once (used by timer)\n"
|
" python3 ddns.py --update Run update once (used by timer)\n"
|
||||||
" python3 ddns.py --force Force update regardless of cached IP\n"
|
" python3 ddns.py --force Force update regardless of cached IP\n"
|
||||||
" python3 ddns.py --getip Print current public IP and exit\n"
|
" python3 ddns.py --getip Print current public IP and exit\n"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
parser.add_argument("--apply", action="store_true", help="Run update once (used by timer)")
|
parser.add_argument("--update", action="store_true", help="Run update once (used by timer)")
|
||||||
parser.add_argument("--force", action="store_true", help="Force update regardless of cached IP")
|
parser.add_argument("--force", action="store_true", help="Force update regardless of cached IP")
|
||||||
parser.add_argument("--getip", action="store_true", help="Print current public IP and exit")
|
parser.add_argument("--getip", action="store_true", help="Print current public IP and exit")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not any([args.apply, args.force, args.getip]):
|
if not any([args.update, args.force, args.getip]):
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -540,7 +540,7 @@ def main():
|
||||||
general = cfg["general"]
|
general = cfg["general"]
|
||||||
setup_logging(general["log_max_kb"], general["log_errors_only"])
|
setup_logging(general["log_max_kb"], general["log_errors_only"])
|
||||||
|
|
||||||
if args.apply or args.force:
|
if args.update or args.force:
|
||||||
run_update(cfg, force=args.force)
|
run_update(cfg, force=args.force)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
dns-blocklists.py -- Download and merge DNS blocklists defined in core.json.
|
dns-blocklists.py -- Download and merge DNS blocklists defined in config.json.
|
||||||
|
|
||||||
Reads the blocklists library from core.json, downloads every blocklist referenced
|
Reads the blocklists library from config.json, downloads every blocklist referenced
|
||||||
by at least one VLAN, merges them into per-combo conf files (one per unique
|
by at least one VLAN, merges them into per-combo conf files (one per unique
|
||||||
combination of blocklist names), then sends SIGHUP to each running dnsmasq
|
combination of blocklist names), then sends SIGHUP to each running dnsmasq
|
||||||
instance so it reloads its config without restarting.
|
instance so it reloads its config without restarting.
|
||||||
|
|
@ -23,7 +23,7 @@ from pathlib import Path
|
||||||
|
|
||||||
PRODUCT_NAME = "routlin"
|
PRODUCT_NAME = "routlin"
|
||||||
SCRIPT_DIR = Path(__file__).parent
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
CONFIG_FILE = SCRIPT_DIR / "core.json"
|
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||||
BLOCKLIST_DIR = SCRIPT_DIR / "blocklists"
|
BLOCKLIST_DIR = SCRIPT_DIR / "blocklists"
|
||||||
LOG_FILE = SCRIPT_DIR / "dns-blocklists.log"
|
LOG_FILE = SCRIPT_DIR / "dns-blocklists.log"
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ def load_config():
|
||||||
with open(CONFIG_FILE) as f:
|
with open(CONFIG_FILE) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
if not data.get("vlans"):
|
if not data.get("vlans"):
|
||||||
die("No vlans defined in core.json.")
|
die("No vlans defined in config.json.")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
health.py -- System health checks for Routlin.
|
health.py -- System health checks for Routlin.
|
||||||
|
|
||||||
Reads core.json, checks services, configuration files, and logs, then writes
|
Reads config.json, checks services, configuration files, and logs, then writes
|
||||||
.health JSON. Imported by core.py; also runnable standalone.
|
.health JSON. Imported by core.py; also runnable standalone.
|
||||||
|
|
||||||
Public API:
|
Public API:
|
||||||
|
|
@ -29,7 +29,7 @@ from validation import derive_interface, derive_vlan_id, is_wg
|
||||||
PRODUCT_NAME = "routlin"
|
PRODUCT_NAME = "routlin"
|
||||||
SCRIPT_DIR = Path(__file__).parent
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
HEALTH_FILE = SCRIPT_DIR / ".health"
|
HEALTH_FILE = SCRIPT_DIR / ".health"
|
||||||
CONFIG_FILE = SCRIPT_DIR / "core.json"
|
CONFIG_FILE = SCRIPT_DIR / "config.json"
|
||||||
BLOCKLIST_DIR = SCRIPT_DIR / "blocklists"
|
BLOCKLIST_DIR = SCRIPT_DIR / "blocklists"
|
||||||
DNSMASQ_CONF_DIR = Path(f"/etc/dnsmasq-{PRODUCT_NAME}")
|
DNSMASQ_CONF_DIR = Path(f"/etc/dnsmasq-{PRODUCT_NAME}")
|
||||||
LEASES_DIR = Path("/var/lib/misc")
|
LEASES_DIR = Path("/var/lib/misc")
|
||||||
|
|
@ -532,7 +532,7 @@ def check_configurations(data):
|
||||||
f"DHCP pool ({vlan['name']})", "warning",
|
f"DHCP pool ({vlan['name']})", "warning",
|
||||||
f"DHCP pool for VLAN '{vlan['name']}' is {pct}% full "
|
f"DHCP pool for VLAN '{vlan['name']}' is {pct}% full "
|
||||||
f"({len(leases)}/{pool_size} leases).",
|
f"({len(leases)}/{pool_size} leases).",
|
||||||
"Expand the pool range in core.json or clean up stale leases "
|
"Expand the pool range in config.json or clean up stale leases "
|
||||||
f"with: `sudo python3 core.py --reset-leases {vlan['name']}`"))
|
f"with: `sudo python3 core.py --reset-leases {vlan['name']}`"))
|
||||||
else:
|
else:
|
||||||
results.append(_ok(f"dhcp_pool_{vlan['name']}",
|
results.append(_ok(f"dhcp_pool_{vlan['name']}",
|
||||||
|
|
@ -596,7 +596,7 @@ def check_configurations(data):
|
||||||
results.append(_problem(
|
results.append(_problem(
|
||||||
"upstream_dns", "Upstream DNS reachability", "warning",
|
"upstream_dns", "Upstream DNS reachability", "warning",
|
||||||
f"Upstream DNS server(s) unreachable on port 53: {', '.join(unreachable)}.",
|
f"Upstream DNS server(s) unreachable on port 53: {', '.join(unreachable)}.",
|
||||||
"Check WAN connectivity and upstream DNS server addresses in core.json."))
|
"Check WAN connectivity and upstream DNS server addresses in config.json."))
|
||||||
elif servers:
|
elif servers:
|
||||||
results.append(_ok("upstream_dns", "Upstream DNS reachability"))
|
results.append(_ok("upstream_dns", "Upstream DNS reachability"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -564,13 +564,13 @@ def main():
|
||||||
# -- Dashboard -------------------------------------------------
|
# -- Dashboard -------------------------------------------------
|
||||||
header("Dashboard (optional)")
|
header("Dashboard (optional)")
|
||||||
print(" The Routlin Dashboard is a web UI for managing the router.")
|
print(" The Routlin Dashboard is a web UI for managing the router.")
|
||||||
print(" It runs as a Docker container. Without it, core.json must")
|
print(" It runs as a Docker container. Without it, config.json must")
|
||||||
print(" be edited manually.")
|
print(" be edited manually.")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
next_step = (
|
next_step = (
|
||||||
f"\n Next step: use the web dashboard to configure your network, or\n"
|
f"\n Next step: use the web dashboard to configure your network, or\n"
|
||||||
f" configure {SCRIPT_DIR}/core.json manually and then run:\n"
|
f" configure {SCRIPT_DIR}/config.json manually and then run:\n"
|
||||||
f" sudo python3 {SCRIPT_DIR}/core.py --apply"
|
f" sudo python3 {SCRIPT_DIR}/core.py --apply"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
validation.py -- Shared structural validators for core.json fields.
|
validation.py -- Shared structural validators for config.json fields.
|
||||||
|
|
||||||
Lives alongside core.py in ~/routlin/ and is volume-mounted into the
|
Lives alongside core.py in ~/routlin/ and is volume-mounted into the
|
||||||
routlin-dash container at /app/validation.py. Importable by both
|
routlin-dash container at /app/validation.py. Importable by both
|
||||||
|
|
@ -304,7 +304,7 @@ def derive_interface(vlan, data):
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
def validate_config(data):
|
def validate_config(data):
|
||||||
"""Validate core.json structure and content. Returns list of error strings."""
|
"""Validate config.json structure and content. Returns list of error strings."""
|
||||||
errors = []
|
errors = []
|
||||||
seen_vlan_ids = {}
|
seen_vlan_ids = {}
|
||||||
seen_interfaces = {}
|
seen_interfaces = {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue