diff --git a/NearDrop/SelfTest/ApIsolation6Tester.swift b/NearDrop/SelfTest/ApIsolation6Tester.swift new file mode 100644 index 0000000..54ef3f1 --- /dev/null +++ b/NearDrop/SelfTest/ApIsolation6Tester.swift @@ -0,0 +1,53 @@ +// +// ApIsolation6Tester.swift +// QuickDrop +// +// Created by Leon Böttger on 14.05.25. +// + +import Foundation +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) { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/sbin/ping6") + process.arguments = ["-I", interface, "-c", "2", "ff02::1"] + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + process.terminationHandler = { _ in + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) ?? "" + + let lines = output + .components(separatedBy: "\n") + .filter { $0.contains("bytes from") } + + let ips = lines.compactMap { line -> String? in + let match = line.components(separatedBy: " ").first(where: { $0.contains(":") && !$0.contains("ff02") }) + return match?.trimmingCharacters(in: .punctuationCharacters) + } + + // Heuristic: Router is often the first responder — exclude the first + let uniqueIPs = Array(Set(ips)) + let withoutRouter = Array(uniqueIPs.dropFirst()) + + completion(withoutRouter) + } + + do { + try process.run() + } catch { + print("Failed to run ping6: \(error)") + completion([]) + } + } +} diff --git a/NearDrop/SelfTest/ApIsolationTester.swift b/NearDrop/SelfTest/ApIsolationTester.swift new file mode 100644 index 0000000..cb420db --- /dev/null +++ b/NearDrop/SelfTest/ApIsolationTester.swift @@ -0,0 +1,98 @@ +// +// ApIsolationTester.swift +// QuickDrop +// +// Created by Leon Böttger on 14.05.25. +// + +import Foundation +import Network + +class DeviceToDeviceHeuristicScanner { + private let scanQueue = DispatchQueue(label: "DeviceToDeviceScanQueue") + private var reachableIPs = [String]() + private var scannedCount = 0 + private var totalToScan = 0 + private var completion: ((Bool) -> Void)? + + /// Start scanning the subnet for reachable peers. + /// - Parameters: + /// - baseSubnet: e.g. `"192.168.1"` — if nil, will auto-detect + /// - completion: true if any peer device was reachable + 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)" } + totalToScan = ipsToScan.count + + for ip in ipsToScan { + testConnection(to: ip, port: port) + } + + // Timeout fallback + DispatchQueue.main.asyncAfter(deadline: .now() + 6.0) { + if self.scannedCount < self.totalToScan { + self.finish() + } + } + } + + private func testConnection(to ip: String, port: UInt16) { + guard let nwPort = NWEndpoint.Port(rawValue: port) else { return } + let connection = NWConnection(host: NWEndpoint.Host(ip), port: nwPort, using: .tcp) + + connection.stateUpdateHandler = { state in + switch state { + case .ready: + self.reachableIPs.append(ip) + connection.cancel() + self.hostFinished() + case .failed, .cancelled: + connection.cancel() + self.hostFinished() + default: + break + } + } + + connection.start(queue: scanQueue) + } + + private func hostFinished() { + scannedCount += 1 + if scannedCount == totalToScan { + finish() + } + } + + private func finish() { + completion?(reachableIPs.count > 0) + } + + /// Attempts to detect the local subnet prefix (e.g., "192.168.0") + private static func getLocalSubnetPrefix() -> String? { + var ifaddr: UnsafeMutablePointer? + if getifaddrs(&ifaddr) == 0 { + var ptr = ifaddr + while ptr != nil { + defer { ptr = ptr!.pointee.ifa_next } + let interface = ptr!.pointee + 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") { + 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 new file mode 100644 index 0000000..6b866f1 --- /dev/null +++ b/NearDrop/SelfTest/MdnsTester.swift @@ -0,0 +1,52 @@ +// +// 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/SelfTestManager.swift b/NearDrop/SelfTest/SelfTestManager.swift new file mode 100644 index 0000000..0f3a580 --- /dev/null +++ b/NearDrop/SelfTest/SelfTestManager.swift @@ -0,0 +1,69 @@ +// +// 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 totalSize = packetCount * packetSize + + 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 new file mode 100644 index 0000000..e555d68 --- /dev/null +++ b/NearDrop/SelfTest/TcpSelfTestClient.swift @@ -0,0 +1,38 @@ +// +// 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") + } + } +} diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 40238e4..a3054ad 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -228,6 +228,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + EB1A7E422DD4D8BD00A6D5A3 /* SelfTest */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SelfTest; sourceTree = ""; }; EB7CD13B2D6F8B6500CB0CB5 /* IAP */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = IAP; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -315,6 +316,7 @@ 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */, EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */, EB7F54112D78E95C00989564 /* TutorialView.swift */, + EB1A7E422DD4D8BD00A6D5A3 /* SelfTest */, EB7F54132D78E9E400989564 /* LicenseWindow.swift */, EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */, EB36435F2D73A29F009CDFF8 /* AudioManager.swift */, @@ -422,6 +424,7 @@ 69DCF4972AB8BF7B00CBE2CC /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( + EB1A7E422DD4D8BD00A6D5A3 /* SelfTest */, EB7CD13B2D6F8B6500CB0CB5 /* IAP */, ); name = QuickDrop;