Add self-tests

This commit is contained in:
Leon Böttger committed 2025-05-15 08:21:43 +02:00
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([])
}
}
}
+98
View File
@@ -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
}
}
+70
View File
@@ -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)
}
}
}
}
+52
View File
@@ -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
}
}
+69
View File
@@ -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)
}
}
}
+38
View File
@@ -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)
}
}
+82
View File
@@ -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")
}
}
}
+3
View File
@@ -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;