@mentra/glasses-media 3.3.0-dev.408 → 3.3.0-dev.413
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Native receivers, WHIP publishers and network helpers shared by the Mentra App.
|
|
|
4
4
|
|
|
5
5
|
- Android: the existing `GlassesMediaSource`, local WHIP and WHEP receivers, scoped SoftAP networking, decoded I420/PCM delivery, and transport diagnostics moved here. The ACS session and raw outgoing streams remain in `acs-meeting`.
|
|
6
6
|
- iOS: `DecodedGlassesMediaSource` delivers `CVPixelBuffer` video and interleaved PCM16. `LocalWhipIngestSource` answers the glasses' existing WHIP client; `WhepVideoSource` retains the cloud subscription path.
|
|
7
|
-
- `GlassesHotspotNetwork` uses the same persistent `NEHotspotConfiguration` join as gallery transfers. Wi-Fi carries the glasses link; cellular carries the call. It verifies the joined SSID and `en0`
|
|
7
|
+
- `GlassesHotspotNetwork` uses the same persistent `NEHotspotConfiguration` join as gallery transfers. Wi-Fi carries the glasses link; cellular carries the call. It verifies the joined SSID and the address of the one interface Network framework reports as Wi-Fi (not a fixed `en0`; a Mac can use `en1`), keeps that interface for loss checks and ACS binding, reports network loss, and retains a cancelled join until the uncancellable system callback has been cleaned up.
|
|
8
8
|
|
|
9
9
|
The publisher owns its outgoing encoders, audio policy, and destination. Receivers do not open the phone microphone or play the glasses microphone locally. iOS uses a receive-only audio device to drive decoded PCM delivery without changing the app's audio session.
|
|
10
10
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// One IPv4 address row reported by `getifaddrs`.
|
|
4
|
+
public struct HotspotInterfaceAddress: Equatable {
|
|
5
|
+
public let name: String
|
|
6
|
+
public let isUp: Bool
|
|
7
|
+
public let ipv4: String
|
|
8
|
+
|
|
9
|
+
public init(name: String, isUp: Bool, ipv4: String) {
|
|
10
|
+
self.name = name
|
|
11
|
+
self.isUp = isUp
|
|
12
|
+
self.ipv4 = ipv4
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// One `getifaddrs` sample: every attached interface's kernel index (its `AF_LINK` row, present
|
|
17
|
+
/// with or without an IPv4 address) and every IPv4 address row.
|
|
18
|
+
public struct HotspotInterfaceSnapshot: Equatable {
|
|
19
|
+
public let links: [String: Int]
|
|
20
|
+
public let addresses: [HotspotInterfaceAddress]
|
|
21
|
+
|
|
22
|
+
public init(links: [String: Int], addresses: [HotspotInterfaceAddress]) {
|
|
23
|
+
self.links = links
|
|
24
|
+
self.addresses = addresses
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Network framework's type for one interface on one evaluated path.
|
|
29
|
+
public struct HotspotInterfaceReport: Equatable {
|
|
30
|
+
public let name: String
|
|
31
|
+
public let index: Int
|
|
32
|
+
public let isWifi: Bool
|
|
33
|
+
|
|
34
|
+
public init(name: String, index: Int, isWifi: Bool) {
|
|
35
|
+
self.name = name
|
|
36
|
+
self.index = index
|
|
37
|
+
self.isWifi = isWifi
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The Wi-Fi interface carrying the glasses hotspot and the DHCP address it held when verified.
|
|
42
|
+
public struct HotspotInterfaceBinding: Hashable {
|
|
43
|
+
public let interface: String
|
|
44
|
+
public let index: Int
|
|
45
|
+
public let address: String
|
|
46
|
+
|
|
47
|
+
public init(interface: String, index: Int, address: String) {
|
|
48
|
+
self.interface = interface
|
|
49
|
+
self.index = index
|
|
50
|
+
self.address = address
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Wi-Fi identity learned during one hotspot session. Create a new value for every join.
|
|
55
|
+
///
|
|
56
|
+
/// A path's interface list describes routes, not hardware: after Wi-Fi moves to an AP without
|
|
57
|
+
/// internet, both the default and the Wi-Fi-constrained path can omit it. An interface's type
|
|
58
|
+
/// does not change while it stays attached, so a positive Wi-Fi report is kept for the same
|
|
59
|
+
/// name and kernel index until that interface is detached or replaced. Omission from a later
|
|
60
|
+
/// report is not evidence. Any non-Wi-Fi report for a name rejects it for the rest of the session.
|
|
61
|
+
public struct HotspotWifiIdentity: Equatable {
|
|
62
|
+
public private(set) var trusted: [String: Int] = [:]
|
|
63
|
+
public private(set) var rejected: Set<String> = []
|
|
64
|
+
/// Trusted names later seen detached or replaced. A fresh report may trust the name again
|
|
65
|
+
/// for discovery, but an address verified before the change stays lost.
|
|
66
|
+
public private(set) var detached: Set<String> = []
|
|
67
|
+
|
|
68
|
+
public init() {}
|
|
69
|
+
|
|
70
|
+
public mutating func observe(_ reports: [HotspotInterfaceReport]) {
|
|
71
|
+
for report in reports where !report.isWifi {
|
|
72
|
+
rejected.insert(report.name)
|
|
73
|
+
trusted[report.name] = nil
|
|
74
|
+
}
|
|
75
|
+
for report in reports where report.isWifi && !report.name.isEmpty && report.index > 0 && !rejected.contains(report.name) {
|
|
76
|
+
// A new index means a different interface took the name; only this report vouches for it.
|
|
77
|
+
if let index = trusted[report.name], index != report.index { detached.insert(report.name) }
|
|
78
|
+
trusted[report.name] = report.index
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Forget an interface that is no longer attached, or whose name now has another index.
|
|
83
|
+
/// Only a new positive report can re-establish it.
|
|
84
|
+
public mutating func observe(_ snapshot: HotspotInterfaceSnapshot) {
|
|
85
|
+
for (name, index) in trusted where snapshot.links[name] != index {
|
|
86
|
+
trusted[name] = nil
|
|
87
|
+
detached.insert(name)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Wi-Fi is not always `en0`: a Mac can have built-in Ethernet on `en0` and Wi-Fi on `en1`.
|
|
93
|
+
/// Identify the hotspot link by interface type and address, never by name or by the first
|
|
94
|
+
/// private address, and keep that exact interface for the rest of the session.
|
|
95
|
+
public enum HotspotInterfacePolicy {
|
|
96
|
+
/// The unique up IPv4 address, on an interface this session trusts as Wi-Fi and that is still
|
|
97
|
+
/// attached with the same index, that can be the hotspot client. With a BLE-reported gateway
|
|
98
|
+
/// it must be a valid client on that gateway's /24. Missing or ambiguous candidates return nil.
|
|
99
|
+
public static func select(snapshot: HotspotInterfaceSnapshot, identity: HotspotWifiIdentity,
|
|
100
|
+
gateway: String?) -> HotspotInterfaceBinding?
|
|
101
|
+
{
|
|
102
|
+
let bindings = Set(snapshot.addresses.compactMap { row -> HotspotInterfaceBinding? in
|
|
103
|
+
guard let index = identity.trusted[row.name], snapshot.links[row.name] == index,
|
|
104
|
+
row.isUp, LocalMediaPolicy.isPrivate(row.ipv4) else { return nil }
|
|
105
|
+
if let gateway, !LocalMediaPolicy.isHotspotClientAddress(row.ipv4, gateway: gateway) { return nil }
|
|
106
|
+
return HotspotInterfaceBinding(interface: row.name, index: index, address: row.ipv4)
|
|
107
|
+
})
|
|
108
|
+
return bindings.count == 1 ? bindings.first : nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// A verified link remains usable only while the same attached interface is up with the same
|
|
112
|
+
/// address. Later path reports may omit it. Another interface acquiring that address, a
|
|
113
|
+
/// detach or replacement under the same name, or a non-Wi-Fi report ends it for the session.
|
|
114
|
+
/// Pass an identity that has already observed `snapshot`.
|
|
115
|
+
public static func isIntact(_ binding: HotspotInterfaceBinding, snapshot: HotspotInterfaceSnapshot,
|
|
116
|
+
identity: HotspotWifiIdentity) -> Bool
|
|
117
|
+
{
|
|
118
|
+
guard identity.trusted[binding.interface] == binding.index, !identity.detached.contains(binding.interface),
|
|
119
|
+
snapshot.links[binding.interface] == binding.index else { return false }
|
|
120
|
+
return snapshot.addresses.contains { $0.name == binding.interface && $0.isUp && $0.ipv4 == binding.address }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -10,7 +10,8 @@ public final class GlassesHotspotNetwork {
|
|
|
10
10
|
private let queue = DispatchQueue(label: "com.mentra.glassesmedia.hotspot")
|
|
11
11
|
private var ssid: String?
|
|
12
12
|
private var lastHotspotSSID: String?
|
|
13
|
-
|
|
13
|
+
/// The verified Wi-Fi interface and DHCP address. Every later check uses this exact pair.
|
|
14
|
+
private var binding: HotspotInterfaceBinding?
|
|
14
15
|
private var gatewayAddress: String?
|
|
15
16
|
private var generation = 0
|
|
16
17
|
private var applying = false
|
|
@@ -18,8 +19,13 @@ public final class GlassesHotspotNetwork {
|
|
|
18
19
|
private var cancelled = false
|
|
19
20
|
private var joinReply: ((Result<String, Error>) -> Void)?
|
|
20
21
|
private var leaveReplies: [() -> Void] = []
|
|
21
|
-
private var
|
|
22
|
+
private var interfaceMonitors: [NWPathMonitor] = []
|
|
23
|
+
/// Wi-Fi identity learned since this join started; replaced on every join and cleared on leave.
|
|
24
|
+
private var wifiIdentity = HotspotWifiIdentity()
|
|
25
|
+
private var monitorsReported: [Bool] = []
|
|
26
|
+
private var interfaceReportsReady: (() -> Void)?
|
|
22
27
|
private var localAccess: LocalNetworkAccessRequest?
|
|
28
|
+
private var localAddress: String? { binding?.address }
|
|
23
29
|
#if MENTRA_E2E
|
|
24
30
|
private var testLeaseConsumed = false
|
|
25
31
|
#endif
|
|
@@ -42,40 +48,42 @@ public final class GlassesHotspotNetwork {
|
|
|
42
48
|
self.applying = false
|
|
43
49
|
self.ownsConfiguration = false
|
|
44
50
|
self.joinReply = completion
|
|
51
|
+
self.startInterfaceMonitors(generation: gen)
|
|
45
52
|
if ProcessInfo.processInfo.isiOSAppOnMac {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Reuse is decided once, so it needs the interface types first.
|
|
54
|
+
self.awaitInterfaceReports(generation: gen) {
|
|
55
|
+
#if MENTRA_E2E
|
|
56
|
+
if let raw = ProcessInfo.processInfo.environment["MENTRA_E2E_PREJOINED_HOTSPOT"] {
|
|
57
|
+
let selected = self.selectBinding()
|
|
58
|
+
guard !self.testLeaseConsumed,
|
|
59
|
+
let lease = try? JSONDecoder().decode(MacE2EHotspotLease.self, from: Data(raw.utf8)),
|
|
60
|
+
lease.matches(ssid: ssid, gateway: gateway, address: selected?.address, now: Date().timeIntervalSince1970),
|
|
61
|
+
let selected
|
|
62
|
+
else {
|
|
63
|
+
self.finishJoin(.failure(LocalMediaError("Mac E2E network lease is invalid, expired or already consumed")))
|
|
64
|
+
self.finishLeave()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
self.testLeaseConsumed = true
|
|
68
|
+
self.bind(selected)
|
|
69
|
+
self.logger.notice("HOTSPOT_JOIN test_harness_connection native_association_untested")
|
|
70
|
+
self.waitForLocalAccess(address: selected.address, generation: gen)
|
|
56
71
|
return
|
|
57
72
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
self.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
self.localAddress = address
|
|
74
|
-
self.logger.info("HOTSPOT_JOIN reuse_verified_macos_connection")
|
|
75
|
-
self.startMonitor(generation: gen)
|
|
76
|
-
self.waitForLocalAccess(address: address, generation: gen)
|
|
77
|
-
} else {
|
|
78
|
-
self.applyConfiguration(ssid: ssid, passphrase: passphrase, generation: gen)
|
|
73
|
+
#endif
|
|
74
|
+
NEHotspotNetwork.fetchCurrent { network in
|
|
75
|
+
self.queue.async {
|
|
76
|
+
guard gen == self.generation, !self.cancelled else { return }
|
|
77
|
+
let selected = self.selectBinding()
|
|
78
|
+
if let selected, LocalMediaPolicy.canReuseHotspot(requestedSSID: ssid, currentSSID: network?.ssid,
|
|
79
|
+
address: selected.address, gateway: gateway)
|
|
80
|
+
{
|
|
81
|
+
self.bind(selected)
|
|
82
|
+
self.logger.info("HOTSPOT_JOIN reuse_verified_macos_connection")
|
|
83
|
+
self.waitForLocalAccess(address: selected.address, generation: gen)
|
|
84
|
+
} else {
|
|
85
|
+
self.applyConfiguration(ssid: ssid, passphrase: passphrase, generation: gen)
|
|
86
|
+
}
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
}
|
|
@@ -106,6 +114,15 @@ public final class GlassesHotspotNetwork {
|
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
/// The verified hotspot address while its own Wi-Fi interface still holds it; nil after
|
|
118
|
+
/// loss, leave or an address change. Never falls back to another interface.
|
|
119
|
+
public func verifiedAddress(completion: @escaping (String?) -> Void) {
|
|
120
|
+
queue.async {
|
|
121
|
+
guard let binding = self.binding, !self.cancelled, self.isBindingIntact(binding) else { completion(nil); return }
|
|
122
|
+
completion(binding.address)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
public func info(completion: @escaping ([String: Any]) -> Void) {
|
|
110
127
|
queue.async {
|
|
111
128
|
var value: [String: Any] = ["available": self.localAddress != nil]
|
|
@@ -124,7 +141,7 @@ public final class GlassesHotspotNetwork {
|
|
|
124
141
|
let parameters = NWParameters.tcp
|
|
125
142
|
if ProcessInfo.processInfo.isiOSAppOnMac {
|
|
126
143
|
// The Mac route probe rejects the Wi-Fi type constraint on an otherwise
|
|
127
|
-
// usable
|
|
144
|
+
// usable hotspot route. Restrict this connection to the verified source IP.
|
|
128
145
|
parameters.requiredLocalEndpoint = .hostPort(host: NWEndpoint.Host(address), port: .any)
|
|
129
146
|
} else {
|
|
130
147
|
parameters.requiredInterfaceType = .wifi
|
|
@@ -221,34 +238,97 @@ public final class GlassesHotspotNetwork {
|
|
|
221
238
|
NEHotspotNetwork.fetchCurrent { [weak self] network in
|
|
222
239
|
self?.queue.async {
|
|
223
240
|
guard let self, gen == self.generation, !self.cancelled else { return }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
self.
|
|
228
|
-
self.startMonitor(generation: gen)
|
|
229
|
-
self.waitForLocalAccess(address: address, generation: gen)
|
|
241
|
+
// The selection applies the BLE gateway's client check when one was reported.
|
|
242
|
+
if network?.ssid == ssid, let selected = self.selectBinding() {
|
|
243
|
+
self.bind(selected)
|
|
244
|
+
self.waitForLocalAccess(address: selected.address, generation: gen)
|
|
230
245
|
} else if remaining > 0 {
|
|
231
246
|
self.queue.asyncAfter(deadline: .now() + 0.5) { self.waitForAddress(ssid: ssid, generation: gen, remaining: remaining - 1) }
|
|
232
247
|
} else {
|
|
233
248
|
let association = network == nil ? "unavailable" : (network?.ssid == ssid ? "matched" : "different")
|
|
234
|
-
|
|
235
|
-
self.finishJoin(.failure(LocalMediaError("Glasses hotspot has no verified Wi-Fi address (SSID=\(association), Wi-Fi IPv4=\(address))")))
|
|
249
|
+
self.finishJoin(.failure(LocalMediaError("Glasses hotspot has no verified Wi-Fi address (SSID=\(association), \(self.wifiDiagnostic()))")))
|
|
236
250
|
self.finishLeave()
|
|
237
251
|
}
|
|
238
252
|
}
|
|
239
253
|
}
|
|
240
254
|
}
|
|
241
255
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
/// iOS can omit an internet-less Wi-Fi from its default path, and iOS-on-Mac has rejected
|
|
257
|
+
/// a Wi-Fi type constraint on a usable hotspot route. Both paths' interface types feed one
|
|
258
|
+
/// session identity (HotspotWifiIdentity): a positive Wi-Fi report outlives later omission
|
|
259
|
+
/// only while the same interface stays attached; contrary reports and replacement fail closed.
|
|
260
|
+
private func startInterfaceMonitors(generation gen: Int) {
|
|
261
|
+
interfaceMonitors.forEach { $0.cancel() }
|
|
262
|
+
let monitors = [NWPathMonitor(requiredInterfaceType: .wifi), NWPathMonitor()]
|
|
263
|
+
interfaceMonitors = monitors
|
|
264
|
+
wifiIdentity = HotspotWifiIdentity()
|
|
265
|
+
monitorsReported = Array(repeating: false, count: monitors.count)
|
|
266
|
+
for (index, monitor) in monitors.enumerated() {
|
|
267
|
+
monitor.pathUpdateHandler = { [weak self] path in
|
|
268
|
+
guard let self, gen == self.generation else { return }
|
|
269
|
+
self.wifiIdentity.observe(path.availableInterfaces.map {
|
|
270
|
+
HotspotInterfaceReport(name: $0.name, index: $0.index, isWifi: $0.type == .wifi)
|
|
271
|
+
})
|
|
272
|
+
self.monitorsReported[index] = true
|
|
273
|
+
if !self.monitorsReported.contains(false) { self.fireInterfaceReportsReady() }
|
|
274
|
+
// This AP intentionally has no internet. Loss of its default internet path is not
|
|
275
|
+
// loss of the local link; check the verified interface and address instead.
|
|
276
|
+
if let binding = self.binding, !self.cancelled, !self.isBindingIntact(binding) {
|
|
277
|
+
self.onLost?("Glasses hotspot connection was lost")
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
monitor.start(queue: queue)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Continue once every monitor has reported, or after a short bound with whatever arrived.
|
|
285
|
+
private func awaitInterfaceReports(generation gen: Int, then proceed: @escaping () -> Void) {
|
|
286
|
+
interfaceReportsReady = { [weak self] in
|
|
287
|
+
guard let self, gen == self.generation, !self.cancelled else { return }
|
|
288
|
+
proceed()
|
|
289
|
+
}
|
|
290
|
+
if !monitorsReported.contains(false) { fireInterfaceReportsReady(); return }
|
|
291
|
+
queue.asyncAfter(deadline: .now() + 2) { [weak self] in
|
|
292
|
+
guard let self, gen == self.generation else { return }
|
|
293
|
+
self.fireInterfaceReportsReady()
|
|
250
294
|
}
|
|
251
|
-
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func fireInterfaceReportsReady() {
|
|
298
|
+
let ready = interfaceReportsReady
|
|
299
|
+
interfaceReportsReady = nil
|
|
300
|
+
ready?()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// Every check samples interfaces first, so a detach or replacement between path updates
|
|
304
|
+
/// is recorded in the session identity before it is used.
|
|
305
|
+
private func observeInterfaces() -> HotspotInterfaceSnapshot {
|
|
306
|
+
let snapshot = Self.interfaceSnapshot()
|
|
307
|
+
wifiIdentity.observe(snapshot)
|
|
308
|
+
return snapshot
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private func selectBinding() -> HotspotInterfaceBinding? {
|
|
312
|
+
HotspotInterfacePolicy.select(snapshot: observeInterfaces(), identity: wifiIdentity, gateway: gatewayAddress)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private func isBindingIntact(_ binding: HotspotInterfaceBinding) -> Bool {
|
|
316
|
+
let snapshot = observeInterfaces()
|
|
317
|
+
return HotspotInterfacePolicy.isIntact(binding, snapshot: snapshot, identity: wifiIdentity)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private func bind(_ selected: HotspotInterfaceBinding) {
|
|
321
|
+
binding = selected
|
|
322
|
+
logger.info("HOTSPOT_JOIN address_verified interface=\(selected.interface, privacy: .public)")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// Interface names and their IPv4 addresses only; never credentials or SSIDs.
|
|
326
|
+
private func wifiDiagnostic() -> String {
|
|
327
|
+
let snapshot = observeInterfaces()
|
|
328
|
+
let wifi = Set(wifiIdentity.trusted.keys)
|
|
329
|
+
let addresses = snapshot.addresses.filter { wifi.contains($0.name) }.map { "\($0.name)=\($0.ipv4)" }
|
|
330
|
+
return "Wi-Fi interfaces=\(wifi.isEmpty ? "none" : wifi.sorted().joined(separator: ",")), " +
|
|
331
|
+
"Wi-Fi IPv4=\(addresses.isEmpty ? "none" : addresses.sorted().joined(separator: ","))"
|
|
252
332
|
}
|
|
253
333
|
|
|
254
334
|
private func waitForLocalAccess(address: String, generation gen: Int) {
|
|
@@ -281,32 +361,46 @@ public final class GlassesHotspotNetwork {
|
|
|
281
361
|
private func finishLeave() {
|
|
282
362
|
generation += 1
|
|
283
363
|
localAccess?.cancel(); localAccess = nil
|
|
284
|
-
|
|
364
|
+
interfaceMonitors.forEach { $0.cancel() }
|
|
365
|
+
interfaceMonitors = []
|
|
366
|
+
wifiIdentity = HotspotWifiIdentity()
|
|
367
|
+
monitorsReported = []
|
|
368
|
+
interfaceReportsReady = nil
|
|
285
369
|
if let ssid, ownsConfiguration { NEHotspotConfigurationManager.shared.removeConfiguration(forSSID: ssid) }
|
|
286
370
|
ownsConfiguration = false
|
|
287
371
|
ssid = nil
|
|
288
|
-
|
|
372
|
+
binding = nil
|
|
289
373
|
gatewayAddress = nil
|
|
290
374
|
let replies = leaveReplies
|
|
291
375
|
leaveReplies.removeAll()
|
|
292
376
|
replies.forEach { $0() }
|
|
293
377
|
}
|
|
294
378
|
|
|
295
|
-
|
|
379
|
+
/// Every attached interface's kernel index (its AF_LINK row) and every IPv4 address with its
|
|
380
|
+
/// interface name and up state. Selection belongs to HotspotInterfacePolicy; an interface
|
|
381
|
+
/// name alone never identifies Wi-Fi. A failed read yields an empty sample, which fails closed.
|
|
382
|
+
static func interfaceSnapshot() -> HotspotInterfaceSnapshot {
|
|
296
383
|
var interfaces: UnsafeMutablePointer<ifaddrs>?
|
|
297
|
-
guard getifaddrs(&interfaces) == 0 else { return
|
|
384
|
+
guard getifaddrs(&interfaces) == 0 else { return HotspotInterfaceSnapshot(links: [:], addresses: []) }
|
|
298
385
|
defer { freeifaddrs(interfaces) }
|
|
386
|
+
var links: [String: Int] = [:]
|
|
387
|
+
var rows: [HotspotInterfaceAddress] = []
|
|
299
388
|
var cursor = interfaces
|
|
300
389
|
while let item = cursor {
|
|
301
390
|
defer { cursor = item.pointee.ifa_next }
|
|
302
391
|
let value = item.pointee
|
|
303
|
-
guard
|
|
304
|
-
|
|
392
|
+
guard let address = value.ifa_addr else { continue }
|
|
393
|
+
let name = String(cString: value.ifa_name)
|
|
394
|
+
if address.pointee.sa_family == UInt8(AF_LINK) {
|
|
395
|
+
let index = address.withMemoryRebound(to: sockaddr_dl.self, capacity: 1) { Int($0.pointee.sdl_index) }
|
|
396
|
+
if index > 0 { links[name] = index }
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
guard address.pointee.sa_family == UInt8(AF_INET) else { continue }
|
|
305
400
|
var host = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
306
401
|
guard getnameinfo(address, socklen_t(address.pointee.sa_len), &host, socklen_t(host.count), nil, 0, NI_NUMERICHOST) == 0 else { continue }
|
|
307
|
-
|
|
308
|
-
if LocalMediaPolicy.isPrivate(ip) { return ip }
|
|
402
|
+
rows.append(HotspotInterfaceAddress(name: name, isUp: value.ifa_flags & UInt32(IFF_UP) != 0, ipv4: String(cString: host)))
|
|
309
403
|
}
|
|
310
|
-
return
|
|
404
|
+
return HotspotInterfaceSnapshot(links: links, addresses: rows)
|
|
311
405
|
}
|
|
312
406
|
}
|