mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Add network connection check
This commit is contained in:
1 parent
6f1c4f4cf9
commit
d9f23f9b44
10 files changed
+111
-331
No files matched your search
+25
-16
@@ -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
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user