diff --git a/api/app.py b/api/app.py index 31a3e08..62bcbe5 100644 --- a/api/app.py +++ b/api/app.py @@ -435,6 +435,25 @@ def update_config(): # Handle identity fields (cell_name, domain, ip_range, wireguard_port) identity_keys = {'cell_name', 'domain', 'ip_range', 'wireguard_port'} identity_updates = {k: v for k, v in data.items() if k in identity_keys} + + # Validate ip_range — must be a valid CIDR within an RFC-1918 range + if 'ip_range' in identity_updates: + import ipaddress as _ipa + _rfc1918 = [ + _ipa.ip_network('10.0.0.0/8'), + _ipa.ip_network('172.16.0.0/12'), + _ipa.ip_network('192.168.0.0/16'), + ] + try: + _net = _ipa.ip_network(identity_updates['ip_range'], strict=False) + if not any(_net.subnet_of(r) for r in _rfc1918): + return jsonify({'error': ( + 'ip_range must be within an RFC-1918 private range ' + '(10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16)' + )}), 400 + except ValueError as _e: + return jsonify({'error': f'Invalid ip_range: {_e}'}), 400 + # Capture old identity and service configs BEFORE saving, for change detection old_identity = dict(config_manager.configs.get('_identity', {})) old_svc_configs = { diff --git a/webui/src/pages/Settings.jsx b/webui/src/pages/Settings.jsx index 7223b9f..acef2e3 100644 --- a/webui/src/pages/Settings.jsx +++ b/webui/src/pages/Settings.jsx @@ -69,14 +69,39 @@ function Section({ icon: Icon, title, children, collapsible = false, defaultOpen ); } +// ── RFC-1918 validation ─────────────────────────────────────────────────────── + +function isRFC1918Cidr(cidr) { + if (!cidr || !cidr.trim()) return false; + const m = cidr.trim().match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/); + if (!m) return false; + const [, a, b, c, d, p] = m.map(Number); + if ([a, b, c, d].some(n => n < 0 || n > 255) || p < 0 || p > 32) return false; + const ip = ((a << 24) | (b << 16) | (c << 8) | d) >>> 0; + const ranges = [ + { net: 0x0a000000, prefix: 8 }, // 10.0.0.0/8 + { net: 0xac100000, prefix: 12 }, // 172.16.0.0/12 + { net: 0xc0a80000, prefix: 16 }, // 192.168.0.0/16 + ]; + for (const { net, prefix } of ranges) { + if (p < prefix) continue; + const mask = (0xffffffff << (32 - prefix)) >>> 0; + if ((ip & mask) >>> 0 === (net & mask) >>> 0) return true; + } + return false; +} + // ── Field components ────────────────────────────────────────────────────────── -function Field({ label, children, hint }) { +function Field({ label, children, hint, error }) { return ( -
{error}
} +