diff --git a/Yggdrasil Network Cross-Platform/ConfigurationProxy.swift b/Yggdrasil Network Cross-Platform/ConfigurationProxy.swift index 6cd581b..e193278 100644 --- a/Yggdrasil Network Cross-Platform/ConfigurationProxy.swift +++ b/Yggdrasil Network Cross-Platform/ConfigurationProxy.swift @@ -32,9 +32,14 @@ class PlatformItemSource: NSObject {} class ConfigurationProxy: PlatformItemSource { private var manager: NETunnelProviderManager? - private var json: Data? = nil + private var json: Data? = nil { + didSet { + summary = MobileSummaryFromConfig(json) + } + } private var dict: [String: Any]? = [:] private var timer: Timer? + public var summary: MobileParsedConfig? init(manager: NETunnelProviderManager) { self.manager = manager @@ -46,11 +51,11 @@ class ConfigurationProxy: PlatformItemSource { } catch { NSLog("ConfigurationProxy: Error deserialising JSON (\(error))") } - #if os(iOS) +#if os(iOS) self.set("name", inSection: "NodeInfo", to: UIDevice.current.name) - #elseif os(OSX) +#elseif os(OSX) self.set("name", inSection: "NodeInfo", to: Host.current().localizedName ?? "") - #endif +#endif self.fix() } @@ -131,16 +136,16 @@ class ConfigurationProxy: PlatformItemSource { self.saveSoon() } } - - public var autoStartAny: Bool { - get { - return self.get("Any", inSection: "AutoStart") as? Bool ?? false - } - set { - self.set("Any", inSection: "AutoStart", to: newValue) - self.saveSoon() - } - } + + public var autoStartAny: Bool { + get { + return self.get("Any", inSection: "AutoStart") as? Bool ?? false + } + set { + self.set("Any", inSection: "AutoStart", to: newValue) + self.saveSoon() + } + } public var autoStartWiFi: Bool { get { @@ -303,29 +308,29 @@ class ConfigurationProxy: PlatformItemSource { wifirule.interfaceTypeMatch = .any rules.insert(wifirule, at: 0) } - #if os(macOS) +#if os(macOS) if self.get("Ethernet", inSection: "AutoStart") as? Bool ?? false { let wifirule = NEOnDemandRuleConnect() wifirule.interfaceTypeMatch = .ethernet rules.insert(wifirule, at: 0) } - #endif +#endif if self.get("WiFi", inSection: "AutoStart") as? Bool ?? false { let wifirule = NEOnDemandRuleConnect() wifirule.interfaceTypeMatch = .wiFi rules.insert(wifirule, at: 0) } - #if canImport(UIKit) +#if canImport(UIKit) if self.get("Mobile", inSection: "AutoStart") as? Bool ?? false { let mobilerule = NEOnDemandRuleConnect() mobilerule.interfaceTypeMatch = .cellular rules.insert(mobilerule, at: 0) } - #endif +#endif manager.onDemandRules = rules manager.isOnDemandEnabled = rules.count > 1 providerProtocol.disconnectOnSleep = rules.count > 1 - + manager.protocolConfiguration = providerProtocol manager.saveToPreferences(completionHandler: { error in @@ -345,8 +350,8 @@ class ConfigurationProxy: PlatformItemSource { private func convertToJson() throws { self.json = try JSONSerialization.data(withJSONObject: self.dict as Any, options: .prettyPrinted) } - - #if canImport(UIKit) + +#if canImport(UIKit) override func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { return "yggdrasil.conf" } @@ -361,5 +366,5 @@ class ConfigurationProxy: PlatformItemSource { } return "yggdrasil.conf.json" } - #endif +#endif } diff --git a/Yggdrasil Network Cross-Platform/CrossPlatformAppDelegate.swift b/Yggdrasil Network Cross-Platform/CrossPlatformAppDelegate.swift index 311d1b7..50c79eb 100644 --- a/Yggdrasil Network Cross-Platform/CrossPlatformAppDelegate.swift +++ b/Yggdrasil Network Cross-Platform/CrossPlatformAppDelegate.swift @@ -72,7 +72,7 @@ class CrossPlatformAppDelegate: PlatformAppDelegate, ObservableObject { @Published var yggdrasilIP: String = "N/A" @Published var yggdrasilSubnet: String = "N/A" @Published var yggdrasilCoords: String = "[]" - + @Published var yggdrasilPeers: [YggdrasilPeer] = [] func yggdrasilVersion() -> String { @@ -108,11 +108,12 @@ class CrossPlatformAppDelegate: PlatformAppDelegate, ObservableObject { func becameBackground() {} func updateStatus(conn: NEVPNConnection) { - if conn.status == .connected { + if conn.status == .connected || conn.status == .connecting { + self.yggdrasilEnabled = true self.requestSummaryIPC() } else if conn.status == .disconnecting || conn.status == .disconnected { - self.clearStatus() self.yggdrasilEnabled = false + self.clearStatus() } } @@ -188,6 +189,8 @@ class CrossPlatformAppDelegate: PlatformAppDelegate, ObservableObject { self.yggdrasilPublicKey = summary.publicKey self.yggdrasilPeers = summary.peers self.yggdrasilConnected = summary.peers.filter { $0.up }.count > 0 + + print("Response: \(String(data: js, encoding: .utf8))") } } } diff --git a/Yggdrasil Network Cross-Platform/IPCResponses.swift b/Yggdrasil Network Cross-Platform/IPCResponses.swift index 774a107..fa0d3c6 100644 --- a/Yggdrasil Network Cross-Platform/IPCResponses.swift +++ b/Yggdrasil Network Cross-Platform/IPCResponses.swift @@ -32,6 +32,9 @@ struct YggdrasilPeer: Codable, Identifiable { let key: String? let priority: UInt8 let cost: UInt16? + let rxBytes: Double? + let txBytes: Double? + let uptime: Int64? enum CodingKeys: String, CodingKey { case remote = "URI" @@ -40,6 +43,9 @@ struct YggdrasilPeer: Codable, Identifiable { case key = "Key" case priority = "Priority" case cost = "Cost" + case rxBytes = "RXBytes" + case txBytes = "TXBytes" + case uptime = "Uptime" } public func getStatusBadgeColor() -> SwiftUI.Color { diff --git a/Yggdrasil Network Extension/PacketTunnelProvider.swift b/Yggdrasil Network Extension/PacketTunnelProvider.swift index ebeeb1e..901a543 100644 --- a/Yggdrasil Network Extension/PacketTunnelProvider.swift +++ b/Yggdrasil Network Extension/PacketTunnelProvider.swift @@ -3,13 +3,13 @@ import Foundation import Yggdrasil class PacketTunnelProvider: NEPacketTunnelProvider { - + var yggdrasil: MobileYggdrasil = MobileYggdrasil() var yggdrasilConfig: ConfigurationProxy? - + func startYggdrasil() -> Error? { var err: Error? = nil - + self.setTunnelNetworkSettings(nil) { (error: Error?) -> Void in NSLog("Starting Yggdrasil") @@ -30,7 +30,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { NSLog("Starting Yggdrasil process produced an error: " + error.localizedDescription) return } - + let address = self.yggdrasil.getAddressString() let subnet = self.yggdrasil.getSubnetString() @@ -41,7 +41,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { tunnelNetworkSettings.ipv6Settings = NEIPv6Settings(addresses: [address], networkPrefixLengths: [7]) tunnelNetworkSettings.ipv6Settings?.includedRoutes = [NEIPv6Route(destinationAddress: "0200::", networkPrefixLength: 7)] tunnelNetworkSettings.mtu = NSNumber(integerLiteral: self.yggdrasil.getMTU()) - + NSLog("Setting tunnel network settings...") self.setTunnelNetworkSettings(tunnelNetworkSettings) { (error: Error?) -> Void in @@ -67,7 +67,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } return err } - + override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { if let conf = (self.protocolConfiguration as! NETunnelProviderProtocol).providerConfiguration { if let json = conf["json"] as? Data { @@ -88,7 +88,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } } - + override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { try? self.yggdrasil.stop() super.stopTunnel(with: reason, completionHandler: completionHandler) @@ -99,6 +99,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { switch request { case "summary": let pj = self.yggdrasil.getPeersJSON() + NSLog("JSON: \(pj)") var peers: [YggdrasilPeer] = [] do { peers = try JSONDecoder().decode( @@ -118,7 +119,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { if let json = try? JSONEncoder().encode(summary) { completionHandler?(json) } - + default: completionHandler?(nil) } diff --git a/Yggdrasil.xcframework/Info.plist b/Yggdrasil.xcframework/Info.plist index bc7e6bb..f271b9e 100644 --- a/Yggdrasil.xcframework/Info.plist +++ b/Yggdrasil.xcframework/Info.plist @@ -5,34 +5,8 @@ AvailableLibraries - LibraryIdentifier - macos-arm64_x86_64 - LibraryPath - Yggdrasil.framework - SupportedArchitectures - - arm64 - x86_64 - - SupportedPlatform - macos - - - LibraryIdentifier - ios-arm64_x86_64-maccatalyst - LibraryPath - Yggdrasil.framework - SupportedArchitectures - - arm64 - x86_64 - - SupportedPlatform - ios - SupportedPlatformVariant - maccatalyst - - + BinaryPath + Yggdrasil.framework/Versions/A/Yggdrasil LibraryIdentifier ios-arm64 LibraryPath @@ -45,6 +19,8 @@ ios + BinaryPath + Yggdrasil.framework/Versions/A/Yggdrasil LibraryIdentifier ios-arm64_x86_64-simulator LibraryPath @@ -59,6 +35,21 @@ SupportedPlatformVariant simulator + + BinaryPath + Yggdrasil.framework/Versions/A/Yggdrasil + LibraryIdentifier + macos-arm64_x86_64 + LibraryPath + Yggdrasil.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + macos + CFBundlePackageType XFWK diff --git a/YggdrasilSwiftUI/Application.swift b/YggdrasilSwiftUI/Application.swift index 038bfb7..677ffe2 100644 --- a/YggdrasilSwiftUI/Application.swift +++ b/YggdrasilSwiftUI/Application.swift @@ -12,11 +12,11 @@ import NetworkExtension struct Application: App { @State private var selection: String? = "Status" - #if os(iOS) +#if os(iOS) @UIApplicationDelegateAdaptor(CrossPlatformAppDelegate.self) static var appDelegate: CrossPlatformAppDelegate - #elseif os(macOS) +#elseif os(macOS) @NSApplicationDelegateAdaptor(CrossPlatformAppDelegate.self) static var appDelegate: CrossPlatformAppDelegate - #endif +#endif @Environment(\.scenePhase) var scenePhase @@ -53,14 +53,14 @@ struct Application: App { //.listStyle(.sidebar) //.navigationSplitViewColumnWidth(200) - Image("YggdrasilLogo") - .renderingMode(.template) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.primary) - .opacity(0.1) - .frame(maxWidth: 200, alignment: .bottom) - .padding(.all, 24) + /*Image("YggdrasilLogo") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.primary) + .opacity(0.05) + .frame(maxWidth: 200, alignment: .bottom) + .padding(.all, 24)*/ } .navigationSplitViewColumnWidth(200) .listStyle(.sidebar) @@ -82,8 +82,8 @@ struct Application: App { } } } - #if os(macOS) +#if os(macOS) .windowStyle(.hiddenTitleBar) - #endif +#endif } } diff --git a/YggdrasilSwiftUI/PeersView.swift b/YggdrasilSwiftUI/PeersView.swift index 3cbf742..a4f809f 100644 --- a/YggdrasilSwiftUI/PeersView.swift +++ b/YggdrasilSwiftUI/PeersView.swift @@ -18,7 +18,7 @@ struct PeersView: View { Section(content: { ForEach(Array(appDelegate.yggdrasilConfig.peers.enumerated()), id: \.offset) { index, peer in HStack() { - TextField("Peer", text: $appDelegate.yggdrasilConfig.peers[index]) + TextField("tls://host:port", text: $appDelegate.yggdrasilConfig.peers[index]) .labelsHidden() #if os(iOS) .disabled(!editMode!.wrappedValue.isEditing) @@ -75,8 +75,18 @@ struct PeersView: View { .foregroundColor(.gray) } } - TextField("Multicast password", text: $appDelegate.yggdrasilConfig.multicastPassword) - .labelStyle(.titleAndIcon) + VStack { + HStack { + Text("Multicast password") + TextField("None", text: $appDelegate.yggdrasilConfig.multicastPassword) + .labelsHidden() + .multilineTextAlignment(.trailing) + } + Text("If provided, this device will only automatically peer with other nodes that share the same password.") + .font(.system(size: 11)) + .foregroundColor(.gray) + .frame(maxWidth: .infinity, alignment: .leading) + } }, header: { Text("Local connectivity") }) @@ -85,8 +95,10 @@ struct PeersView: View { .navigationTitle("Peers") #if os(iOS) .toolbar { - Button("Add", systemImage: "plus") { - appDelegate.yggdrasilConfig.peers.append("") + if editMode!.wrappedValue.isEditing { + Button("Add", systemImage: "plus") { + appDelegate.yggdrasilConfig.peers.append("") + } } EditButton() .onChange(of: editMode!.wrappedValue) { edit in diff --git a/YggdrasilSwiftUI/SettingsView.swift b/YggdrasilSwiftUI/SettingsView.swift index be25d7a..624b764 100644 --- a/YggdrasilSwiftUI/SettingsView.swift +++ b/YggdrasilSwiftUI/SettingsView.swift @@ -8,7 +8,6 @@ import SwiftUI struct SettingsView: View { - //@Binding public var yggdrasilConfiguration: ConfigurationProxy @ObservedObject private var appDelegate = Application.appDelegate @State private var deviceName = "" @@ -35,55 +34,55 @@ struct SettingsView: View { }) /* - Section(content: { - VStack(alignment: .leading) { - Button("Import configuration") { - - } - #if os(macOS) - .buttonStyle(.link) - #endif - .foregroundColor(.accentColor) - Text("Import configuration from another device, including the public key and Yggdrasil IP address.") - .font(.system(size: 11)) - .foregroundColor(.gray) - } - - VStack(alignment: .leading) { - Button("Export configuration") { - - } - #if os(macOS) - .buttonStyle(.link) - #endif - .foregroundColor(.accentColor) - Text("Configuration will be exported as a file. Your configuration contains your private key which is extremely sensitive. Do not share it with anyone.") - .font(.system(size: 11)) - .foregroundColor(.gray) - } - - VStack(alignment: .leading) { - Button("Reset configuration") { - - } - #if os(macOS) - .buttonStyle(.link) - #endif - .foregroundColor(.red) - Text("Resetting will overwrite with newly generated configuration. Your public key and Yggdrasil IP address will change.") - .font(.system(size: 11)) - .foregroundColor(.gray) - } - }, header: { - Text("Configuration") - }) + Section(content: { + VStack(alignment: .leading) { + Button("Import configuration") { + + } + #if os(macOS) + .buttonStyle(.link) + #endif + .foregroundColor(.accentColor) + Text("Import configuration from another device, including the public key and Yggdrasil IP address.") + .font(.system(size: 11)) + .foregroundColor(.gray) + } + + VStack(alignment: .leading) { + Button("Export configuration") { + + } + #if os(macOS) + .buttonStyle(.link) + #endif + .foregroundColor(.accentColor) + Text("Configuration will be exported as a file. Your configuration contains your private key which is extremely sensitive. Do not share it with anyone.") + .font(.system(size: 11)) + .foregroundColor(.gray) + } + + VStack(alignment: .leading) { + Button("Reset configuration") { + + } + #if os(macOS) + .buttonStyle(.link) + #endif + .foregroundColor(.red) + Text("Resetting will overwrite with newly generated configuration. Your public key and Yggdrasil IP address will change.") + .font(.system(size: 11)) + .foregroundColor(.gray) + } + }, header: { + Text("Configuration") + }) */ } .formStyle(.grouped) .navigationTitle("Settings") - #if os(iOS) +#if os(iOS) .navigationBarTitleDisplayMode(.large) - #endif +#endif } } diff --git a/YggdrasilSwiftUI/StatusView.swift b/YggdrasilSwiftUI/StatusView.swift index 7a8b552..853d1df 100644 --- a/YggdrasilSwiftUI/StatusView.swift +++ b/YggdrasilSwiftUI/StatusView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CoreImage.CIFilterBuiltins #if os(iOS) typealias MyListStyle = DefaultListStyle @@ -18,16 +19,19 @@ struct StatusView: View { @State private var statusBadgeColor: SwiftUI.Color = .gray @State private var statusBadgeText: String = "Not enabled" + @State private var showingPublicKeyPopover = false + + let context = CIContext() + let filter = CIFilter.qrCodeGenerator() private func getStatusBadgeColor() -> SwiftUI.Color { - if !appDelegate.yggdrasilSupported { - return .gray - } else if appDelegate.yggdrasilConnected { + if appDelegate.yggdrasilConnected { return .green } else if appDelegate.yggdrasilEnabled { return .yellow + } else { + return .gray } - return .gray } private func getStatusBadgeText() -> String { @@ -42,6 +46,40 @@ struct StatusView: View { } } + func formatBytes(bytes: Double) -> String { + guard bytes > 0 else { + return "N/A" + } + + // Adapted from http://stackoverflow.com/a/18650828 + let suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + let k: Double = 1024 + let i = floor(log(bytes) / log(k)) + + // Format number with thousands separator and everything below 1 GB with no decimal places. + let numberFormatter = NumberFormatter() + numberFormatter.maximumFractionDigits = i < 3 ? 0 : 1 + numberFormatter.numberStyle = .decimal + + let numberString = numberFormatter.string(from: NSNumber(value: bytes / pow(k, i))) ?? "Unknown" + let suffix = suffixes[Int(i)] + return "\(numberString) \(suffix)" + } + +#if os(iOS) + func generateQRCode(from string: String) -> UIImage { + filter.message = Data(string.utf8) + + if let outputImage = filter.outputImage { + if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { + return UIImage(cgImage: cgImage) + } + } + + return UIImage(systemName: "xmark.circle") ?? UIImage() + } +#endif + var body: some View { Form { Section(content: { @@ -125,6 +163,25 @@ struct StatusView: View { .truncationMode(.tail) .lineLimit(1) .textSelection(.enabled) +#if os(iOS) + if appDelegate.yggdrasilPublicKey != "N/A" { + Button("QR Code", systemImage: "qrcode", action: { + showingPublicKeyPopover = true + }) + .labelStyle(.iconOnly) + .popover(isPresented: $showingPublicKeyPopover) { + Text("Public Key") + .font(.headline) + .padding() + Image(uiImage: generateQRCode(from: "\(appDelegate.yggdrasilPublicKey)")) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 200, height: 200) + .padding() + } + } +#endif } }, header: { Text("Details") @@ -146,25 +203,48 @@ struct StatusView: View { return a.remote < b.remote }), id: \.remote) { peer in VStack { - Text(peer.remote) - .frame(maxWidth: .infinity, alignment: .leading) - .truncationMode(.tail) - .lineLimit(1) - .textSelection(.enabled) - .padding(.bottom, 2) + HStack { + Text(peer.remote) + .frame(maxWidth: .infinity, alignment: .leading) + .truncationMode(.tail) + .lineLimit(1) + .textSelection(.enabled) + .padding(.bottom, 2) + } HStack { Image(systemName: "circlebadge.fill") .foregroundColor(peer.getStatusBadgeColor()) .onChange(of: peer.up) { newValue in statusBadgeColor = peer.getStatusBadgeColor() } - Text(peer.up ? peer.address ?? "Unknown IP address" : "Not connected") + Text(peer.up ? "Connected" : "Not connected") .frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(Color.gray) .font(.system(size: 11)) .truncationMode(.tail) .lineLimit(1) .textSelection(.enabled) + if peer.up { + Spacer() + if let uptime = peer.uptime { + Label(Duration(secondsComponent: uptime/1000000000, attosecondsComponent: 0).formatted(), systemImage: "clock") + .font(.system(size: 11)) + .labelStyle(.titleAndIcon) + .foregroundStyle(.secondary) + } + if let rxBytes = peer.rxBytes { + Label(formatBytes(bytes: rxBytes), systemImage: "arrowshape.down") + .font(.system(size: 11)) + .labelStyle(.titleAndIcon) + .foregroundStyle(.teal) + } + if let txBytes = peer.txBytes { + Label(formatBytes(bytes: txBytes), systemImage: "arrowshape.up") + .font(.system(size: 11)) + .labelStyle(.titleAndIcon) + .foregroundStyle(.purple) + } + } } } .padding(.all, 2) @@ -178,9 +258,9 @@ struct StatusView: View { } .formStyle(.grouped) .navigationTitle("Yggdrasil") - #if os(iOS) +#if os(iOS) .navigationBarTitleDisplayMode(.large) - #endif +#endif } }