"""Input validation helpers for settings that end up in generated OpenVPN configs.""" import ipaddress import os import re SCRIPTS_DIR = "/etc/openvpn/scripts" SCRIPT_RE = re.compile(r"^/etc/openvpn/scripts/[A-Za-z0-9_.-]{1,64}$") HOSTNAME_RE = re.compile( r"^(?=.{1,253}$)([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$" ) FORBIDDEN_CHARS = set('\n\r"\\\x00') def assert_safe_scalar(name: str, value) -> None: """Reject values that could break out of a config line (newlines, quotes, backslashes, NUL).""" if isinstance(value, (list, tuple)): for item in value: assert_safe_scalar(name, item) return if isinstance(value, str) and (FORBIDDEN_CHARS & set(value) or any(ord(c) < 32 for c in value)): raise ValueError(f"Unsafe characters in '{name}'") def valid_netmask(mask: str) -> bool: try: ipaddress.IPv4Network(f"0.0.0.0/{mask}") return True except ValueError: return False def valid_route(route: str) -> bool: """'a.b.c.d/nn' or 'a.b.c.d m.m.m.m'.""" parts = route.split() try: if len(parts) == 1: ipaddress.IPv4Network(parts[0], strict=False) return True if len(parts) == 2: ipaddress.IPv4Address(parts[0]) return valid_netmask(parts[1]) except ValueError: pass return False def valid_host(value: str) -> bool: try: ipaddress.ip_address(value) return True except ValueError: return bool(HOSTNAME_RE.match(value)) def check_script(path: str) -> None: """A connect/disconnect script must be a root-owned, non-writable file inside SCRIPTS_DIR.""" if not path: return if not SCRIPT_RE.match(path): raise ValueError(f"Script path must match {SCRIPTS_DIR}/") real = os.path.realpath(path) if os.path.dirname(real) != SCRIPTS_DIR: raise ValueError("Script must reside directly in " + SCRIPTS_DIR) if not os.path.isfile(real): raise ValueError("Script file does not exist") st = os.stat(real) if st.st_uid != 0 or st.st_mode & 0o022: raise ValueError("Script must be owned by root and not writable by group/others") dst = os.stat(SCRIPTS_DIR) if dst.st_uid != 0 or dst.st_mode & 0o022: raise ValueError(SCRIPTS_DIR + " must be owned by root and not writable by group/others")