Network panel: core on/off (TUN) toggle without a password, Amnezia conflict guard, install hint for the netd service

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-07 21:42:40 +03:00
parent 14114d1a58
commit 5a119d01a1
+41 -2
View File
@@ -35,6 +35,9 @@ final class NetCore: ObservableObject {
@Published var autoPick = "" // what "auto" chose
@Published var clients: [NetClient] = []
@Published var busy = false
/// TUN on = the core carries the Mac's traffic; off = idle (e.g. back on AmneziaVPN).
@Published var tunOn = false
@Published var lastError: String?
/// Delays measured by the group test (covers the subscription nodes too).
private var measured: [String: Int] = [:]
@@ -80,6 +83,9 @@ final class NetCore: ObservableObject {
return
}
running = true
if let cfg = await request("/configs") as? [String: Any], let tun = cfg["tun"] as? [String: Any] {
tunOn = tun["enable"] as? Bool ?? false
}
// Subscription (VLESS) nodes keep their history in the provider, not in /proxies.
var providerDelay: [String: Int] = [:]
if let p = await request("/providers/proxies/vless-cluster") as? [String: Any],
@@ -141,6 +147,26 @@ final class NetCore: ObservableObject {
await refresh()
}
static var amneziaRunning: Bool {
NSWorkspace.shared.runningApplications.contains {
($0.bundleIdentifier ?? "").lowercased().contains("amnezia") || ($0.localizedName ?? "").contains("Amnezia")
}
}
/// Turns traffic capture on/off without a password (the core keeps running).
func setTun(_ on: Bool) async {
if on && Self.amneziaRunning {
lastError = "Сначала выключи AmneziaVPN — иначе она и ядро подерутся за маршрут."
return
}
lastError = nil
tunOn = on
_ = await request("/configs", method: "PATCH", body: ["tun": ["enable": on]], timeout: 8)
SoundEngine.shared.play(on ? "pop" : "close")
try? await Task.sleep(for: .seconds(1))
await refresh()
}
static func pretty(_ name: String) -> (String, String) {
switch name {
case "auto": return ("Авто", "первый живой")
@@ -214,13 +240,26 @@ struct NetPanel: View {
}
}
if let err = net.lastError {
Text(err).font(.system(size: 11.5)).foregroundColor(Color(hex: "#FB923C"))
.fixedSize(horizontal: false, vertical: true).frame(width: 444, alignment: .leading)
}
if !net.running {
Text("Запусти ядро: в Терминале `~/Documents/brov-secrets/start.sh` (AmneziaVPN при этом выключи). Скоро это будет делать сам BroV.")
Text("Ядро не отвечает. Если служба ещё не установлена — выключи AmneziaVPN и один раз выполни в Терминале:\nsudo sh ~/Documents/brov-secrets/install-netd.sh\nДальше ядро будет запускаться само при включении Мака.")
.font(.system(size: 11.5))
.foregroundColor(Color(hex: "#B0B5BE"))
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
.frame(width: 440, alignment: .leading)
.frame(width: 444, alignment: .leading)
} else {
NetRow(title: "Ядро перехватывает трафик",
subtitle: net.tunOn ? "весь трафик Мака идёт через BroV" : "выключено — Мак ходит сам (можно включить AmneziaVPN)",
delay: nil, isOn: net.tunOn, accent: "#A78BFA") {
Task { await net.setTun(!net.tunOn) }
}
.frame(width: 444)
Rectangle().fill(Color.white.opacity(0.08)).frame(height: 1)
HStack(alignment: .top, spacing: 14) {
VStack(alignment: .leading, spacing: 4) {
Text("ВЫХОД В ИНТЕРНЕТ").font(.system(size: 9.5, weight: .bold)).foregroundColor(Color(hex: "#6B7079"))