Add network connection check

This commit is contained in:
Leon Böttger committed 2025-05-16 10:47:39 +02:00
1 parent 6f1c4f4cf9
commit d9f23f9b44
10 files changed
+111 -331

No files matched your search

+25 -16
View File
@@ -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.")
}
}
@@ -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
}
+4 -2
View File
@@ -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"]
+8 -3
View File
@@ -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[..<lastDot])
let result = String(ip[..<lastDot])
log("[LUI] Found local subnet prefix: \(result) for interface \(name)")
return result
}
}
}
-70
View File
@@ -1,70 +0,0 @@
//
// 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
@@ -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
}
}
@@ -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<sockaddr_in>.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
}
-68
View File
@@ -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)
}
}
}
-38
View File
@@ -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..<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
@@ -1,82 +0,0 @@
//
// 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")
}
}
}