• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

homebridge / ciao / 24959623340

26 Apr 2026 02:59PM UTC coverage: 36.584% (+2.8%) from 33.809%
24959623340

Pull #68

github

web-flow
Merge 34146bdf1 into 88c230ac4
Pull Request #68: v1.3.7

461 of 1646 branches covered (28.01%)

Branch coverage included in aggregate %.

51 of 83 new or added lines in 4 files covered. (61.45%)

4 existing lines in 3 files now uncovered.

1287 of 3132 relevant lines covered (41.09%)

29.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

14.13
/src/NetworkManager.ts
1
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
2
import assert from "assert";
6✔
3
import childProcess from "child_process";
6✔
4
import createDebug from "debug";
6✔
5
import { EventEmitter } from "events";
6✔
6
import deepEqual from "fast-deep-equal";
6✔
7
import net from "net";
6✔
8
import os, { NetworkInterfaceInfo } from "os";
6✔
9
import { getNetAddress } from "./util/domain-formatter";
6✔
10
import Timeout = NodeJS.Timeout;
11

12
const debug = createDebug("ciao:NetworkManager");
6✔
13

14
export type InterfaceName = string;
15
export type MacAddress = string;
16

17
export type IPv4Address = string;
18
export type IPv6Address = string;
19
export type IPAddress = IPv4Address | IPv6Address;
20

21
export const enum IPFamily {
6✔
22
  IPv4 = "IPv4",
6✔
23
  IPv6 = "IPv6",
6✔
24
}
25

26
export const enum WifiState {
6✔
27
  UNDEFINED,
6✔
28
  NOT_A_WIFI_INTERFACE,
6✔
29
  NOT_ASSOCIATED,
6✔
30
  CONNECTED,
6✔
31
}
32

33
export interface NetworkInterface {
34
  name: InterfaceName;
35
  loopback: boolean;
36
  mac: MacAddress;
37

38
  // one of ipv4 or ipv6 will be present, most of the time even both
39
  ipv4?: IPv4Address;
40
  ip4Netmask?: IPv4Address;
41
  ipv4Netaddress?: IPv4Address;
42
  ipv6?: IPv6Address; // link-local ipv6 fe80::/10
43
  ipv6Netmask?: IPv6Address;
44

45
  globallyRoutableIpv6?: IPv6Address; // first routable ipv6 address
46
  globallyRoutableIpv6Netmask?: IPv6Address;
47

48
  uniqueLocalIpv6?: IPv6Address; // fc00::/7 (those are the fd ula addresses; fc prefix isn't really used, used for globally assigned ula)
49
  uniqueLocalIpv6Netmask?: IPv6Address;
50
}
51

52
export interface NetworkUpdate {
53
  added?: NetworkInterface[];
54
  removed?: NetworkInterface[];
55
  changes?: InterfaceChange[];
56
}
57

58
export interface InterfaceChange {
59
  name: InterfaceName;
60

61
  outdatedIpv4?: IPv4Address;
62
  updatedIpv4?: IPv4Address;
63

64
  outdatedIpv6?: IPv6Address;
65
  updatedIpv6?: IPv6Address;
66

67
  outdatedGloballyRoutableIpv6?: IPv6Address;
68
  updatedGloballyRoutableIpv6?: IPv6Address;
69

70
  outdatedUniqueLocalIpv6?: IPv6Address;
71
  updatedUniqueLocalIpv6?: IPv6Address;
72
}
73

74
export interface NetworkManagerOptions {
75
  interface?: (InterfaceName | IPAddress) | (InterfaceName | IPAddress)[];
76
  excludeIpv6?: boolean;
77
  excludeIpv6Only?: boolean;
78
}
79

80
export const enum NetworkManagerEvent {
6✔
81
  NETWORK_UPDATE = "network-update",
6✔
82
}
83

84
export declare interface NetworkManager {
85

86
  on(event: "network-update", listener: (networkUpdate: NetworkUpdate) => void): this;
87

88
  emit(event: "network-update", networkUpdate: NetworkUpdate): boolean;
89

90
}
91

92
/**
93
 * The NetworkManager maintains a representation of the network interfaces define on the host system.
94
 * It periodically checks for updated network information.
95
 *
96
 * The NetworkManager makes the following decision when checking for interfaces:
97
 * * First of all it gathers the default network interface of the system (by checking the routing table of the os)
98
 * * The following interfaces are going to be tracked:
99
 *   * The loopback interface
100
 *   * All interfaces which match the subnet of the default interface
101
 *   * All interfaces which contain a globally unique (aka globally routable) ipv6 address
102
 */
103
export class NetworkManager extends EventEmitter {
6✔
104

105
  private static readonly SPACE_PATTERN = /\s+/g;
6✔
106
  private static readonly NOTHING_FOUND_MESSAGE = "no interfaces found";
6✔
107

108
  private static readonly POLLING_TIME = 15 * 1000; // 15 seconds
6✔
109

110
  private readonly restrictedInterfaces?: InterfaceName[];
111
  private readonly excludeIpv6: boolean; // if defined, we only pick ipv4 address records from an available network interface
112
  private readonly excludeIpv6Only: boolean;
113

114
  private currentInterfaces: Map<InterfaceName, NetworkInterface> = new Map();
9✔
115
  /**
116
   * A subset of our network interfaces, holding only loopback interfaces (or what node considers "internal").
117
   */
118
  private loopbackInterfaces: Map<InterfaceName, NetworkInterface> = new Map();
9✔
119
  private initPromise?: Promise<void>;
120

121
  private currentTimer?: Timeout;
122

123
  constructor(options?: NetworkManagerOptions) {
124
    super();
9✔
125
    this.setMaxListeners(100); // we got one listener for every Responder, 100 should be fine for now
9✔
126

127
    if (options && options.interface) {
9!
128
      let interfaces: (InterfaceName | IPAddress)[];
129

130
      if (typeof options.interface === "string") {
×
131
        interfaces = [options.interface];
×
132
      } else if (Array.isArray(options.interface)) {
×
133
        interfaces = options.interface;
×
134
      } else {
135
        throw new Error("Found invalid type for 'interfaces' NetworkManager option!");
×
136
      }
137

138
      const restrictedInterfaces: InterfaceName[] = [];
×
139

140
      for (const iface of interfaces) {
×
141
        if (net.isIP(iface)) {
×
142
          const interfaceName = NetworkManager.resolveInterface(iface);
×
143
          if (interfaceName) {
×
144
            restrictedInterfaces.push(interfaceName);
×
145
          } else {
146
            console.log("CIAO: Interface was specified as ip (%s), though couldn't find a matching interface for the given address.", options.interface);
×
147
          }
148
        } else {
149
          restrictedInterfaces.push(iface);
×
150
        }
151
      }
152

153
      if (restrictedInterfaces.length === 0) {
×
154
        console.log("CIAO: 'restrictedInterfaces' array was empty. Going to fallback to bind on all available interfaces.");
×
155
      } else {
156
        this.restrictedInterfaces = restrictedInterfaces;
×
157
      }
158
    }
159
    this.excludeIpv6 = !!(options && options.excludeIpv6);
9✔
160
    this.excludeIpv6Only = this.excludeIpv6 || !!(options && options.excludeIpv6Only);
9✔
161

162
    if (options) {
9!
163
      debug("Created NetworkManager with options: %s", JSON.stringify(options));
9✔
164
    }
165

166
    this.initPromise = new Promise(resolve => {
9✔
167
      this.getCurrentNetworkInterfaces().then(map => {
9✔
168
        this.currentInterfaces = map;
×
169

170
        const otherInterfaces: InterfaceName[] = Object.keys(os.networkInterfaces());
×
171

172
        const interfaceNames: InterfaceName[] = [];
×
173
        for (const name of this.currentInterfaces.keys()) {
×
174
          interfaceNames.push(name);
×
175

176
          const index = otherInterfaces.indexOf(name);
×
177
          if (index !== -1) {
×
178
            otherInterfaces.splice(index, 1);
×
179
          }
180
        }
181
        debug("Initial networks [%s] ignoring [%s]", interfaceNames.join(", "), otherInterfaces.join(", "));
×
182

183
        this.initPromise = undefined;
×
184
        resolve();
×
185

186
        this.scheduleNextJob();
×
187
      });
188
    });
189
  }
190

191
  public async waitForInit(): Promise<void> {
192
    if (this.initPromise) {
×
193
      await this.initPromise;
×
194
    }
195
  }
196

197
  public shutdown(): void {
198
    if (this.currentTimer) {
×
199
      clearTimeout(this.currentTimer);
×
200
      this.currentTimer = undefined;
×
201
    }
202

203
    this.removeAllListeners();
×
204
  }
205

206
  public getInterfaceMap(): Map<InterfaceName, NetworkInterface> {
207
    if (this.initPromise) {
×
208
      assert.fail("Not yet initialized!");
×
209
    }
210
    return this.currentInterfaces;
×
211
  }
212

213
  public getInterface(name: InterfaceName): NetworkInterface | undefined {
214
    if (this.initPromise) {
×
215
      assert.fail("Not yet initialized!");
×
216
    }
217
    return this.currentInterfaces.get(name);
×
218
  }
219

220
  public isLoopbackNetaddressV4(netaddress: IPv4Address): boolean {
221
    for (const networkInterface of this.loopbackInterfaces.values()) {
×
222
      if (networkInterface.ipv4Netaddress === netaddress) {
×
223
        return true;
×
224
      }
225
    }
226

227
    return false;
×
228
  }
229

230
  private scheduleNextJob(): void {
231
    this.currentTimer = setTimeout(this.checkForNewInterfaces.bind(this), NetworkManager.POLLING_TIME);
×
232
    this.currentTimer.unref(); // this timer won't prevent shutdown
×
233
  }
234

235
  private async checkForNewInterfaces(): Promise<void> {
236
    const latestInterfaces = await this.getCurrentNetworkInterfaces();
×
237
    if (!this.currentTimer) { // if the timer is undefined, NetworkManager was shut down
×
238
      return;
×
239
    }
240

241
    let added: NetworkInterface[] | undefined = undefined;
×
242
    let removed: NetworkInterface[] | undefined = undefined;
×
243
    let changes: InterfaceChange[] | undefined = undefined;
×
244

245
    for (const [name, networkInterface] of latestInterfaces) {
×
246
      const currentInterface = this.currentInterfaces.get(name);
×
247

248
      if (currentInterface) { // the interface could potentially have changed
×
249
        if (!deepEqual(currentInterface, networkInterface)) {
×
250
          // indeed the interface changed
251
          const change: InterfaceChange = {
×
252
            name: name,
253
          };
254

255
          if (currentInterface.ipv4 !== networkInterface.ipv4) { // check for changed ipv4
×
256
            if (currentInterface.ipv4) {
×
257
              change.outdatedIpv4 = currentInterface.ipv4;
×
258
            }
259
            if (networkInterface.ipv4) {
×
260
              change.updatedIpv4 = networkInterface.ipv4;
×
261
            }
262
          }
263

264
          if (currentInterface.ipv6 !== networkInterface.ipv6) { // check for changed link-local ipv6
×
265
            if (currentInterface.ipv6) {
×
266
              change.outdatedIpv6 = currentInterface.ipv6;
×
267
            }
268
            if (networkInterface.ipv6) {
×
269
              change.updatedIpv6 = networkInterface.ipv6;
×
270
            }
271
          }
272

273
          if (currentInterface.globallyRoutableIpv6 !== networkInterface.globallyRoutableIpv6) { // check for changed routable ipv6
×
274
            if (currentInterface.globallyRoutableIpv6) {
×
275
              change.outdatedGloballyRoutableIpv6 = currentInterface.globallyRoutableIpv6;
×
276
            }
277
            if (networkInterface.globallyRoutableIpv6) {
×
278
              change.updatedGloballyRoutableIpv6 = networkInterface.globallyRoutableIpv6;
×
279
            }
280
          }
281

282
          if (currentInterface.uniqueLocalIpv6 !== networkInterface.uniqueLocalIpv6) { // check for changed ula
×
283
            if (currentInterface.uniqueLocalIpv6) {
×
284
              change.outdatedUniqueLocalIpv6 = currentInterface.uniqueLocalIpv6;
×
285
            }
286
            if (networkInterface.uniqueLocalIpv6) {
×
287
              change.updatedUniqueLocalIpv6 = networkInterface.uniqueLocalIpv6;
×
288
            }
289
          }
290

291
          this.currentInterfaces.set(name, networkInterface);
×
292
          if (networkInterface.loopback) {
×
293
            this.loopbackInterfaces.set(name, networkInterface);
×
294
          }
295

296
          (changes ??= []).push(change);
×
297
        }
298
      } else { // new interface was added/started
299
        this.currentInterfaces.set(name, networkInterface);
×
300
        if (networkInterface.loopback) {
×
301
          this.currentInterfaces.set(name, networkInterface);
×
302
        }
303

304
        (added ??= []).push(networkInterface);
×
305
      }
306
    }
307

308
    // at this point we updated any existing interfaces and added all new interfaces
309
    // thus if the length of below is not the same interface must have been removed
310
    // this check ensures that we do not unnecessarily loop twice through our interfaces
311
    if (this.currentInterfaces.size !== latestInterfaces.size) {
×
312
      for (const [name, networkInterface] of this.currentInterfaces) {
×
313
        if (!latestInterfaces.has(name)) { // interface was removed
×
314
          this.currentInterfaces.delete(name);
×
315
          this.loopbackInterfaces.delete(name);
×
316

317
          (removed ??= []).push(networkInterface);
×
318

319
        }
320
      }
321
    }
322

323
    if (added || removed || changes) { // emit an event only if anything changed
×
324
      const addedString = added? added.map(iface => iface.name).join(","): "";
×
325
      const removedString = removed? removed.map(iface => iface.name).join(","): "";
×
326
      const changesString = changes? changes.map(iface => {
×
327
        let string = `{ name: ${iface.name} `;
×
328
        if (iface.outdatedIpv4 || iface.updatedIpv4) {
×
329
          string += `, ${iface.outdatedIpv4} -> ${iface.updatedIpv4} `;
×
330
        }
331
        if (iface.outdatedIpv6 || iface.updatedIpv6) {
×
332
          string += `, ${iface.outdatedIpv6} -> ${iface.updatedIpv6} `;
×
333
        }
334
        if (iface.outdatedGloballyRoutableIpv6 || iface.updatedGloballyRoutableIpv6) {
×
335
          string += `, ${iface.outdatedGloballyRoutableIpv6} -> ${iface.updatedGloballyRoutableIpv6} `;
×
336
        }
337
        if (iface.outdatedUniqueLocalIpv6 || iface.updatedUniqueLocalIpv6) {
×
338
          string += `, ${iface.outdatedUniqueLocalIpv6} -> ${iface.updatedUniqueLocalIpv6} `;
×
339
        }
340
        return string + "}";
×
341
      }).join(","): "";
342

343
      debug("Detected network changes: added: [%s], removed: [%s], changes: [%s]!", addedString, removedString, changesString);
×
344

345
      this.emit(NetworkManagerEvent.NETWORK_UPDATE, {
×
346
        added: added,
347
        removed: removed,
348
        changes: changes,
349
      });
350
    }
351

352
    this.scheduleNextJob();
×
353
  }
354

355
  private async getCurrentNetworkInterfaces(): Promise<Map<InterfaceName, NetworkInterface>> {
356
    let names: InterfaceName[];
357
    if (this.restrictedInterfaces) {
9!
358
      names = this.restrictedInterfaces;
×
359

360
      const loopback = NetworkManager.getLoopbackInterface();
×
361
      if (!names.includes(loopback)) {
×
362
        names.push(loopback);
×
363
      }
364
    } else {
365
      try {
9✔
366
        names = await NetworkManager.getNetworkInterfaceNames();
9✔
367
      } catch (error) {
368
        debug(`WARNING Detecting network interfaces for platform '${os.platform()}' failed. Trying to assume network interfaces! (${error.message})`);
×
369
        // fallback way of gathering network interfaces (remember, there are docker images where the arp command is not installed)
370
        names = NetworkManager.assumeNetworkInterfaceNames();
×
371
      }
372
    }
373

374

375
    const interfaces: Map<InterfaceName, NetworkInterface> = new Map();
×
376
    const networkInterfaces = os.networkInterfaces();
×
377

378
    for (const name of names) {
×
379
      const infos = networkInterfaces[name];
×
380
      if (!infos) {
×
381
        continue;
×
382
      }
383

384
      let ipv4Info: NetworkInterfaceInfo | undefined = undefined;
×
385
      let ipv6Info: NetworkInterfaceInfo | undefined = undefined;
×
386
      let routableIpv6Info: NetworkInterfaceInfo | undefined = undefined;
×
387
      let uniqueLocalIpv6Info: NetworkInterfaceInfo | undefined = undefined;
×
388
      let internal = false;
×
389

390
      for (const info of infos) {
×
391
        if (info.internal) {
×
392
          internal = true;
×
393
        }
394

395
        // @ts-expect-error Nodejs 18+ uses the number 4 instead of the string "IPv4"
396
        if ((info.family === "IPv4" || info.family === 4) && !ipv4Info) {
×
397
          ipv4Info = info;
×
398
        // @ts-expect-error Nodejs 18+ uses the number 4 instead of the string "IPv4"
399
        } else if (info.family === "IPv6" || info.family === 6) {
×
400
          if (this.excludeIpv6) {
×
401
            continue;
×
402
          }
403

404
          if (info.scopeid && !ipv6Info) { // we only care about non zero scope (aka link-local ipv6)
×
405
            ipv6Info = info;
×
406
          } else if (info.scopeid === 0) { // global routable ipv6
×
407
            if (info.address.startsWith("fc") || info.address.startsWith("fd")) {
×
408
              if (!uniqueLocalIpv6Info) {
×
409
                uniqueLocalIpv6Info = info;
×
410
              }
411
            } else if (!routableIpv6Info) {
×
412
              routableIpv6Info = info;
×
413
            }
414
          }
415
        }
416

417
        if (ipv4Info && ipv6Info && routableIpv6Info && uniqueLocalIpv6Info) {
×
418
          break;
×
419
        }
420
      }
421

422
      assert(ipv4Info || ipv6Info, "Could not find valid addresses for interface '" + name + "'");
×
423

424
      if (this.excludeIpv6Only && !ipv4Info) {
×
425
        continue;
×
426
      }
427

428
      const networkInterface: NetworkInterface = {
×
429
        name: name,
430
        loopback: internal,
431
        mac: (ipv4Info?.mac || ipv6Info?.mac)!,
×
432
      };
433

434
      if (ipv4Info) {
×
435
        networkInterface.ipv4 = ipv4Info.address;
×
436
        networkInterface.ip4Netmask = ipv4Info.netmask;
×
437
        networkInterface.ipv4Netaddress = getNetAddress(ipv4Info.address, ipv4Info.netmask);
×
438
      }
439

440
      if (ipv6Info) {
×
441
        networkInterface.ipv6 = ipv6Info.address;
×
442
        networkInterface.ipv6Netmask = ipv6Info.netmask;
×
443
      }
444

445
      if (routableIpv6Info) {
×
446
        networkInterface.globallyRoutableIpv6 = routableIpv6Info.address;
×
447
        networkInterface.globallyRoutableIpv6Netmask = routableIpv6Info.netmask;
×
448
      }
449

450
      if (uniqueLocalIpv6Info) {
×
451
        networkInterface.uniqueLocalIpv6 = uniqueLocalIpv6Info.address;
×
452
        networkInterface.uniqueLocalIpv6Netmask = uniqueLocalIpv6Info.netmask;
×
453
      }
454

455
      interfaces.set(name, networkInterface);
×
456
    }
457

458
    return interfaces;
×
459
  }
460

461
  public static resolveInterface(address: IPAddress): InterfaceName | undefined {
462
    let interfaceName: InterfaceName | undefined;
463

464
    outer: for (const [name, infoArray] of Object.entries(os.networkInterfaces())) {
×
465
      for (const info of infoArray ?? []) {
×
466
        if (info.address === address) {
×
467
          interfaceName = name;
×
468
          break outer; // exit out of both loops
×
469
        }
470
      }
471
    }
472

473
    return interfaceName;
×
474
  }
475

476
  private static async getNetworkInterfaceNames(): Promise<InterfaceName[]> {
477
    // this function will always include the loopback interface
478

479
    let promise: Promise<InterfaceName[]>;
480
    switch (os.platform()) {
9!
481
      case "win32":
482
        promise = NetworkManager.getWindowsNetworkInterfaces();
×
483
        break;
×
484
      case "linux": {
485
        promise = NetworkManager.getLinuxNetworkInterfaces();
9✔
486
        break;
9✔
487
      }
488
      case "darwin":
489
        promise = NetworkManager.getDarwinNetworkInterfaces();
×
490
        break;
×
491
      case "freebsd": {
492
        promise = NetworkManager.getFreeBSDNetworkInterfaces();
×
493
        break;
×
494
      }
495
      case "openbsd":
496
      case "sunos": {
497
        promise = NetworkManager.getOpenBSD_SUNOS_NetworkInterfaces();
×
498
        break;
×
499
      }
500
      default:
501
        debug("Found unsupported platform %s", os.platform());
×
502
        return Promise.reject(new Error("unsupported platform!"));
×
503
    }
504

505
    let names: InterfaceName[];
506
    try {
9✔
507
      names = await promise;
9✔
508
    } catch (error) {
509
      if (error.message !== NetworkManager.NOTHING_FOUND_MESSAGE) {
×
510
        throw error;
×
511
      }
512
      names = [];
×
513
    }
514
    const loopback = NetworkManager.getLoopbackInterface();
×
515

516
    if (!names.includes(loopback)) {
×
517
      names.unshift(loopback);
×
518
    }
519

520
    return promise;
×
521
  }
522

523
  private static assumeNetworkInterfaceNames(): InterfaceName[] {
524
    // this method is a fallback trying to calculate network related interfaces in an platform independent way
525

526
    const names: InterfaceName[] = [];
×
527
    Object.entries(os.networkInterfaces()).forEach(([name, infos]) => {
×
528
      for (const info of infos ?? []) {
×
529
        // we add the loopback interface or interfaces which got a unique (global or local) ipv6 address
530
        // we currently don't just add all interfaces with ipv4 addresses as are often interfaces like VPNs, container/vms related
531

532
        // unique global or unique local ipv6 addresses give an indication that we are truly connected to "the Internet"
533
        // as something like SLAAC must be going on
534
        // in the end
535
        // @ts-expect-error Nodejs 18+ uses the number 4/6 instead of the string "IPv4"/"IPv6"
536
        if (info.internal || (info.family === "IPv4" || info.family === 4) || (info.family === "IPv6" || info.family === 6) && info.scopeid === 0) {
×
537
          if (!names.includes(name)) {
×
538
            names.push(name);
×
539
          }
540
          break;
×
541
        }
542
      }
543
    });
544

545
    return names;
×
546
  }
547

548
  private static getLoopbackInterface(): InterfaceName {
549
    for (const [name, infos] of Object.entries(os.networkInterfaces())) {
×
550
      for (const info of infos ?? []) {
×
551
        if (info.internal) {
×
552
          return name;
×
553
        }
554
      }
555
    }
556

557
    throw new Error("Could not detect loopback interface!");
×
558
  }
559

560
  private static getWindowsNetworkInterfaces(): Promise<InterfaceName[]> {
561
    // does not return loopback interface
562
    return new Promise((resolve, reject) => {
×
NEW
563
      childProcess.exec("arp -a | findstr /C:\"---\"", { windowsHide: true }, (error, stdout) => {
×
564
        if (error) {
×
565
          reject(error);
×
566
          return;
×
567
        }
568

569
        const lines = stdout.split(os.EOL);
×
570

571
        const addresses: IPv4Address[] = [];
×
572
        for (let i = 0; i < lines.length - 1; i++) {
×
573
          const line = lines[i].trim().split(" ");
×
574

575
          if (line[line.length - 3]) {
×
576
            addresses.push(line[line.length - 3]);
×
577
          } else {
578
            debug(`WINDOWS: Failed to read interface name from line ${i}: '${lines[i]}'`);
×
579
          }
580
        }
581

582
        const names: InterfaceName[] = [];
×
583
        for (const address of addresses) {
×
584
          const name = NetworkManager.resolveInterface(address);
×
585
          if (name) {
×
586
            if (!names.includes(name)) {
×
587
              names.push(name);
×
588
            }
589
          } else {
590
            debug(`WINDOWS: Couldn't resolve to an interface name from '${address}'`);
×
591
          }
592
        }
593

594
        if (names.length) {
×
595
          resolve(names);
×
596
        } else {
597
          reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
×
598
        }
599
      });
600
    });
601
  }
602

603
  private static getDarwinNetworkInterfaces(): Promise<InterfaceName[]> {
604
    /*
605
     * Previous efforts used the routing table to get all relevant network interfaces.
606
     * Particularly using "netstat -r -f inet -n".
607
     * First attempt was to use the "default" interface to the 0.0.0.0 catch all route using "route get 0.0.0.0".
608
     * Though this fails when the router isn't connected to the internet, thus no "internet route" exists.
609
     */
610

611
    // does not return loopback interface
612
    return new Promise((resolve, reject) => {
×
613
      // for ipv6 "ndp -a -n |grep -v permanent" with filtering for "expired"
NEW
614
      childProcess.exec("arp -a -n -l", { windowsHide: true }, async (error, stdout) => {
×
615
        if (error) {
×
616
          reject(error);
×
617
          return;
×
618
        }
619

620
        const lines = stdout.split(os.EOL);
×
621
        const names: InterfaceName[] = [];
×
622

623
        for (let i = 1; i < lines.length - 1; i++) {
×
624
          const interfaceName = lines[i].trim().split(NetworkManager.SPACE_PATTERN)[4];
×
625
          if (!interfaceName) {
×
626
            debug(`DARWIN: Failed to read interface name from line ${i}: '${lines[i]}'`);
×
627
            continue;
×
628
          }
629

630
          if (!names.includes(interfaceName)) {
×
631
            names.push(interfaceName);
×
632
          }
633
        }
634

635
        const promises: Promise<void>[] = [];
×
636
        for (const name of names) {
×
637
          const promise = NetworkManager.getDarwinWifiNetworkState(name).then(state => {
×
638
            if (state !== WifiState.NOT_A_WIFI_INTERFACE && state !== WifiState.CONNECTED) {
×
639
              // removing wifi networks which are not connected to any networks
640
              const index = names.indexOf(name);
×
641
              if (index !== -1) {
×
642
                names.splice(index, 1);
×
643
              }
644
            }
645
          });
646

647
          promises.push(promise);
×
648
        }
649

650
        await Promise.all(promises);
×
651

652
        if (names.length) {
×
653
          resolve(names);
×
654
        } else {
655
          reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
×
656
        }
657
      });
658
    });
659
  }
660

661
  private static getLinuxNetworkInterfaces(): Promise<InterfaceName[]> {
662
    // does not return loopback interface
663
    return new Promise((resolve, reject) => {
21✔
664
      // "ip -o link show" lists all network interfaces (one per line) without exposing
665
      // neighbor/ARP table data about other machines on the network.
666
      // The -o flag ensures one-line-per-interface output for reliable parsing.
667
      childProcess.exec("ip -o link show", { windowsHide: true }, (error, stdout) => {
21✔
668
        if (error) {
12✔
669
          if (error.message.includes("ip: not found")) {
3!
670
            debug("LINUX: ip was not found on the system. Falling back to assuming network interfaces!");
×
671
            resolve(NetworkManager.assumeNetworkInterfaceNames());
×
672
            return;
×
673
          }
674

675
          reject(error);
3✔
676
          return;
3✔
677
        }
678

679
        const lines = stdout.split(os.EOL);
9✔
680
        const names: InterfaceName[] = [];
9✔
681

682
        for (let i = 0; i < lines.length - 1; i++) {
9✔
683
          const parts = lines[i].trim().split(NetworkManager.SPACE_PATTERN);
36✔
684

685
          // ip -o link show output format: "<index>: <name>: <flags> ..."
686
          // need at least 3 parts: index, name, flags
687
          if (parts.length < 3 || !parts[1] || !parts[2]) {
36!
NEW
688
            debug(`LINUX: Failed to parse interface from line ${i}: '${lines[i]}'`);
×
UNCOV
689
            continue;
×
690
          }
691

692
          // parts[1] is "<name>:" — strip the trailing colon
693
          const interfaceName = parts[1].replace(/:$/, "");
36✔
694
          if (!interfaceName) {
36!
695
            debug(`LINUX: Failed to read interface name from line ${i}: '${lines[i]}'`);
×
696
            continue;
×
697
          }
698

699
          // parts[2] contains the interface flags e.g. "<BROADCAST,MULTICAST,UP,LOWER_UP>"
700
          // skip loopback interfaces
701
          if (parts[2].includes("LOOPBACK")) {
36✔
702
            continue;
6✔
703
          }
704

705
          if (!names.includes(interfaceName)) {
30!
706
            names.push(interfaceName);
30✔
707
          }
708
        }
709

710
        if (names.length) {
9✔
711
          resolve(names);
6✔
712
        } else {
713
          reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
3✔
714
        }
715
      });
716
    });
717
  }
718

719
  private static getFreeBSDNetworkInterfaces(): Promise<InterfaceName[]> {
720
    // does not return loopback interface
721
    return new Promise((resolve, reject) => {
×
NEW
722
      childProcess.exec("arp -a -n", { windowsHide: true }, (error, stdout) => {
×
723
        if (error) {
×
724
          reject(error);
×
725
          return;
×
726
        }
727

728
        const lines = stdout.split(os.EOL);
×
729
        const names: InterfaceName[] = [];
×
730

731

732
        for (let i = 0; i < lines.length - 1; i++) {
×
733
          const interfaceName = lines[i].trim().split(NetworkManager.SPACE_PATTERN)[5];
×
734
          if (!interfaceName) {
×
735
            debug(`FreeBSD: Failed to read interface name from line ${i}: '${lines[i]}'`);
×
736
            continue;
×
737
          }
738

739
          if (!names.includes(interfaceName)) {
×
740
            names.push(interfaceName);
×
741
          }
742
        }
743

744
        if (names.length) {
×
745
          resolve(names);
×
746
        } else {
747
          reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
×
748
        }
749
      });
750
    });
751
  }
752

753
  private static getOpenBSD_SUNOS_NetworkInterfaces(): Promise<InterfaceName[]> {
754
    // does not return loopback interface
755
    return new Promise((resolve, reject) => {
×
756
      // for ipv6 something like "ndp -a -n | grep R" (grep for reachable; maybe exclude permanent?)
NEW
757
      childProcess.exec("arp -a -n", { windowsHide: true }, (error, stdout) => {
×
758
        if (error) {
×
759
          reject(error);
×
760
          return;
×
761
        }
762

763
        const interfaceArrayOffset = os.platform() === "sunos" ? 0 : 2;
×
764
        const lines = stdout.split(os.EOL);
×
765
        const names: InterfaceName[] = [];
×
766

767
        for (let i = 1; i < lines.length - 1; i++) {
×
768
          const interfaceName = lines[i].trim().split(NetworkManager.SPACE_PATTERN)[interfaceArrayOffset];
×
769
          if (!interfaceName) {
×
770
            debug(`${os.platform()}: Failed to read interface name from line ${i}: '${lines[i]}'`);
×
771
            continue;
×
772
          }
773

774
          if (!names.includes(interfaceName)) {
×
775
            names.push(interfaceName);
×
776
          }
777
        }
778

779
        if (names.length) {
×
780
          resolve(names);
×
781
        } else {
782
          reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
×
783
        }
784
      });
785
    });
786
  }
787

788
  private static getDarwinWifiNetworkState(name: InterfaceName): Promise<WifiState> {
789
    return new Promise(resolve => {
×
790
      /*
791
         * networksetup outputs the following in the listed scenarios:
792
         *
793
         * executed for an interface which is not a Wi-Fi interface:
794
         * "<name> is not a Wi-Fi interface.
795
         * Error: Error obtaining wireless information."
796
         *
797
         * executed for a turned off Wi-Fi interface:
798
         * "You are not associated with an AirPort network.
799
         * Wi-Fi power is currently off."
800
         *
801
         * executed for a turned on Wi-Fi interface which is not connected:
802
         * "You are not associated with an AirPort network."
803
         *
804
         * executed for a connected Wi-Fi interface:
805
         * "Current Wi-Fi Network: <network name>"
806
         *
807
         * Other messages handled here.
808
         * "All Wi-Fi network services are disabled": encountered on macOS VM machines
809
         */
NEW
810
      childProcess.exec("networksetup -getairportnetwork " + name, { windowsHide: true }, (error, stdout) => {
×
811
        if (error) {
×
812
          if (stdout.includes("not a Wi-Fi interface")) {
×
813
            resolve(WifiState.NOT_A_WIFI_INTERFACE);
×
814
            return;
×
815
          }
816

817
          console.log(`CIAO WARN: While checking networksetup for ${name} encountered an error (${error.message}) with output: ${stdout.replace(os.EOL, "; ")}`);
×
818
          resolve(WifiState.UNDEFINED);
×
819
          return;
×
820
        }
821

822
        let wifiState = WifiState.UNDEFINED;
×
823
        if (stdout.includes("not a Wi-Fi interface")) {
×
824
          wifiState = WifiState.NOT_A_WIFI_INTERFACE;
×
825
        } else if (stdout.includes("Current Wi-Fi Network")) {
×
826
          wifiState = WifiState.CONNECTED;
×
827
        } else if (stdout.includes("not associated")) {
×
828
          wifiState = WifiState.NOT_ASSOCIATED;
×
829
        } else if (stdout.includes("All Wi-Fi network services are disabled")) {
×
830
          // typically encountered on a macOS VM or something not having a WiFi card
831
          wifiState = WifiState.NOT_A_WIFI_INTERFACE;
×
832
        } else {
833
          console.log(`CIAO WARN: While checking networksetup for ${name} encountered an unknown output: ${stdout.replace(os.EOL, "; ")}`);
×
834
        }
835

836
        resolve(wifiState);
×
837
      });
838
    });
839
  }
840

841
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc