Detect ethernet/wifi network change - swift

I want to detect when the network changes from ethernet to wifi (or wifi to ethernet). I want to have an observer to notify me about this change.
reachability isn't good enough - it's always returns ReachableViaWiFi for both cases.
P.S -
There were some questions regarding this topic before, but none of them has a good answer, and since those questions are more than a year old, maybe someone already find out how to do it

You can access system network preferences through SystemConfiguration module, which helps you get touch to system preferences store currently resides in the default location /Library/Preferences/SystemConfiguration/preferences.plist.
Since then, you can receive notifications from SCDynamicStore by SCDynamicStoreNotifyValue(_:_:) or retrieve value by SCDynamicStoreCopyValue(_:_:).
Example for directly lookup current primary network service:
var store = SCDynamicStoreCreate(nil, "Example" as CFString, nil, nil)
var global = SCDynamicStoreCopyValue(store, "State:/Network/Global/IPv4" as CFString)!
var pref = SCPreferencesCreate(nil, "Example" as CFString, nil)
var service = SCNetworkServiceCopy(pref!, global["PrimaryService"] as! CFString)
var interface = SCNetworkServiceGetInterface(service!)
SCNetworkInterfaceGetInterfaceType(interface!) /// Optional("IEEE80211") -> Wi-Fi
Or create dynamic store with callback and set notification keys to receive notifications as every time primary network service changes the notification is going to fire:
var callback: SCDynamicStoreCallBack = { (store, _, _) in
/* Do anything you want */
}
var store = SCDynamicStoreCreate(nil, "Example" as CFString, callback, nil)
SCDynamicStoreSetNotificationKeys(store!, ["State:/Network/Global/IPv4"] as CFArray, nil)

Please note that a Mac can have multiple active interfaces at the same time and some of these may be Ethernet and some of them may be WiFi. Even if you just monitor the primary interfaces, take note that a Mac can have multiple primary interfaces, one per protocol (e.g. the primary interface for IPv4 may not be the primary one for IPv6).
For demonstration purposes, I will assume that you want to monitor the primary IPv4 interface. Here is code that you can just copy & paste to a swift file and directly run from command line (e.g. swift someFile.swift):
import Foundation
import SystemConfiguration
let DynamicStore = SCDynamicStoreCreate(
nil, "Name of your App" as CFString,
{ ( _, _, _ ) in PrimaryIPv4InterfaceChanged() }, nil)!
func PrimaryIPv4InterfaceChanged ( ) {
guard let ipv4State = SCDynamicStoreCopyValue(DynamicStore,
"State:/Network/Global/IPv4" as CFString) as? [CFString: Any]
else {
print("No primary IPv4 interface available")
return
}
guard let primaryServiceID =
ipv4State[kSCDynamicStorePropNetPrimaryService]
else { return }
let interfaceStateName =
"Setup:/Network/Service/\(primaryServiceID)/Interface"
as CFString
guard let primaryServiceState = SCDynamicStoreCopyValue(
DynamicStore, interfaceStateName) as? [CFString: Any]
else { return }
guard let hardwareType =
primaryServiceState[kSCPropNetInterfaceHardware]
else { return }
switch hardwareType as! CFString {
case kSCEntNetAirPort:
print("Primary IPv4 interface is now WiFi")
case kSCEntNetEthernet:
print("Primary IPv4 interface is now Ethernet")
default:
print("Primary IPv4 interface is something else")
}
}
SCDynamicStoreSetNotificationKeys(
DynamicStore, [ "State:/Network/Global/IPv4" ] as CFArray, nil)
SCDynamicStoreSetDispatchQueue(DynamicStore, DispatchQueue.main)
dispatchMain()
While it is running, try switching your primary IPv4 interface, pull network cables, turn off WiFi, etc. and watch the output. You can stop it by hitting CTRL+C on your keyboard.

You could run a little bash script under launchd that monitors the interfaces you are interested in and launches something when they change.
Say your wired connection is en0, you could run:
./netmon en0
Save this script as netmon and make it executable with chmod +x netmon
#!/bin/bash
interface=$1
# Get current status of interface whose name is passed, e.g. en0
status(){
ifconfig $1 | awk '/status:/{print $2}'
}
# Monitor interface until killed, echoing changes in status
previous=$(status $interface)
while :; do
current=$(status $interface)
if [ $current != $previous ]; then
echo $interface now $current
previous=$current
fi
sleep 5
done

Related

I make Beacon in my Swift app which is advertising data but its local name is not shown. Can we add custom data packet during advertisement?

My app act as a beacon but I want to add its local name like my appName. I want to know that can we advertise custom packet in which I can add local name while advertising major, minior, proximityuuid, and identifier in Swift.
My current code for advertiseing ibeacon:
func initLocalBeacon() {
if localBeacon != nil {
stopLocalBeacon()
}
let uuid = UUID(uuidString: localBeaconUUID)!
localBeacon = CLBeaconRegion(uuid: uuid, major: localBeaconMajor, minor: localBeaconMinor, identifier: identifier)
beaconPeripheralData = localBeacon.peripheralData(withMeasuredPower: nil)
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: nil)
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
if peripheral.state == .poweredOn {
peripheralManager.startAdvertising(beaconPeripheralData as? [String: Any])
}
else if peripheral.state == .poweredOff {
peripheralManager.stopAdvertising()
}
}
I am trying to add custom packet in which I add local name for my beacon. Is it possible?
Yor app has very limited control over BLE advertisements on iOS because it is a shared resource across all apps.
Here’s what an app can do:
Trigger an iBeacon advert while it is in the foreground. You only control a 16 byte UUID a 2 byte major and a 2 byte minor.
Trigger a 16-byte service UUID advert while in the foreground.
Trigger a number of bits in a 128 bit bit mask to be turned on in an Overflow Area advert while your app is in the background.
Here is what the phone (not your app) can do:
Trigger a scan response packet containing the name of the phone. This is automatic by the operating system and the name is the name of the phone in Settings.
Your app cannot change the advertised name in the scan response. It cannot advertise additional data over BLE other than shown above, giving you a limited number of bytes to work with.

What is the best way to get BSD drive names on macOS (Swift)?

What's the best way I can get a list of BSD names of all USB devices (and maybe including internal Mac drives) without using a diskutil CLI wrapper?
I don't want to use any wrappers that interact with the CLI interface, as this way of interacting is quite slow and unreliable:
This is an example of why I'm not happy with using CLI wrappers
(Compare 'Time elapsed for DiskUtil CLI Wrapper.' and 'Time elapsed for Disk Arbitration')
What is the best way to implement the solution for my problem?
Use the data from IOReg?
If yes, how can I get a list of BSD names of connected devices using it?
Here is an example what I want to get:
["disk0", "disk0s1", "disk0s2", "disk0s3", "disk1", "disk1s1", "disk1s2", "disk1s3", "disk1s4", "disk2", "disk2s1", "disk2s2", "disk3", "disk3s1", "disk3s1s1", "disk3s2", "disk3s3", "disk3s4", "disk3s5", "disk3s6", "disk4", "disk4s1", "disk4s2", "disk5", "disk5s1", "disk5s2", "disk6", "disk6s1", "disk6s2", "disk10", "disk10s1", "disk10s2", "disk11", "disk11s1"]
At the moment, I have the following:
static func getMountedBSDNames() -> [String] {
guard let session = DASessionCreate(nil) else { return [] }
guard let mountedVolumeURLs = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil) else { return [] }
var BSDNames: [String] = []
for volumeURL in mountedVolumeURLs {
if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, volumeURL as CFURL), let BSDName = DADiskGetBSDName(disk) {
BSDNames.append(
String(cString: BSDName)
)
}
}
return BSDNames
}
But in this case, only mounted are returning.
I want there to have even those, that were ejected
I achieved the desired result using the IOReg lookup method:
Elapsed Time
func getDriveBSDNames() -> [String] {
var iterator: io_iterator_t = 0
let matching: CFDictionary = IOServiceMatching(kIOServicePlane)
// Use 'kIOMasterPortDefault' for macOS older than 12.0 Monterey
IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator)
var child: io_object_t = IOIteratorNext(iterator)
var BSDNames: [String] = []
while child > 0 {
if let BSDNameAnyObject = IORegistryEntryCreateCFProperty(child, "BSD Name" as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)) {
if let BSDNameString = (BSDNameAnyObject.takeRetainedValue() as? String), BSDNameString.starts(with: "disk") {
BSDNames.append(
BSDNameString
)
}
}
child = IOIteratorNext(iterator)
}
return BSDNames
}
In this case, it was also necessary to filter the output of the results using:
BSDNameString.starts(with: "disk")
(otherwise, some unnecessary devices were added, such as en0, anpi0, llw0, etc.)
Note that while the Disk Arbitration framework doesn't have a function for synchronously enumerating all disks, it does effectively support asynchronous enumeration by registering a callback for disk appearance. This may or may not be useful depending on your use case - when providing the user with an interactive list of devices, this is usually exactly what you want though, as you'll automatically be notified of newly added devices.
I don't do Swift, sorry, but the following C code should be easy enough to understand to come up with something similar in other languages.
#include <DiskArbitration/DiskArbitration.h>
#include <stdio.h>
static void disk_appeared(DADiskRef disk, void* context)
{
printf("%s\n", DADiskGetBSDName(disk) ?: "(null)");
}
int main()
{
DASessionRef session = DASessionCreate(kCFAllocatorDefault);
DASessionSetDispatchQueue(session, dispatch_get_main_queue());
DARegisterDiskAppearedCallback(session, NULL, disk_appeared, NULL /*context*/);
dispatch_main();
}
Note that the callback will also be called for APFS snapshots, which don't have a BSD name, so DADiskGetBSDName returns NULL and you'll have to do a little bit of filtering.

iOS 13 wifi ssid in airplane mode

I'm currently updating my application in regards to what is being returned from CNCopyCurrentNetworkInfo. I understand the privacy changes Apple implemented in regards to this starting on iOS 13 so i'm currently updating the implementation.
This is pretty straight forward. However the issue i'm running into is within this part of the app, the user will probably be in airplane mode (in-flight app). Regardless of the CLLocationManager.authorizationStatus(), even if it is .notDetermined which then triggers the requestWhenInUseAuthorization() method and once the user chooses either "Allow Once" or "Allow while Using App", i'm still not able to get the Wi-Fi ssid.
static func fetchSSIDInfo() -> String? {
if isSimulator() {
return "wireless"
} else {
if let interfaces: CFArray = CNCopySupportedInterfaces() {
for i in 0..<CFArrayGetCount(interfaces) {
let interfaceName: UnsafeRawPointer = CFArrayGetValueAtIndex(interfaces, i)
let rec = unsafeBitCast(interfaceName, to: AnyObject.self)
// skips this in airplane mode
if let unsafeInterfaceData = CNCopyCurrentNetworkInfo("\(rec)" as CFString) {
if let interfaceData = unsafeInterfaceData as Dictionary? {
let ssid = interfaceData["SSID" as NSObject] as? String
let bssid = interfaceData["BSSID" as NSObject] as? String
if ssid != "Wi-Fi" && bssid != "00:00:00:00:00:00" {
return ssid
} else {
return "invalid"
}
}
}
}
}
}
return nil
}
In the code above, when in airplane mode it actually skips the if let unsafeInterfaceData. When it isn't in airplane mode, it's working as expected and returns either the ssid or the invalid string depending if user allows location services.
My question is how am I able to get this working on airplane mode? Maybe i'm missing something, but at this point not too sure.
In iOS13, and possibly earlier versions (that I do not have immediately available to test on), once you enable "Airplane mode," the WiFi is automatically disconnected.
An end-user would need to proactively reenable WiFi on their device to reconnect, while still having Airplane mode enabled.
Your if statement isn't getting executed, likely because there's no network information yet. Reenabling WiFi should get you the expected results.

How do I get the broadcast address of my wifi network using Swift?

currently I am using GCDAsyncUdpSocket to send a udp broadcast. Currently, the code sends it to a hardcoded 255.255.255.255 address. Is that the correct broadcast address on all networks? Will it fail on certain networks? What is the correct swift code?
socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
socket!.send(ipRequestData!, toHost: "255.255.255.255", port: discoveryPort, withTimeout: 10000, tag: 0)
This question has an answer but it is in Objective-C
Calculating the Broadcast Address in Objective-C
Your broadcast address depends on your host address and the subnet mask. You can find the formula to calculate the broadcast address on Wikipedia:
broadcastAddress = ipAddress | ~subnetMask
You can calculate it with the following function:
func calculateBroadcastAddress(ipAddress: String, subnetMask: String) -> String {
let ipAdressArray = ipAddress.split(separator: ".")
let subnetMaskArray = subnetMask.split(separator: ".")
guard ipAdressArray.count == 4 && subnetMaskArray.count == 4 else {
return "255.255.255.255"
}
var broadcastAddressArray = [String]()
for i in 0..<4 {
let ipAddressByte = UInt8(ipAdressArray[i]) ?? 0
let subnetMaskbyte = UInt8(subnetMaskArray[i]) ?? 0
let broadcastAddressByte = ipAddressByte | ~subnetMaskbyte
broadcastAddressArray.append(String(broadcastAddressByte))
}
return broadcastAddressArray.joined(separator: ".")
}
Example:
let broadcastAddress = calculateBroadcastAddress(ipAddress: "172.16.0.0", subnetMask: "255.240.0.0")
Result:
"172.31.255.255"
In general, you should never choose the main queue for network operations because it will lead to lags in the user interface (which is drawn in the main queue). Better use
let udpSocket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.global())
Do not forget to explicitly enable broadcasts for this socket
do {
try udpSocket?.enableBroadcast(true)
} catch let error {
print(error)
}
Also consider that Apple enforces an iOS app to work with IPv6, which does not support broadcasts but multicasts. So maybe you should switch to multicasts at least if your device is inside a pure IPv6 network.

NEHotspotHelper.register not received call back iOS11

I am working on NEHotspotHelper and trying to register but not receiving call back. Firstly,
I enabled Capability : Network Extensions
Then added this following code,
let options: [String: NSObject] = [kNEHotspotHelperOptionDisplayName : "ABC" as NSObject]
let queue: DispatchQueue = DispatchQueue(label: "com.ABC", attributes: DispatchQueue.Attributes.concurrent)
NSLog("Started wifi scanning.")
NEHotspotHelper.register(options: options, queue: queue) { (cmd: NEHotspotHelperCommand) in
NSLog("Received command: \(cmd.commandType.rawValue)")
if cmd.commandType == NEHotspotHelperCommandType.filterScanList {
//Get all available hotspots
let list: [NEHotspotNetwork] = cmd.networkList!
//Figure out the hotspot you wish to connect to
print(list)
} else if cmd.commandType == NEHotspotHelperCommandType.evaluate {
if let network = cmd.network {
//Set high confidence for the network
network.setConfidence(NEHotspotHelperConfidence.high)
let response = cmd.createResponse(NEHotspotHelperResult.success)
response.setNetwork(network)
response.deliver() //Respond back
}
} else if cmd.commandType == NEHotspotHelperCommandType.authenticate {
//Perform custom authentication and respond back with success
// if all is OK
let response = cmd.createResponse(NEHotspotHelperResult.success)
response.deliver() //Respond back
}
}
Kindly let me know if I am missing any step.
You should check the result of the register() function. If it's returning false, something is probably not configured correctly. See the full list of configuration instructions below.
Also in the screenshot you provided, you have the entitlements enabled for Hotspot Configuration, but the API you're calling is for Hotspot Helper. The two features require very different entitlements. You'll need to make sure everything is configured for Hotspot Helper to call that API. Again, see below for full details. See Hotspot Helper vs. Hotspot Configuration for more details about the differences of these similarly named APIs.
To use NEHotspotHelper:
Apply for the Network Extension entitlement.
This needs to be done at Apple's website here.
Modify your Provisioning Profile.
Go to http://developer.apple.com. Hit Edit near your profile. On the bottom where it says Entitlements, choose the one that contains the Network Extension entitlement.
Update your app's entitlements file.
The application must set com.apple.developer.networking.HotspotHelper as one of its entitlements. The value of the entitlement is a boolean set to true.
Add Background Mode
The application's Info.plist must include a UIBackgroundModes array containing network-authentication.
Note that unlike all the other background modes that are converted to human readable strings, this one will stay as network-authentication.
Call the NEHotspotHelper.register() function.
This method should be called once when the application starts up. Invoking it again will have no effect and result in false being returned.
You should make sure the function returns true. Otherwise something one of the above steps is probably not configured properly.
Understand when this callback will be called.
From the documentation, it's not entirely clear when exactly this callback will be called. For example, one might assume that NEHotspotHelper could be used to monitor for network connections. However, the callback will (only?) be called when the user navigates to the Settings app and goes to the Wi-Fi page.
Since your callback will be called only while the user in the Settings app, you should attach to the debugger and use print().
Swift Example
let targetSsid = "SFO WiFi"
let targetPassword = "12345678"
let targetAnnotation: String = "Acme Wireless"
let options: [String: NSObject] = [
kNEHotspotHelperOptionDisplayName: targetAnnotation as NSString
]
let queue = DispatchQueue(label: "com.example.test")
let isAvailable = NEHotspotHelper.register(options: options, queue: queue) { (command) in
switch command.commandType {
case .evaluate,
.filterScanList:
let originalNetworklist = command.networkList ?? []
let networkList = originalNetworklist.compactMap { network -> NEHotspotNetwork? in
print("networkName: \(network.ssid); strength: \(network.signalStrength)")
if network.ssid == targetSsid {
network.setConfidence(.high)
network.setPassword(targetPassword)
return network
}
return nil
}
let response = command.createResponse(.success)
response.setNetworkList(networkList)
response.deliver()
default:
break
}
}
assert(isAvailable)
Sources:
https://developer.apple.com/documentation/networkextension/nehotspothelper/1618965-register
https://medium.com/#prvaghela/nehotspothelper-register-an-app-as-a-hotspot-helper-cf92a6ed7b72
https://stackoverflow.com/a/39189063/35690