Development

This commit is contained in:
Matthew Grotke 2026-05-25 21:49:47 -04:00
parent ac0aa4de22
commit 91d8b950b7
5 changed files with 38 additions and 35 deletions

View file

@ -165,19 +165,19 @@ def main():
help="VLAN ID of the WireGuard VLAN to add the peer to (e.g. 40)")
args = parser.parse_args()
# -- Validate IP -----------------------------------------------------------
# Validate IP =======================================================
try:
peer_ip = str(ipaddress.IPv4Address(args.ip))
except ValueError:
die(f"'{args.ip}' is not a valid IPv4 address.")
# -- Load config and find WG VLAN ------------------------------------------
# Load config and find WG VLAN ==========================================
data = load_config()
vlan = find_wg_vlan(data, iface=args.iface, vlan_id=args.vlan_id)
iface = resolve_wg_iface(vlan, data)
# -- Validate peer IP is within subnet -------------------------------------
# Validate peer IP is within subnet =================================
try:
network = ipaddress.IPv4Network(f"{vlan['subnet']}/{vlan['subnet_mask']}", strict=False)
except (KeyError, ValueError) as e:
@ -186,19 +186,19 @@ def main():
if ipaddress.IPv4Address(peer_ip) not in network:
die(f"IP {peer_ip} is not within the VPN subnet {network}.")
# -- Check for duplicates --------------------------------------------------
# Check for duplicates ==============================================
peers = vlan.setdefault("peers", [])
if any(p.get("name") == args.name for p in peers):
die(f"A peer named '{args.name}' already exists.")
if any(p.get("ip") == peer_ip for p in peers):
die(f"IP {peer_ip} is already assigned to another peer.")
# -- Generate keypair and read server public key ---------------------------
# Generate keypair and read server public key =======================
print(f"Generating keypair for '{args.name}'...")
private_key, public_key = generate_keypair()
srv_pub = server_pubkey(iface)
# -- Update config.json ------------------------------------------------------
# Update config.json ================================================
peers.append({
"name": args.name,
"ip": peer_ip,
@ -209,7 +209,7 @@ def main():
save_config(data)
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)
if args.output:
out_path = Path(args.output)

View file

@ -545,7 +545,7 @@ def main():
print(" This wizard installs required packages and optionally")
print(" sets up the web dashboard and external HTTPS access.")
# -- Package manager -------------------------------------------
# Package manager ===================================================
pm = detect_pm()
if pm is None:
print()
@ -557,11 +557,11 @@ def main():
print(f"\n Detected package manager: {pm}")
pm_ok = True
# -- Core packages ---------------------------------------------
# Core packages =====================================================
if pm_ok:
install_core_packages(pm)
# -- Dashboard -------------------------------------------------
# Dashboard =========================================================
header("Dashboard (optional)")
print(" The Routlin Dashboard is a web UI for managing the router.")
print(" It runs as a Docker container. Without it, config.json must")
@ -596,7 +596,7 @@ def main():
default="y"
)
# -- Docker ----------------------------------------------------
# Docker ============================================================
header("Docker")
if pm_ok:
install_docker(pm)
@ -605,15 +605,15 @@ def main():
else:
print(" Docker is already installed.")
# -- docker-compose.yml ----------------------------------------
# docker-compose.yml ================================================
setup_docker_compose(reuse_config=reuse_config)
create_dotfiles()
# -- Dashboard timer -------------------------------------------
# Dashboard timer ===================================================
header("Dashboard Timer")
install_dashboard_timer()
# -- External access -------------------------------------------
# External access ===================================================
header("External Access (optional)")
ext_domain = _external_access_domain()
if ext_domain:
@ -643,7 +643,7 @@ def main():
print(next_step)
return
# -- Caddy -----------------------------------------------------
# Caddy =============================================================
header("Caddy (HTTPS)")
if pm_ok:
install_caddy(pm)

View file

@ -331,11 +331,11 @@ def validate_config(data):
_vid = _derived_ids[i]
vlan_ifaces.append(_lan if _vid == 1 else f"{_lan}.{_vid}")
# -- upstream_dns block ----------------------------------------------------
# upstream_dns block ============================================
if not data.get("upstream_dns", {}).get("upstream_servers"):
errors.append("upstream_dns.upstream_servers is missing or empty.")
# -- WAN / LAN interfaces --------------------------------------------------
# WAN / LAN interfaces ==========================================
gen = data.get("network_interfaces", {})
wan = gen.get("wan_interface", "")
lan = gen.get("lan_interface", "")
@ -357,7 +357,7 @@ def validate_config(data):
if wan == lan:
errors.append(f"network_interfaces.wan_interface and network_interfaces.lan_interface must be different (both set to '{wan}').")
# -- Blocklist library -----------------------------------------------------
# Blocklist library =============================================
blocklists_by_name = {}
for idx, bl in enumerate(data.get("dns_blocking", {}).get("blocklists", [])):
name = bl.get("name", "")
@ -373,7 +373,7 @@ def validate_config(data):
else:
blocklists_by_name[name] = bl
# -- Per-VLAN validation ---------------------------------------------------
# Per-VLAN validation ===========================================
vlan_networks = {} # iface -> IPv4Network (used for NAT section)
for i, (vlan, iface) in enumerate(zip(_all_vlans, vlan_ifaces)):
@ -403,7 +403,7 @@ def validate_config(data):
errors.append(f"{label}: mdns_reflection must be false for WireGuard interfaces.")
if is_wg(vlan):
# -- vpn_information -----------------------------------------------
# vpn_information =======================================
vpi = vlan.get("vpn_information")
if not isinstance(vpi, dict):
errors.append(f"{label}: vpn_information must be a plain object.")
@ -418,7 +418,7 @@ def validate_config(data):
else:
seen_listen_ports[lp] = name
# -- subnet/subnet_mask --------------------------------------------
# subnet/subnet_mask ====================================
for field in ("subnet", "subnet_mask"):
if not vlan.get(field):
errors.append(f"{label}: missing required field '{field}'.")
@ -430,7 +430,7 @@ def validate_config(data):
except ValueError as e:
errors.append(f"{label}: invalid subnet/subnet_mask: {e}")
# -- server_identities ---------------------------------------------
# server_identities =====================================
if not vlan.get("server_identities"):
errors.append(f"{label}: server_identities is empty or missing.")
identity_ips = []
@ -449,7 +449,7 @@ def validate_config(data):
else:
identity_ips.append(ip_addr)
# -- vpn_information.explicit_overrides ----------------------------
# vpn_information.explicit_overrides ====================
eo = vpi.get("explicit_overrides", {}) if isinstance(vpi, dict) else {}
if not isinstance(eo, dict):
errors.append(f"{label}: vpn_information.explicit_overrides must be a plain object.")
@ -476,7 +476,7 @@ def validate_config(data):
if domain_val and not domainname(domain_val):
errors.append(f"{label}: vpn_information.domain '{domain_val}' is not a valid domain name.")
# -- peers ---------------------------------------------------------
# peers =================================================
seen_peer_names = {}
seen_peer_ips = {}
for pidx, peer in enumerate(vlan.get("peers", [])):
@ -556,7 +556,7 @@ def validate_config(data):
if ip:
identity_ips.append(ip)
# -- Validate explicit_overrides ---------------------------------------
# Validate explicit_overrides ===============================
eo = d.get("explicit_overrides", {})
if not isinstance(eo, dict):
errors.append(f"{label}: explicit_overrides must be a plain object.")
@ -642,7 +642,7 @@ def validate_config(data):
if bl_name not in blocklists_by_name:
errors.append(f"{label}: use_blocklists references unknown blocklist '{bl_name}'.")
# -- NAT / firewall validation ---------------------------------------------
# NAT / firewall validation =====================================
valid_protos = VALID_PROTOCOLS
known_interfaces = set(seen_interfaces.keys())
@ -675,7 +675,7 @@ def validate_config(data):
if net:
nat_check_ip_in_network(f"{label} redirect_to", r.get("redirect_to", ""), net)
# -- port_forwarding validation (top-level) --------------------------------
# port_forwarding validation (top-level) ========================
for idx, r in enumerate(data.get("port_forwarding", [])):
desc = r.get("description", "?")
label = f"port_forwarding[{idx}] '{desc}'"
@ -709,13 +709,13 @@ def validate_config(data):
if r.get("dst_port") is not None:
nat_check_port(f"{label} dst_port", r.get("dst_port"))
# -- radius_default uniqueness check ---------------------------------------
# radius_default uniqueness check ===============================
defaults = [v["name"] for v in data.get("vlans", []) if v.get("radius_default") is True]
if len(defaults) > 1:
errors.append(f"Multiple VLANs have radius_default: true ({', '.join(defaults)}). "
f"Only one VLAN may be the RADIUS default.")
# -- RADIUS requires multiple VLANs ----------------------------------------
# RADIUS requires multiple VLANs ================================
non_wg_vlans = [v for v in data.get("vlans", []) if not is_wg(v)]
has_radius_clients = any(
r.get("radius_client")
@ -728,7 +728,7 @@ def validate_config(data):
"Dynamic VLAN assignment requires at least two VLANs."
)
# -- host_overrides validation ---------------------------------------------
# host_overrides validation =====================================
all_vlan_nets = list(vlan_networks.values())
for idx, entry in enumerate(data.get("host_overrides", [])):
lbl = f"host_overrides[{idx}] '{entry.get('host', '?')}'"
@ -744,7 +744,7 @@ def validate_config(data):
if all_vlan_nets and not any(ip_addr in net for net in all_vlan_nets):
errors.append(f"{lbl}: '{ip_str}' does not fall within any configured VLAN subnet.")
# -- banned_ips validation -------------------------------------------------
# banned_ips validation =========================================
for idx, entry in enumerate(data.get("banned_ips", [])):
ip_val = entry.get("ip", "")
lbl = f"banned_ips[{idx}] '{entry.get('description', '')}'"