This commit is contained in:
Neil Alexander 2024-06-23 10:54:21 +01:00
parent 0bea3a1613
commit 1b9e84d056
No known key found for this signature in database
GPG key ID: A02A2019A2BB0944
9 changed files with 234 additions and 137 deletions

View file

@ -32,9 +32,14 @@ class PlatformItemSource: NSObject {}
class ConfigurationProxy: PlatformItemSource { class ConfigurationProxy: PlatformItemSource {
private var manager: NETunnelProviderManager? 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 dict: [String: Any]? = [:]
private var timer: Timer? private var timer: Timer?
public var summary: MobileParsedConfig?
init(manager: NETunnelProviderManager) { init(manager: NETunnelProviderManager) {
self.manager = manager self.manager = manager
@ -46,11 +51,11 @@ class ConfigurationProxy: PlatformItemSource {
} catch { } catch {
NSLog("ConfigurationProxy: Error deserialising JSON (\(error))") NSLog("ConfigurationProxy: Error deserialising JSON (\(error))")
} }
#if os(iOS) #if os(iOS)
self.set("name", inSection: "NodeInfo", to: UIDevice.current.name) self.set("name", inSection: "NodeInfo", to: UIDevice.current.name)
#elseif os(OSX) #elseif os(OSX)
self.set("name", inSection: "NodeInfo", to: Host.current().localizedName ?? "") self.set("name", inSection: "NodeInfo", to: Host.current().localizedName ?? "")
#endif #endif
self.fix() self.fix()
} }
@ -132,15 +137,15 @@ class ConfigurationProxy: PlatformItemSource {
} }
} }
public var autoStartAny: Bool { public var autoStartAny: Bool {
get { get {
return self.get("Any", inSection: "AutoStart") as? Bool ?? false return self.get("Any", inSection: "AutoStart") as? Bool ?? false
} }
set { set {
self.set("Any", inSection: "AutoStart", to: newValue) self.set("Any", inSection: "AutoStart", to: newValue)
self.saveSoon() self.saveSoon()
} }
} }
public var autoStartWiFi: Bool { public var autoStartWiFi: Bool {
get { get {
@ -303,25 +308,25 @@ class ConfigurationProxy: PlatformItemSource {
wifirule.interfaceTypeMatch = .any wifirule.interfaceTypeMatch = .any
rules.insert(wifirule, at: 0) rules.insert(wifirule, at: 0)
} }
#if os(macOS) #if os(macOS)
if self.get("Ethernet", inSection: "AutoStart") as? Bool ?? false { if self.get("Ethernet", inSection: "AutoStart") as? Bool ?? false {
let wifirule = NEOnDemandRuleConnect() let wifirule = NEOnDemandRuleConnect()
wifirule.interfaceTypeMatch = .ethernet wifirule.interfaceTypeMatch = .ethernet
rules.insert(wifirule, at: 0) rules.insert(wifirule, at: 0)
} }
#endif #endif
if self.get("WiFi", inSection: "AutoStart") as? Bool ?? false { if self.get("WiFi", inSection: "AutoStart") as? Bool ?? false {
let wifirule = NEOnDemandRuleConnect() let wifirule = NEOnDemandRuleConnect()
wifirule.interfaceTypeMatch = .wiFi wifirule.interfaceTypeMatch = .wiFi
rules.insert(wifirule, at: 0) rules.insert(wifirule, at: 0)
} }
#if canImport(UIKit) #if canImport(UIKit)
if self.get("Mobile", inSection: "AutoStart") as? Bool ?? false { if self.get("Mobile", inSection: "AutoStart") as? Bool ?? false {
let mobilerule = NEOnDemandRuleConnect() let mobilerule = NEOnDemandRuleConnect()
mobilerule.interfaceTypeMatch = .cellular mobilerule.interfaceTypeMatch = .cellular
rules.insert(mobilerule, at: 0) rules.insert(mobilerule, at: 0)
} }
#endif #endif
manager.onDemandRules = rules manager.onDemandRules = rules
manager.isOnDemandEnabled = rules.count > 1 manager.isOnDemandEnabled = rules.count > 1
providerProtocol.disconnectOnSleep = rules.count > 1 providerProtocol.disconnectOnSleep = rules.count > 1
@ -346,7 +351,7 @@ class ConfigurationProxy: PlatformItemSource {
self.json = try JSONSerialization.data(withJSONObject: self.dict as Any, options: .prettyPrinted) self.json = try JSONSerialization.data(withJSONObject: self.dict as Any, options: .prettyPrinted)
} }
#if canImport(UIKit) #if canImport(UIKit)
override func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { override func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return "yggdrasil.conf" return "yggdrasil.conf"
} }
@ -361,5 +366,5 @@ class ConfigurationProxy: PlatformItemSource {
} }
return "yggdrasil.conf.json" return "yggdrasil.conf.json"
} }
#endif #endif
} }

View file

@ -108,11 +108,12 @@ class CrossPlatformAppDelegate: PlatformAppDelegate, ObservableObject {
func becameBackground() {} func becameBackground() {}
func updateStatus(conn: NEVPNConnection) { func updateStatus(conn: NEVPNConnection) {
if conn.status == .connected { if conn.status == .connected || conn.status == .connecting {
self.yggdrasilEnabled = true
self.requestSummaryIPC() self.requestSummaryIPC()
} else if conn.status == .disconnecting || conn.status == .disconnected { } else if conn.status == .disconnecting || conn.status == .disconnected {
self.clearStatus()
self.yggdrasilEnabled = false self.yggdrasilEnabled = false
self.clearStatus()
} }
} }
@ -188,6 +189,8 @@ class CrossPlatformAppDelegate: PlatformAppDelegate, ObservableObject {
self.yggdrasilPublicKey = summary.publicKey self.yggdrasilPublicKey = summary.publicKey
self.yggdrasilPeers = summary.peers self.yggdrasilPeers = summary.peers
self.yggdrasilConnected = summary.peers.filter { $0.up }.count > 0 self.yggdrasilConnected = summary.peers.filter { $0.up }.count > 0
print("Response: \(String(data: js, encoding: .utf8))")
} }
} }
} }

View file

@ -32,6 +32,9 @@ struct YggdrasilPeer: Codable, Identifiable {
let key: String? let key: String?
let priority: UInt8 let priority: UInt8
let cost: UInt16? let cost: UInt16?
let rxBytes: Double?
let txBytes: Double?
let uptime: Int64?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case remote = "URI" case remote = "URI"
@ -40,6 +43,9 @@ struct YggdrasilPeer: Codable, Identifiable {
case key = "Key" case key = "Key"
case priority = "Priority" case priority = "Priority"
case cost = "Cost" case cost = "Cost"
case rxBytes = "RXBytes"
case txBytes = "TXBytes"
case uptime = "Uptime"
} }
public func getStatusBadgeColor() -> SwiftUI.Color { public func getStatusBadgeColor() -> SwiftUI.Color {

View file

@ -99,6 +99,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
switch request { switch request {
case "summary": case "summary":
let pj = self.yggdrasil.getPeersJSON() let pj = self.yggdrasil.getPeersJSON()
NSLog("JSON: \(pj)")
var peers: [YggdrasilPeer] = [] var peers: [YggdrasilPeer] = []
do { do {
peers = try JSONDecoder().decode( peers = try JSONDecoder().decode(

View file

@ -5,34 +5,8 @@
<key>AvailableLibraries</key> <key>AvailableLibraries</key>
<array> <array>
<dict> <dict>
<key>LibraryIdentifier</key> <key>BinaryPath</key>
<string>macos-arm64_x86_64</string> <string>Yggdrasil.framework/Versions/A/Yggdrasil</string>
<key>LibraryPath</key>
<string>Yggdrasil.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-maccatalyst</string>
<key>LibraryPath</key>
<string>Yggdrasil.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>maccatalyst</string>
</dict>
<dict>
<key>LibraryIdentifier</key> <key>LibraryIdentifier</key>
<string>ios-arm64</string> <string>ios-arm64</string>
<key>LibraryPath</key> <key>LibraryPath</key>
@ -45,6 +19,8 @@
<string>ios</string> <string>ios</string>
</dict> </dict>
<dict> <dict>
<key>BinaryPath</key>
<string>Yggdrasil.framework/Versions/A/Yggdrasil</string>
<key>LibraryIdentifier</key> <key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string> <string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key> <key>LibraryPath</key>
@ -59,6 +35,21 @@
<key>SupportedPlatformVariant</key> <key>SupportedPlatformVariant</key>
<string>simulator</string> <string>simulator</string>
</dict> </dict>
<dict>
<key>BinaryPath</key>
<string>Yggdrasil.framework/Versions/A/Yggdrasil</string>
<key>LibraryIdentifier</key>
<string>macos-arm64_x86_64</string>
<key>LibraryPath</key>
<string>Yggdrasil.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
</dict>
</array> </array>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XFWK</string> <string>XFWK</string>

View file

@ -12,11 +12,11 @@ import NetworkExtension
struct Application: App { struct Application: App {
@State private var selection: String? = "Status" @State private var selection: String? = "Status"
#if os(iOS) #if os(iOS)
@UIApplicationDelegateAdaptor(CrossPlatformAppDelegate.self) static var appDelegate: CrossPlatformAppDelegate @UIApplicationDelegateAdaptor(CrossPlatformAppDelegate.self) static var appDelegate: CrossPlatformAppDelegate
#elseif os(macOS) #elseif os(macOS)
@NSApplicationDelegateAdaptor(CrossPlatformAppDelegate.self) static var appDelegate: CrossPlatformAppDelegate @NSApplicationDelegateAdaptor(CrossPlatformAppDelegate.self) static var appDelegate: CrossPlatformAppDelegate
#endif #endif
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@ -53,14 +53,14 @@ struct Application: App {
//.listStyle(.sidebar) //.listStyle(.sidebar)
//.navigationSplitViewColumnWidth(200) //.navigationSplitViewColumnWidth(200)
Image("YggdrasilLogo") /*Image("YggdrasilLogo")
.renderingMode(.template) .renderingMode(.template)
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.foregroundColor(.primary) .foregroundColor(.primary)
.opacity(0.1) .opacity(0.05)
.frame(maxWidth: 200, alignment: .bottom) .frame(maxWidth: 200, alignment: .bottom)
.padding(.all, 24) .padding(.all, 24)*/
} }
.navigationSplitViewColumnWidth(200) .navigationSplitViewColumnWidth(200)
.listStyle(.sidebar) .listStyle(.sidebar)
@ -82,8 +82,8 @@ struct Application: App {
} }
} }
} }
#if os(macOS) #if os(macOS)
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
#endif #endif
} }
} }

View file

@ -18,7 +18,7 @@ struct PeersView: View {
Section(content: { Section(content: {
ForEach(Array(appDelegate.yggdrasilConfig.peers.enumerated()), id: \.offset) { index, peer in ForEach(Array(appDelegate.yggdrasilConfig.peers.enumerated()), id: \.offset) { index, peer in
HStack() { HStack() {
TextField("Peer", text: $appDelegate.yggdrasilConfig.peers[index]) TextField("tls://host:port", text: $appDelegate.yggdrasilConfig.peers[index])
.labelsHidden() .labelsHidden()
#if os(iOS) #if os(iOS)
.disabled(!editMode!.wrappedValue.isEditing) .disabled(!editMode!.wrappedValue.isEditing)
@ -75,8 +75,18 @@ struct PeersView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
} }
} }
TextField("Multicast password", text: $appDelegate.yggdrasilConfig.multicastPassword) VStack {
.labelStyle(.titleAndIcon) 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: { }, header: {
Text("Local connectivity") Text("Local connectivity")
}) })
@ -85,8 +95,10 @@ struct PeersView: View {
.navigationTitle("Peers") .navigationTitle("Peers")
#if os(iOS) #if os(iOS)
.toolbar { .toolbar {
Button("Add", systemImage: "plus") { if editMode!.wrappedValue.isEditing {
appDelegate.yggdrasilConfig.peers.append("") Button("Add", systemImage: "plus") {
appDelegate.yggdrasilConfig.peers.append("")
}
} }
EditButton() EditButton()
.onChange(of: editMode!.wrappedValue) { edit in .onChange(of: editMode!.wrappedValue) { edit in

View file

@ -8,7 +8,6 @@
import SwiftUI import SwiftUI
struct SettingsView: View { struct SettingsView: View {
//@Binding public var yggdrasilConfiguration: ConfigurationProxy
@ObservedObject private var appDelegate = Application.appDelegate @ObservedObject private var appDelegate = Application.appDelegate
@State private var deviceName = "" @State private var deviceName = ""
@ -35,55 +34,55 @@ struct SettingsView: View {
}) })
/* /*
Section(content: { Section(content: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Button("Import configuration") { Button("Import configuration") {
} }
#if os(macOS) #if os(macOS)
.buttonStyle(.link) .buttonStyle(.link)
#endif #endif
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
Text("Import configuration from another device, including the public key and Yggdrasil IP address.") Text("Import configuration from another device, including the public key and Yggdrasil IP address.")
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(.gray) .foregroundColor(.gray)
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
Button("Export configuration") { Button("Export configuration") {
} }
#if os(macOS) #if os(macOS)
.buttonStyle(.link) .buttonStyle(.link)
#endif #endif
.foregroundColor(.accentColor) .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.") 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)) .font(.system(size: 11))
.foregroundColor(.gray) .foregroundColor(.gray)
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
Button("Reset configuration") { Button("Reset configuration") {
} }
#if os(macOS) #if os(macOS)
.buttonStyle(.link) .buttonStyle(.link)
#endif #endif
.foregroundColor(.red) .foregroundColor(.red)
Text("Resetting will overwrite with newly generated configuration. Your public key and Yggdrasil IP address will change.") Text("Resetting will overwrite with newly generated configuration. Your public key and Yggdrasil IP address will change.")
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(.gray) .foregroundColor(.gray)
} }
}, header: { }, header: {
Text("Configuration") Text("Configuration")
}) })
*/ */
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle("Settings") .navigationTitle("Settings")
#if os(iOS) #if os(iOS)
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
#endif #endif
} }
} }

View file

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import CoreImage.CIFilterBuiltins
#if os(iOS) #if os(iOS)
typealias MyListStyle = DefaultListStyle typealias MyListStyle = DefaultListStyle
@ -18,16 +19,19 @@ struct StatusView: View {
@State private var statusBadgeColor: SwiftUI.Color = .gray @State private var statusBadgeColor: SwiftUI.Color = .gray
@State private var statusBadgeText: String = "Not enabled" @State private var statusBadgeText: String = "Not enabled"
@State private var showingPublicKeyPopover = false
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
private func getStatusBadgeColor() -> SwiftUI.Color { private func getStatusBadgeColor() -> SwiftUI.Color {
if !appDelegate.yggdrasilSupported { if appDelegate.yggdrasilConnected {
return .gray
} else if appDelegate.yggdrasilConnected {
return .green return .green
} else if appDelegate.yggdrasilEnabled { } else if appDelegate.yggdrasilEnabled {
return .yellow return .yellow
} else {
return .gray
} }
return .gray
} }
private func getStatusBadgeText() -> String { 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 { var body: some View {
Form { Form {
Section(content: { Section(content: {
@ -125,6 +163,25 @@ struct StatusView: View {
.truncationMode(.tail) .truncationMode(.tail)
.lineLimit(1) .lineLimit(1)
.textSelection(.enabled) .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: { }, header: {
Text("Details") Text("Details")
@ -146,25 +203,48 @@ struct StatusView: View {
return a.remote < b.remote return a.remote < b.remote
}), id: \.remote) { peer in }), id: \.remote) { peer in
VStack { VStack {
Text(peer.remote) HStack {
.frame(maxWidth: .infinity, alignment: .leading) Text(peer.remote)
.truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1) .truncationMode(.tail)
.textSelection(.enabled) .lineLimit(1)
.padding(.bottom, 2) .textSelection(.enabled)
.padding(.bottom, 2)
}
HStack { HStack {
Image(systemName: "circlebadge.fill") Image(systemName: "circlebadge.fill")
.foregroundColor(peer.getStatusBadgeColor()) .foregroundColor(peer.getStatusBadgeColor())
.onChange(of: peer.up) { newValue in .onChange(of: peer.up) { newValue in
statusBadgeColor = peer.getStatusBadgeColor() statusBadgeColor = peer.getStatusBadgeColor()
} }
Text(peer.up ? peer.address ?? "Unknown IP address" : "Not connected") Text(peer.up ? "Connected" : "Not connected")
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
.font(.system(size: 11)) .font(.system(size: 11))
.truncationMode(.tail) .truncationMode(.tail)
.lineLimit(1) .lineLimit(1)
.textSelection(.enabled) .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) .padding(.all, 2)
@ -178,9 +258,9 @@ struct StatusView: View {
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle("Yggdrasil") .navigationTitle("Yggdrasil")
#if os(iOS) #if os(iOS)
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
#endif #endif
} }
} }