#!/usr/bin/python3 -I
"""Root-owned, fixed-purpose TUN bridge. Never loads user-supplied core JSON."""
import argparse
import ctypes
import fcntl
import ipaddress
import json
import os
import select
import shutil
import signal
import socket
import stat
import subprocess
import sys
import tempfile
import time

BINARY = "/opt/lumenvpn/bin/sing-box"

def child_watchdog():
    # If this privileged helper is killed, sing-box still receives SIGINT and
    # removes its own routes/nftables rules. It does not become an orphan daemon.
    parent = os.getppid()
    if ctypes.CDLL(None, use_errno=True).prctl(1, signal.SIGINT, 0, 0, 0) != 0:
        os._exit(1)
    if os.getppid() != parent:
        os.kill(os.getpid(), signal.SIGINT)

def interrupted(_signum, _frame):
    raise KeyboardInterrupt

def addresses(value):
    parts = value.split(",")
    if not 1 <= len(parts) <= 256:
        raise ValueError("address limit")
    result = []
    for part in parts:
        address = ipaddress.ip_address(part)
        if address.is_unspecified or address.is_multicast:
            raise ValueError("invalid endpoint")
        result.append(str(address) + ("/32" if address.version == 4 else "/128"))
    return result

def configuration(port, endpoints, dns="1.1.1.1"):
    if not 1024 <= port <= 65535:
        raise ValueError("port limit")
    dns_address = addresses(dns)
    if len(dns_address) > 8:
        raise ValueError("DNS limit")
    return {
        "log": {"level": "error"},
        "dns": {"servers": [{"type": "udp", "tag": "remote", "server": dns_address[0].split("/")[0], "detour": "lumen-socks"}], "final": "remote"},
        "inbounds": [{"type": "tun", "tag": "lumen-tun", "interface_name": "lumenVPN",
                      "address": ["172.31.253.1/30", "fd31:253::1/126"], "mtu": 1500,
                      "auto_route": True, "auto_redirect": True, "strict_route": True,
                      "dns_mode": "hijack", "route_exclude_address": addresses(endpoints)}],
        "outbounds": [{"type": "socks", "tag": "lumen-socks", "server": "127.0.0.1", "server_port": port,
                       "version": "5", "bind_interface": "lo"}],
        "route": {"rules": [{"port": 53, "action": "hijack-dns"}], "final": "lumen-socks"}
    }

def owner_stamp(pid, uid):
    path = "/proc/" + str(pid)
    if os.stat(path).st_uid != uid:
        raise ValueError("owner mismatch")
    with open(path + "/stat", encoding="ascii") as stream:
        value = stream.read(8192)
    return value[value.rfind(")") + 2:].split()[19]

def trusted_binary():
    # Do not run a replaced core or a core under a user-writable parent as root.
    for path in ["/opt", "/opt/lumenvpn", "/opt/lumenvpn/bin", BINARY]:
        info = os.lstat(path)
        if info.st_uid != 0 or info.st_mode & (stat.S_IWGRP | stat.S_IWOTH) or stat.S_ISLNK(info.st_mode):
            raise ValueError("untrusted package path")

def main():
    signal.signal(signal.SIGTERM, interrupted)
    signal.signal(signal.SIGINT, interrupted)
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("--port", type=int, required=True)
    parser.add_argument("--owner-pid", type=int, required=True)
    parser.add_argument("--endpoints", required=True)
    parser.add_argument("--dns", default="1.1.1.1")
    args = parser.parse_args()
    uid = int(os.environ.get("PKEXEC_UID", "0"))
    if os.geteuid() != 0 or uid < 1 or args.owner_pid < 2:
        raise ValueError("authorization required")
    stamp = owner_stamp(args.owner_pid, uid)
    trusted_binary()
    config = configuration(args.port, args.endpoints, args.dns)
    lock = os.open("/run/lumenvpn-tun.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
    fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
    if any(name == "lumenVPN" for _, name in socket.if_nameindex()):
        raise ValueError("existing TUN")
    directory = tempfile.mkdtemp(prefix="lumenvpn-tun-", dir="/run")
    process = None
    try:
        path = directory + "/config.json"
        with open(path, "x", encoding="utf-8") as stream:
            json.dump(config, stream)
        os.chmod(path, 0o600)
        process = subprocess.Popen([BINARY, "run", "-c", path], stdin=subprocess.DEVNULL,
                                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                                   preexec_fn=child_watchdog,
                                   env={"PATH": "/usr/sbin:/usr/bin:/sbin:/bin", "HOME": directory})
        for _ in range(100):
            if process.poll() is not None:
                raise ValueError("TUN failed")
            if any(name == "lumenVPN" for _, name in socket.if_nameindex()):
                break
            time.sleep(0.1)
        else:
            raise ValueError("TUN timeout")
        print("TUN_READY", flush=True)
        last_ping = time.monotonic()
        while process.poll() is None and time.monotonic() - last_ping < 15:
            if owner_stamp(args.owner_pid, uid) != stamp:
                break
            if select.select([sys.stdin], [], [], 1)[0]:
                line = sys.stdin.readline(64)
                if line != "ping\n":
                    break
                last_ping = time.monotonic()
    finally:
        if process is not None and process.poll() is None:
            process.send_signal(signal.SIGINT)
            try:
                process.wait(timeout=10)
            except subprocess.TimeoutExpired:
                process.kill()
                process.wait()
        shutil.rmtree(directory)
        os.close(lock)

if __name__ == "__main__":
    try:
        main()
    except (Exception, KeyboardInterrupt):
        # Never print supplied values, paths, keys or the privileged core's logs.
        print("TUN_ERROR", flush=True)
        sys.exit(1)
