mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Add self-tests
This commit is contained in:
1 parent
c9140cf19f
commit
73e273b790
8 files changed
+465
No files matched your search
@@ -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([])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ifaddrs>?
|
||||
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[..<lastDot])
|
||||
}
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifaddr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// FirewallTester.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 14.05.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
|
||||
class FirewallSelfTest {
|
||||
private var listener: NWListener?
|
||||
private var didReceiveResponse = false
|
||||
|
||||
func testLocalLoopbackConnection(timeout: TimeInterval = 3.0, completion: @escaping (Bool) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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..<count {
|
||||
var value = UInt32(i).bigEndian
|
||||
let packet = Data(bytes: &value, count: 4)
|
||||
allData.append(packet)
|
||||
}
|
||||
|
||||
connection.send(content: allData, completion: .contentProcessed { error in
|
||||
if let error = error {
|
||||
print("Client error: \(error)")
|
||||
} else {
|
||||
print("✅ Client sent \(count) packets")
|
||||
}
|
||||
connection.cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .main)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// TcpSelfTestServer.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 14.05.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
class TCPSelfTestServer {
|
||||
private var listener: NWListener?
|
||||
private var receivedData = Data()
|
||||
var onComplete: ((Bool, String) -> 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..<total {
|
||||
let range = i*4..<i*4+4
|
||||
let chunk = receivedData.subdata(in: range)
|
||||
let value = chunk.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
||||
values.append(value)
|
||||
}
|
||||
|
||||
let expected = Array(0..<UInt32(values.count))
|
||||
let dropped = expected.filter { !values.contains($0) }
|
||||
let duplicates = Dictionary(grouping: values, by: { $0 }).filter { $1.count > 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +228,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
EB1A7E422DD4D8BD00A6D5A3 /* SelfTest */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SelfTest; sourceTree = "<group>"; };
|
||||
EB7CD13B2D6F8B6500CB0CB5 /* IAP */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = IAP; sourceTree = "<group>"; };
|
||||
/* 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;
|
||||
|
||||
Reference in new issue
Block a user