@mentra/glasses-media 3.3.0-dev.820 → 3.3.0-dev.821

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.
@@ -18,10 +18,14 @@ public struct HotspotInterfaceAddress: Equatable {
18
18
  public struct HotspotInterfaceSnapshot: Equatable {
19
19
  public let links: [String: Int]
20
20
  public let addresses: [HotspotInterfaceAddress]
21
+ /// Attached interfaces whose `AF_LINK` type is `IFT_ETHER`. Apple platforms report Wi-Fi and
22
+ /// wired Ethernet this way; cellular, VPN tunnel and loopback links have other types.
23
+ public let ethernetLinks: Set<String>
21
24
 
22
- public init(links: [String: Int], addresses: [HotspotInterfaceAddress]) {
25
+ public init(links: [String: Int], addresses: [HotspotInterfaceAddress], ethernetLinks: Set<String> = []) {
23
26
  self.links = links
24
27
  self.addresses = addresses
28
+ self.ethernetLinks = ethernetLinks
25
29
  }
26
30
  }
27
31
 
@@ -87,6 +91,13 @@ public struct HotspotWifiIdentity: Equatable {
87
91
  detached.insert(name)
88
92
  }
89
93
  }
94
+
95
+ /// Pin a binding chosen from hotspot client evidence (see `HotspotInterfacePolicy.select`)
96
+ /// as this session's Wi-Fi interface. It then ends exactly like a reported one.
97
+ public mutating func adopt(_ binding: HotspotInterfaceBinding) {
98
+ guard !rejected.contains(binding.interface), !detached.contains(binding.interface) else { return }
99
+ trusted[binding.interface] = binding.index
100
+ }
90
101
  }
91
102
 
92
103
  /// Wi-Fi is not always `en0`: a Mac can have built-in Ethernet on `en0` and Wi-Fi on `en1`.
@@ -96,11 +107,33 @@ public enum HotspotInterfacePolicy {
96
107
  /// The unique up IPv4 address, on an interface this session trusts as Wi-Fi and that is still
97
108
  /// attached with the same index, that can be the hotspot client. With a BLE-reported gateway
98
109
  /// it must be a valid client on that gateway's /24. Missing or ambiguous candidates return nil.
110
+ ///
111
+ /// A phone on cellular has no Wi-Fi path when the join starts, and after association iOS
112
+ /// leaves the internet-less glasses AP out of both paths, so no Wi-Fi report ever arrives.
113
+ /// Only when the session has no trusted Wi-Fi interface, the hotspot client address is the
114
+ /// evidence instead: the unique valid client of the BLE gateway's /24 on an attached
115
+ /// Ethernet-type link that no path has reported as non-Wi-Fi. Callers bind it only after the
116
+ /// glasses SSID matched, then require a connection from it to reach the gateway, and must
117
+ /// `adopt` it so the session pins that interface.
99
118
  public static func select(snapshot: HotspotInterfaceSnapshot, identity: HotspotWifiIdentity,
100
119
  gateway: String?) -> HotspotInterfaceBinding?
120
+ {
121
+ if !identity.trusted.isEmpty {
122
+ return unique(snapshot, gateway: gateway) { identity.trusted[$0] }
123
+ }
124
+ guard let gateway else { return nil }
125
+ return unique(snapshot, gateway: gateway) { name in
126
+ guard snapshot.ethernetLinks.contains(name), !identity.rejected.contains(name),
127
+ !identity.detached.contains(name) else { return nil }
128
+ return snapshot.links[name]
129
+ }
130
+ }
131
+
132
+ private static func unique(_ snapshot: HotspotInterfaceSnapshot, gateway: String?,
133
+ index eligibleIndex: (String) -> Int?) -> HotspotInterfaceBinding?
101
134
  {
102
135
  let bindings = Set(snapshot.addresses.compactMap { row -> HotspotInterfaceBinding? in
103
- guard let index = identity.trusted[row.name], snapshot.links[row.name] == index,
136
+ guard let index = eligibleIndex(row.name), snapshot.links[row.name] == index,
104
137
  row.isUp, LocalMediaPolicy.isPrivate(row.ipv4) else { return nil }
105
138
  if let gateway, !LocalMediaPolicy.isHotspotClientAddress(row.ipv4, gateway: gateway) { return nil }
106
139
  return HotspotInterfaceBinding(interface: row.name, index: index, address: row.ipv4)
@@ -257,6 +257,8 @@ public final class GlassesHotspotNetwork {
257
257
  /// a Wi-Fi type constraint on a usable hotspot route. Both paths' interface types feed one
258
258
  /// session identity (HotspotWifiIdentity): a positive Wi-Fi report outlives later omission
259
259
  /// only while the same interface stays attached; contrary reports and replacement fail closed.
260
+ /// A phone that starts on cellular never gets a report; HotspotInterfacePolicy then uses
261
+ /// the hotspot client address as the evidence.
260
262
  private func startInterfaceMonitors(generation gen: Int) {
261
263
  interfaceMonitors.forEach { $0.cancel() }
262
264
  let monitors = [NWPathMonitor(requiredInterfaceType: .wifi), NWPathMonitor()]
@@ -318,8 +320,10 @@ public final class GlassesHotspotNetwork {
318
320
  }
319
321
 
320
322
  private func bind(_ selected: HotspotInterfaceBinding) {
323
+ let evidence = wifiIdentity.trusted[selected.interface] == selected.index ? "wifi_report" : "hotspot_client"
324
+ wifiIdentity.adopt(selected)
321
325
  binding = selected
322
- logger.info("HOTSPOT_JOIN address_verified interface=\(selected.interface, privacy: .public)")
326
+ logger.info("HOTSPOT_JOIN address_verified interface=\(selected.interface, privacy: .public) evidence=\(evidence, privacy: .public)")
323
327
  }
324
328
 
325
329
  /// Interface names and their IPv4 addresses only; never credentials or SSIDs.
@@ -327,8 +331,11 @@ public final class GlassesHotspotNetwork {
327
331
  let snapshot = observeInterfaces()
328
332
  let wifi = Set(wifiIdentity.trusted.keys)
329
333
  let addresses = snapshot.addresses.filter { wifi.contains($0.name) }.map { "\($0.name)=\($0.ipv4)" }
334
+ let other = snapshot.addresses.filter { !wifi.contains($0.name) && LocalMediaPolicy.isPrivate($0.ipv4) }
335
+ .map { "\($0.name)\(snapshot.ethernetLinks.contains($0.name) ? "" : "(non-ethernet)")=\($0.ipv4)" }
330
336
  return "Wi-Fi interfaces=\(wifi.isEmpty ? "none" : wifi.sorted().joined(separator: ",")), " +
331
- "Wi-Fi IPv4=\(addresses.isEmpty ? "none" : addresses.sorted().joined(separator: ","))"
337
+ "Wi-Fi IPv4=\(addresses.isEmpty ? "none" : addresses.sorted().joined(separator: ",")), " +
338
+ "other private IPv4=\(other.isEmpty ? "none" : other.sorted().joined(separator: ","))"
332
339
  }
333
340
 
334
341
  private func waitForLocalAccess(address: String, generation gen: Int) {
@@ -376,14 +383,16 @@ public final class GlassesHotspotNetwork {
376
383
  replies.forEach { $0() }
377
384
  }
378
385
 
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.
386
+ /// Every attached interface's kernel index and link type (its AF_LINK row) and every IPv4
387
+ /// address with its interface name and up state. Selection belongs to HotspotInterfacePolicy;
388
+ /// an interface name alone never identifies Wi-Fi. A failed read yields an empty sample, which
389
+ /// fails closed.
382
390
  static func interfaceSnapshot() -> HotspotInterfaceSnapshot {
383
391
  var interfaces: UnsafeMutablePointer<ifaddrs>?
384
392
  guard getifaddrs(&interfaces) == 0 else { return HotspotInterfaceSnapshot(links: [:], addresses: []) }
385
393
  defer { freeifaddrs(interfaces) }
386
394
  var links: [String: Int] = [:]
395
+ var ethernetLinks: Set<String> = []
387
396
  var rows: [HotspotInterfaceAddress] = []
388
397
  var cursor = interfaces
389
398
  while let item = cursor {
@@ -392,8 +401,13 @@ public final class GlassesHotspotNetwork {
392
401
  guard let address = value.ifa_addr else { continue }
393
402
  let name = String(cString: value.ifa_name)
394
403
  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 }
404
+ let (index, type) = address.withMemoryRebound(to: sockaddr_dl.self, capacity: 1) {
405
+ (Int($0.pointee.sdl_index), Int32($0.pointee.sdl_type))
406
+ }
407
+ if index > 0 {
408
+ links[name] = index
409
+ if type == IFT_ETHER { ethernetLinks.insert(name) }
410
+ }
397
411
  continue
398
412
  }
399
413
  guard address.pointee.sa_family == UInt8(AF_INET) else { continue }
@@ -401,6 +415,6 @@ public final class GlassesHotspotNetwork {
401
415
  guard getnameinfo(address, socklen_t(address.pointee.sa_len), &host, socklen_t(host.count), nil, 0, NI_NUMERICHOST) == 0 else { continue }
402
416
  rows.append(HotspotInterfaceAddress(name: name, isUp: value.ifa_flags & UInt32(IFF_UP) != 0, ipv4: String(cString: host)))
403
417
  }
404
- return HotspotInterfaceSnapshot(links: links, addresses: rows)
418
+ return HotspotInterfaceSnapshot(links: links, addresses: rows, ethernetLinks: ethernetLinks)
405
419
  }
406
420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/glasses-media",
3
- "version": "3.3.0-dev.820",
3
+ "version": "3.3.0-dev.821",
4
4
  "description": "Shared native glasses media receivers and local network transport",
5
5
  "license": "MIT",
6
6
  "author": "Mentra Community",