// VPNProvider.swift // Jarvis — remote-access providers. The app can't start another app's VPN tunnel // (iOS sandboxing), so the provider only tailors hints, help text, and an // "open the app" jump. Reachability + endpoint selection is provider-agnostic. import UIKit enum VPNProvider: String, CaseIterable, Codable, Identifiable { case direct // LAN only, no VPN case tailscale case headscale // self-hosted control plane, Tailscale clients case netbird case zerotier case netmaker case wireguard var id: String { rawValue } var displayName: String { switch self { case .direct: return "Direct (LAN)" case .tailscale: return "Tailscale" case .headscale: return "Headscale" case .netbird: return "NetBird" case .zerotier: return "ZeroTier" case .netmaker: return "Netmaker" case .wireguard: return "WireGuard" } } /// Whether a separate remote/VPN address is meaningful for this provider. var needsRemote: Bool { self != .direct } /// Placeholder shown in the remote-address field. var remoteHint: String { switch self { case .direct: return "—" case .tailscale: return "jarvis.tailnet.ts.net:8080 or 100.x.y.z:8080" case .headscale: return "100.x.y.z:8080 (MagicDNS or tailnet IP)" case .netbird: return "100.x.y.z:8080" case .zerotier: return "10.x.x.x:8080 (managed IP)" case .netmaker: return "10.x.x.x:8080" case .wireguard: return "10.0.0.x:8080 (peer IP)" } } var blurb: String { switch self { case .direct: return "Use the LAN address only. Best when the phone is always on the home network." case .tailscale: return "On your home wifi Jarvis uses the LAN address directly; away, it uses your Tailscale address. Make sure Tailscale is connected when remote." case .headscale: return "Self-hosted Tailscale control plane. Same client behaviour as Tailscale — keep the client connected when away." case .netbird: return "Open-source WireGuard mesh. Keep the NetBird app connected when off the LAN." case .zerotier: return "Managed network via ZeroTier. Keep the ZeroTier app joined to the network when remote." case .netmaker: return "Self-hosted WireGuard mesh. Keep your Netmaker client up when off the LAN." case .wireguard: return "Plain WireGuard tunnel. Enable the tunnel in the WireGuard app when remote." } } /// Best-effort URL scheme + App Store fallback used by "Open ". /// iOS can't start the tunnel for us — this just brings the app forward. private var urlScheme: URL? { switch self { case .tailscale, .headscale: return URL(string: "tailscale://") case .zerotier: return URL(string: "zerotier://") case .wireguard: return URL(string: "wireguard://") default: return nil } } private var appStoreURL: URL? { switch self { case .tailscale, .headscale: return URL(string: "https://apps.apple.com/app/id1470499037") case .zerotier: return URL(string: "https://apps.apple.com/app/id1084101492") case .wireguard: return URL(string: "https://apps.apple.com/app/id1441195209") case .netbird: return URL(string: "https://apps.apple.com/app/id6443155627") default: return nil } } var canOpenApp: Bool { urlScheme != nil || appStoreURL != nil } /// Try the app's scheme; fall back to its App Store page. @MainActor func openApp() { let candidates = [urlScheme, appStoreURL].compactMap { $0 } func attempt(_ i: Int) { guard i < candidates.count else { return } UIApplication.shared.open(candidates[i]) { ok in if !ok { attempt(i + 1) } } } attempt(0) } }