diff --git a/NotchBuddy/Sources/App/NetPanel.swift b/NotchBuddy/Sources/App/NetPanel.swift index 303db09..8020ba2 100644 --- a/NotchBuddy/Sources/App/NetPanel.swift +++ b/NotchBuddy/Sources/App/NetPanel.swift @@ -23,6 +23,8 @@ struct NetClient: Identifiable, Equatable { var subnet: String var on: Bool var delay: Int? + /// What "off" means: REJECT (network unreachable) or DIRECT (home: you're there). + var offName: String = "REJECT" } @MainActor @@ -49,7 +51,10 @@ final class NetCore: ObservableObject { static let clientInfo: [String: (title: String, subnet: String)] = [ "saga": ("Сага", "192.168.8.0/24"), "planet9": ("Planet9", "192.168.68.0/24"), + "home": ("Дом", "192.168.10.0/24 · вне дома"), ] + /// Order in the panel. + static let clientOrder = ["home", "saga", "planet9"] // MARK: Root helper (netctl.py) // @@ -153,11 +158,12 @@ final class NetCore: ObservableObject { delay: delay) } } - clients = Self.clientInfo.keys.sorted().compactMap { id in - guard let g = proxies["\(id)-sw"] else { return nil } - let info = Self.clientInfo[id]! + clients = Self.clientOrder.compactMap { id in + guard let g = proxies["\(id)-sw"], let info = Self.clientInfo[id] else { return nil } + let all = g["all"] as? [String] ?? [] return NetClient(id: id, title: info.title, subnet: info.subnet, - on: (g["now"] as? String) == id, delay: lastDelay(id)) + on: (g["now"] as? String) == id, delay: lastDelay(id), + offName: all.first ?? "REJECT") } } @@ -191,7 +197,8 @@ final class NetCore: ObservableObject { func set(client id: String, on: Bool) async { if let i = clients.firstIndex(where: { $0.id == id }) { clients[i].on = on } - _ = await call(["cmd": "select", "group": "\(id)-sw", "name": on ? id : "REJECT"]) + let off = clients.first(where: { $0.id == id })?.offName ?? "REJECT" + _ = await call(["cmd": "select", "group": "\(id)-sw", "name": on ? id : off]) SoundEngine.shared.play(on ? "pop" : "close") if on, let m = (await call(["cmd": "delay", "proxy": id], timeout: 15))?["delays"] as? [String: Int] { for (k, v) in m { measured[k] = v } @@ -374,7 +381,7 @@ struct NetPanel: View { Task { await net.set(client: c.id, on: !c.on) } } } - Text("Выключенная сеть недоступна. Claude и интернет это не трогает.") + Text("Выключенная сеть клиента недоступна; «Дом» выключен — значит напрямую (ты дома). Claude и интернет это не трогает.") .font(.system(size: 10.5)).foregroundColor(Color(hex: "#6B7079")) .fixedSize(horizontal: false, vertical: true) .padding(.top, 4) diff --git a/scripts/netcore/gen.py b/scripts/netcore/gen.py index 3a15ab5..d4b5060 100755 --- a/scripts/netcore/gen.py +++ b/scripts/netcore/gen.py @@ -19,9 +19,14 @@ KEYS = os.path.join(HERE, "keys") CORE = HERE HOME_NET = "192.168.10.0/24" -CLIENTS = { # name: (conf file, routed subnets) - "saga": ("saga.conf", ["192.168.8.0/24", "10.0.0.0/24"]), - "planet9": ("planet9.conf", ["192.168.68.0/24", "172.3.3.0/24"]), +CLIENTS = { # name: (conf file, routed subnets, what "off" means) + # Client networks: off = REJECT (unreachable). Only the LANs are routed — their + # tunnel subnets overlap (Saga and home both use 10.0.0.x). + "saga": ("saga.conf", ["192.168.8.0/24"], "REJECT"), + "planet9": ("planet9.conf", ["192.168.68.0/24", "172.3.3.0/24"], "REJECT"), + # Home: off = DIRECT (you're at home, the LAN is right there); on = through the + # home WireGuard when away. Optional: only if src/home.conf exists. + "home": ("home.conf", ["192.168.10.0/24"], "DIRECT"), } AI_DOMAINS = ["anthropic.com", "claude.ai", "claude.com", "openai.com", "chatgpt.com", "oaistatic.com", "oaiusercontent.com", "github.com", "githubusercontent.com"] @@ -140,13 +145,15 @@ def main(): prov = os.path.join(KEYS, "amnezia.yaml") with open(os.open(prov, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), "w") as f: f.write(y({"proxies": awg}) + "\n") - proxies = [wg(n, f) for n, (f, _) in CLIENTS.items()] + clients = {n: c for n, c in CLIENTS.items() if os.path.exists(os.path.join(SRC, c[0]))} + proxies = [wg(n, f) for n, (f, _, _) in clients.items()] sub = open(os.path.join(SRC, "vless.sub")).read().strip() - rules = [f"IP-CIDR,{HOME_NET},DIRECT,no-resolve", "IP-CIDR,127.0.0.0/8,DIRECT,no-resolve", + rules = ([] if "home" in clients else [f"IP-CIDR,{HOME_NET},DIRECT,no-resolve"]) + [ + "IP-CIDR,127.0.0.0/8,DIRECT,no-resolve", # Home's own public IP (RustDesk, Gitea): never via a foreign exit. "IP-CIDR,79.111.14.0/32,DIRECT,no-resolve", "DOMAIN-SUFFIX,sanjeev.ru,DIRECT"] - for name, (_, nets) in CLIENTS.items(): + for name, (_, nets, _) in clients.items(): # Through a switch group: BroV turns client networks on/off without a reload. rules += [f"IP-CIDR,{n},{name}-sw,no-resolve" for n in nets] rules += [f"DOMAIN-SUFFIX,{d},ai-out" for d in AI_DOMAINS] @@ -188,7 +195,8 @@ def main(): "url": "https://www.gstatic.com/generate_204", "interval": 120, "tolerance": 100, "lazy": False}, ] + [ # Client networks: off (REJECT) until switched on in BroV. - {"name": f"{n}-sw", "type": "select", "proxies": ["REJECT", n]} for n in CLIENTS + # Client networks: first option = "off" (REJECT, or DIRECT for home). + {"name": f"{n}-sw", "type": "select", "proxies": [off, n]} for n, (_, _, off) in clients.items() ], "rules": rules, } diff --git a/scripts/netcore/install.sh b/scripts/netcore/install.sh index d697033..436f333 100644 --- a/scripts/netcore/install.sh +++ b/scripts/netcore/install.sh @@ -73,6 +73,13 @@ if [ -d "$OLD" ] && [ ! -L "$OLD" ]; then [ -f "$CORE/cache.db" ] || copy_regular "$OLD/cache.db" "$CORE/cache.db" copy_regular "$OLD/providers/vless.yaml" "$CORE/providers/vless.yaml" fi +# Extra WireGuard networks dropped into ~/.brov-secrets/wg/.conf (e.g. home.conf): +# moved into the root folder, the user copy is removed. +for f in "$USER_HOME"/.brov-secrets/wg/*.conf; do + [ -f "$f" ] && [ ! -L "$f" ] || continue + install -m 600 -o root -g wheel "$f" "$CORE/src/$(basename "$f")" && rm -f "$f" + echo "✓ WireGuard $(basename "$f" .conf) перенесён в ядро" +done ls "$CORE"/keys/*.vpnkey >/dev/null 2>&1 || { echo "✗ нет ни одного ключа Амнезии в $CORE/keys"; exit 1; } for f in saga.conf planet9.conf vless.sub; do [ -f "$CORE/src/$f" ] || { echo "✗ нет $CORE/src/$f"; exit 1; } diff --git a/scripts/netcore/netctl.py b/scripts/netcore/netctl.py index 505b4b2..8500129 100644 --- a/scripts/netcore/netctl.py +++ b/scripts/netcore/netctl.py @@ -7,8 +7,8 @@ to this helper over a Unix socket that only the installing user may open, and th allows exactly these operations: state groups, provider nodes with delays, TUN on/off - select {group, name} ai-out / amnezia / saga-sw / planet9-sw, name must be a member - delay {group}|{proxy} speed test of ai-out / amnezia, or of the saga / planet9 tunnel + select {group, name} ai-out / amnezia / saga-sw / planet9-sw / home-sw, name must be a member + delay {group}|{proxy} speed test of ai-out / amnezia, or of the saga / planet9 / home tunnel tun {on} traffic capture on/off add_key {text} vpn:// Amnezia key: checked by gen.py, stored, provider reloaded remove_key {name} "AWG " connection @@ -35,9 +35,9 @@ SOCK = "/var/run/brov-netctl.sock" API = "http://127.0.0.1:9097" TEST_URL = "https://www.gstatic.com/generate_204" -SELECT_GROUPS = {"ai-out", "amnezia", "saga-sw", "planet9-sw"} +SELECT_GROUPS = {"ai-out", "amnezia", "saga-sw", "planet9-sw", "home-sw"} DELAY_GROUPS = {"ai-out", "amnezia"} -CLIENT_TUNNELS = {"saga", "planet9"} +CLIENT_TUNNELS = {"saga", "planet9", "home"} MAX_REQUEST = 64 * 1024 ALLOWED_UID = int(sys.argv[1]) if len(sys.argv) > 1 else -1