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
}
}