From d9f23f9b44c06bf7ebe0876ada8ded5dd7427aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6ttger?= Date: Fri, 16 May 2025 10:47:39 +0200 Subject: [PATCH] Add network connection check --- NearDrop/AppDelegate.swift | 41 ++++++---- .../SelfTest/ActiveNetworkInterface.swift | 36 ++++++++ NearDrop/SelfTest/ApIsolation6Tester.swift | 6 +- NearDrop/SelfTest/ApIsolationTester.swift | 11 ++- NearDrop/SelfTest/FirewallTester.swift | 70 ---------------- NearDrop/SelfTest/MdnsTester.swift | 52 ------------ .../SelfTest/NetworkConnectionCheck.swift | 38 +++++++++ NearDrop/SelfTest/SelfTestManager.swift | 68 --------------- NearDrop/SelfTest/TcpSelfTestClient.swift | 38 --------- NearDrop/SelfTest/TcpSelfTestServer.swift | 82 ------------------- 10 files changed, 111 insertions(+), 331 deletions(-) create mode 100644 NearDrop/SelfTest/ActiveNetworkInterface.swift delete mode 100644 NearDrop/SelfTest/FirewallTester.swift delete mode 100644 NearDrop/SelfTest/MdnsTester.swift create mode 100644 NearDrop/SelfTest/NetworkConnectionCheck.swift delete mode 100644 NearDrop/SelfTest/SelfTestManager.swift delete mode 100644 NearDrop/SelfTest/TcpSelfTestClient.swift delete mode 100644 NearDrop/SelfTest/TcpSelfTestServer.swift diff --git a/NearDrop/AppDelegate.swift b/NearDrop/AppDelegate.swift index d6aca2f..b287dac 100644 --- a/NearDrop/AppDelegate.swift +++ b/NearDrop/AppDelegate.swift @@ -89,27 +89,36 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele UNUserNotificationCenter.current().delegate=self - let scanner = DeviceToDeviceHeuristicScanner() - scanner.scan { allowed in - if allowed { - log("✅ Device-to-device likely allowed (peer responded on LAN).") - } else { - - let scanner2 = IPv6DeviceScanner() - scanner2.scan(interface: "en0") { devices in - if devices.isEmpty { - log("❌ No local devices responded — peer-to-peer may be blocked.") - - self.openAlert(type: .ApIsolation) - } else { - log("✅ Found IPv6 devices (excluding router):") - for ip in devices { - print(" • \(ip)") + + let hasConnection = isConnectedToNetwork() + log("Currently used interface: \(getActiveNetworkInterface())") + + if hasConnection { + let scanner = DeviceToDeviceHeuristicScanner() + scanner.scan { allowed in + if allowed { + log("✅ Device-to-device likely allowed (peer responded on LAN).") + } else { + + let scanner2 = IPv6DeviceScanner() + scanner2.scan() { devices in + if devices.isEmpty { + log("❌ No local devices responded — peer-to-peer may be blocked.") + + self.openAlert(type: .ApIsolation) + } else { + log("✅ Found IPv6 devices (excluding router):") + for ip in devices { + print(" • \(ip)") + } } } } } } + else { + log("❌ Network unavailable, skipping device-to-device check.") + } } diff --git a/NearDrop/SelfTest/ActiveNetworkInterface.swift b/NearDrop/SelfTest/ActiveNetworkInterface.swift new file mode 100644 index 0000000..2e0a61f --- /dev/null +++ b/NearDrop/SelfTest/ActiveNetworkInterface.swift @@ -0,0 +1,36 @@ +// +// ActiveNetworkInterface.swift +// QuickDrop +// +// Created by Leon Böttger on 16.05.25. +// + +import Foundation + +/// Returns the name of the currently active network interface (e.g. "en0", "en1") +func getActiveNetworkInterface() -> String? { + let process = Process() + let pipe = Pipe() + + process.standardOutput = pipe + process.standardError = Pipe() // Ignore errors as in `2>/dev/null` + process.launchPath = "/bin/bash" + process.arguments = ["-c", "route get default 2>/dev/null | awk '/interface: / {print $2}'"] + + do { + try process.run() + } catch { + print("Failed to run process: \(error)") + return nil + } + + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + if let output = String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines), !output.isEmpty { + return output + } + + return nil +} diff --git a/NearDrop/SelfTest/ApIsolation6Tester.swift b/NearDrop/SelfTest/ApIsolation6Tester.swift index 54ef3f1..e894323 100644 --- a/NearDrop/SelfTest/ApIsolation6Tester.swift +++ b/NearDrop/SelfTest/ApIsolation6Tester.swift @@ -11,10 +11,12 @@ import Network class IPv6DeviceScanner { /// Scans for IPv6 devices using `ping6 ff02::1` multicast on the given interface. /// - Parameters: - /// - interface: The interface to scan on (e.g. "en0"). /// - timeout: How long to wait for replies (default 2 seconds). /// - completion: Callback with a list of detected device IPs (excluding router). - func scan(interface: String = "en0", timeout: TimeInterval = 2.0, completion: @escaping ([String]) -> Void) { + func scan(timeout: TimeInterval = 2.0, completion: @escaping ([String]) -> Void) { + + let interface = getActiveNetworkInterface() ?? "en0" + let process = Process() process.executableURL = URL(fileURLWithPath: "/sbin/ping6") process.arguments = ["-I", interface, "-c", "2", "ff02::1"] diff --git a/NearDrop/SelfTest/ApIsolationTester.swift b/NearDrop/SelfTest/ApIsolationTester.swift index cb420db..a520168 100644 --- a/NearDrop/SelfTest/ApIsolationTester.swift +++ b/NearDrop/SelfTest/ApIsolationTester.swift @@ -22,7 +22,7 @@ class DeviceToDeviceHeuristicScanner { func scan(subnet baseSubnet: String? = nil, port: UInt16 = 80, completion: @escaping (Bool) -> Void) { self.completion = completion let subnet = baseSubnet ?? Self.getLocalSubnetPrefix() ?? "192.168.1" - let ipsToScan = (2...20).map { "\(subnet).\($0)" } + let ipsToScan = (2...254).map { "\(subnet).\($0)" } totalToScan = ipsToScan.count for ip in ipsToScan { @@ -80,14 +80,19 @@ class DeviceToDeviceHeuristicScanner { let addrFamily = interface.ifa_addr.pointee.sa_family if addrFamily == UInt8(AF_INET), let name = String(validatingUTF8: interface.ifa_name), - name == "en0" || name == "en1" || name.contains("wlan") { + name == getActiveNetworkInterface() { var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), &hostname, socklen_t(hostname.count), nil, 0, NI_NUMERICHOST) if let ip = String(validatingUTF8: hostname), let lastDot = ip.lastIndex(of: ".") { freeifaddrs(ifaddr) - return String(ip[.. Void) { - // Step 1: Create a listener on a random available port - do { - let parameters = NWParameters.tcp - listener = try NWListener(using: parameters, on: 0) // 0 = auto-select available port - } catch { - print("Failed to create listener: \(error)") - completion(false) - return - } - - listener?.stateUpdateHandler = { newState in - if case .ready = newState { - // Step 2: Once listener is ready, connect to it via loopback - guard let port = self.listener?.port else { - completion(false) - return - } - - let connection = NWConnection(host: "127.0.0.1", port: port, using: .tcp) - connection.stateUpdateHandler = { state in - switch state { - case .ready: - self.didReceiveResponse = true - connection.cancel() - self.listener?.cancel() - completion(true) - case .failed, .cancelled: - if !self.didReceiveResponse { - completion(false) - } - default: - break - } - } - - connection.start(queue: .main) - } - } - - listener?.newConnectionHandler = { newConnection in - newConnection.cancel() // We just need to know we can connect - } - - listener?.start(queue: .main) - - // Step 3: Timeout fallback - DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { - if !self.didReceiveResponse { - self.listener?.cancel() - completion(false) - } - } - } -} diff --git a/NearDrop/SelfTest/MdnsTester.swift b/NearDrop/SelfTest/MdnsTester.swift deleted file mode 100644 index 6b866f1..0000000 --- a/NearDrop/SelfTest/MdnsTester.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// MdnsTester.swift -// QuickDrop -// -// Created by Leon Böttger on 14.05.25. -// - -import Foundation - -class MDNSSelfTest: NSObject, NetServiceDelegate, NetServiceBrowserDelegate { - private var service: NetService? - private var browser: NetServiceBrowser? - private var didFindOwnService = false - private var completionHandler: ((Bool) -> Void)? - - func testBonjourAvailability(timeout: TimeInterval = 5.0, completion: @escaping (Bool) -> Void) { - self.completionHandler = completion - - // Step 1: Start advertising a Bonjour service - let serviceName = "MDNS-Test-\(UUID().uuidString.prefix(6))" - service = NetService(domain: "local.", type: "_mdnstest._tcp.", name: serviceName, port: 12345) - service?.delegate = self - service?.publish() - - // Step 2: Start browsing for it - browser = NetServiceBrowser() - browser?.delegate = self - browser?.searchForServices(ofType: "_mdnstest._tcp.", inDomain: "local.") - - // Step 3: Timeout if nothing is found - DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { - self.stop() - self.completionHandler?(self.didFindOwnService) - } - } - - func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { - if service.name == self.service?.name { - didFindOwnService = true - stop() - completionHandler?(true) - completionHandler = nil - } - } - - func stop() { - service?.stop() - browser?.stop() - service = nil - browser = nil - } -} diff --git a/NearDrop/SelfTest/NetworkConnectionCheck.swift b/NearDrop/SelfTest/NetworkConnectionCheck.swift new file mode 100644 index 0000000..7c88dba --- /dev/null +++ b/NearDrop/SelfTest/NetworkConnectionCheck.swift @@ -0,0 +1,38 @@ +// +// NetworkConnectionCheck.swift +// QuickDrop +// +// Created by Leon Böttger on 16.05.25. +// + +import Foundation +import SystemConfiguration + +/// Returns true if the device is connected to any network (Wi-Fi, Ethernet, etc.) +func isConnectedToNetwork() -> Bool { + var zeroAddress = sockaddr_in( + sin_len: UInt8(MemoryLayout.size), + sin_family: sa_family_t(AF_INET), + sin_port: 0, + sin_addr: in_addr(s_addr: 0), + sin_zero: (0,0,0,0,0,0,0,0) + ) + + guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { zeroSockAddress in + SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) + } + }) else { + return false + } + + var flags = SCNetworkReachabilityFlags() + if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) { + return false + } + + let isReachable = flags.contains(.reachable) + let needsConnection = flags.contains(.connectionRequired) + + return isReachable && !needsConnection +} diff --git a/NearDrop/SelfTest/SelfTestManager.swift b/NearDrop/SelfTest/SelfTestManager.swift deleted file mode 100644 index 0b3d64d..0000000 --- a/NearDrop/SelfTest/SelfTestManager.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// SelfTestManager.swift -// QuickDrop -// -// Created by Leon Böttger on 15.05.25. -// - -import Foundation -import Network - -class SelfTestManager { - - static func run() { - - let tester = FirewallSelfTest() - tester.testLocalLoopbackConnection { allowed in - if allowed { - print("✅ Incoming connections appear allowed (no firewall or allowed exception).") - } else { - print("❌ Incoming connections may be blocked (firewall or system policy).") - } - } - - let tester2 = MDNSSelfTest() - tester2.testBonjourAvailability { available in - if available { - print("✅ mDNS is working — multicast DNS responses received.") - } else { - print("❌ mDNS seems blocked — possibly by network or router settings.") - } - } - - let scanner = DeviceToDeviceHeuristicScanner() - scanner.scan { allowed in - if allowed { - print("✅ Device-to-device likely allowed (peer responded on LAN).") - } else { - - let scanner2 = IPv6DeviceScanner() - scanner2.scan(interface: "en0") { devices in - if devices.isEmpty { - print("❌ No local devices responded — peer-to-peer may be blocked.") - } else { - print("✅ Found IPv6 devices (excluding router):") - for ip in devices { - print(" • \(ip)") - } - } - } - } - } - - let port: UInt16 = 43210 - let packetCount = 10 // 32K packets - let packetSize = 512 * 1024 // Each packet = UInt32 - - let server = TCPSelfTestServer() - let client = TCPSelfTestClient() - - server.start(port: NWEndpoint.Port(rawValue: port)!, expectedPackets: packetCount) { success, report in - print(success ? "✅ Success: \(report)" : "❌ Failure: \(report)") - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - client.sendSequentialPackets(to: port, count: packetCount) - } - } -} diff --git a/NearDrop/SelfTest/TcpSelfTestClient.swift b/NearDrop/SelfTest/TcpSelfTestClient.swift deleted file mode 100644 index e555d68..0000000 --- a/NearDrop/SelfTest/TcpSelfTestClient.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// TcpSelfTestClient.swift -// QuickDrop -// -// Created by Leon Böttger on 14.05.25. -// - -import Foundation -import Network - -class TCPSelfTestClient { - func sendSequentialPackets(to port: UInt16, count: Int) { - guard let nwPort = NWEndpoint.Port(rawValue: port) else { return } - - let connection = NWConnection(host: "127.0.0.1", port: nwPort, using: .tcp) - connection.stateUpdateHandler = { state in - if state == .ready { - var allData = Data() - for i in 0.. Void)? - private var expectedPackets: Int = 0 - - func start(port: NWEndpoint.Port, expectedPackets: Int, onComplete: @escaping (Bool, String) -> Void) { - self.onComplete = onComplete - self.expectedPackets = expectedPackets - - do { - let listener = try NWListener(using: .tcp, on: port) - self.listener = listener - listener.newConnectionHandler = { connection in - connection.start(queue: .main) - self.receive(on: connection) - } - listener.start(queue: .main) - } catch { - onComplete(false, "Server failed: \(error.localizedDescription)") - } - } - - private func receive(on connection: NWConnection) { - connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in - if let data = data { - self.receivedData.append(data) - } - - if isComplete || self.receivedData.count >= self.expectedPackets * 4 { - connection.cancel() - self.listener?.cancel() - self.verify() - } else if error == nil { - self.receive(on: connection) - } else { - self.onComplete?(false, "Receive error: \(String(describing: error))") - } - } - } - - private func verify() { - var values = [UInt32]() - let total = receivedData.count / 4 - for i in 0.. 1 }.keys - - if !dropped.isEmpty || !duplicates.isEmpty || values != expected { - var issues = [String]() - if !dropped.isEmpty { - issues.append("Dropped packets: \(dropped.prefix(10))...") - } - if !duplicates.isEmpty { - issues.append("Duplicates: \(duplicates.prefix(10))") - } - if values != expected { - issues.append("Out-of-order delivery detected.") - } - onComplete?(false, issues.joined(separator: " | ")) - } else { - onComplete?(true, "All packets received in order") - } - } -}