import assert from "assert" ; import childProcess from "child_process" ; import createDebug from "debug" ; import { EventEmitter } from "events" ; import deepEqual from "fast-deep-equal" ; import net from "net" ; import os , { NetworkInterfaceInfo } from "os" ; import { getNetAddress } from "./util/domain-formatter" ; import Timeout = NodeJS . Timeout ; const debug = createDebug ( "ciao:NetworkManager" ) ; export type InterfaceName = string ; export type MacAddress = string ; export type IPv4Address = string ; export type IPv6Address = string ; export type IPAddress = IPv4Address | IPv6Address ; export const enum IPFamily { IPv4 = "IPv4" , IPv6 = "IPv6" } export const enum WifiState { UNDEFINED , NOT_A_WIFI_INTERFACE , NOT_ASSOCIATED , CONNECTED } export interface NetworkInterface { name : InterfaceName ; loopback : boolean ; mac : MacAddress ; ipv4 ? : IPv4Address ; ip4Netmask ? : IPv4Address ; ipv4Netaddress ? : IPv4Address ; ipv6 ? : IPv6Address ; ipv6Netmask ? : IPv6Address ; globallyRoutableIpv6 ? : IPv6Address ; globallyRoutableIpv6Netmask ? : IPv6Address ; uniqueLocalIpv6 ? : IPv6Address ; uniqueLocalIpv6Netmask ? : IPv6Address ; } export interface NetworkUpdate { added ? : NetworkInterface [ ] ; removed ? : NetworkInterface [ ] ; changes ? : InterfaceChange [ ] ; } export interface InterfaceChange { name : InterfaceName ; outdatedIpv4 ? : IPv4Address ; updatedIpv4 ? : IPv4Address ; outdatedIpv6 ? : IPv6Address ; updatedIpv6 ? : IPv6Address ; outdatedGloballyRoutableIpv6 ? : IPv6Address ; updatedGloballyRoutableIpv6 ? : IPv6Address ; outdatedUniqueLocalIpv6 ? : IPv6Address ; updatedUniqueLocalIpv6 ? : IPv6Address ; } export interface NetworkManagerOptions { interface ? : ( InterfaceName | IPAddress ) | ( InterfaceName | IPAddress ) [ ] ; excludeIpv6 ? : boolean ; excludeIpv6Only ? : boolean ; } export const enum NetworkManagerEvent { NETWORK_UPDATE = "network-update" } export declare interface NetworkManager { on ( event , listener ) : this ; emit ( event , networkUpdate ) : boolean ; } /**
 * The NetworkManager maintains a representation of the network interfaces define on the host system.
 * It periodically checks for updated network information.
 *
 * The NetworkManager makes the following decision when checking for interfaces:
 * * First of all it gathers the default network interface of the system (by checking the routing table of the os)
 * * The following interfaces are going to be tracked:
 *   * The loopback interface
 *   * All interfaces which match the subnet of the default interface
 *   * All interfaces which contain a globally unique (aka globally routable) ipv6 address
 */ export class NetworkManager extends EventEmitter { private static readonly SPACE_PATTERN = /\s+/g ; private static readonly NOTHING_FOUND_MESSAGE = "no interfaces found" ; private static readonly POLLING_TIME = 15 * 1000 ; private readonly restrictedInterfaces ? : InterfaceName [ ] ; private readonly excludeIpv6 : boolean ; private readonly excludeIpv6Only : boolean ; private currentInterfaces : Map < InterfaceName , NetworkInterface > = new Map ( ) ; /**
     * A subset of our network interfaces, holding only loopback interfaces (or what node considers "internal").
     */ private loopbackInterfaces : Map < InterfaceName , NetworkInterface > = new Map ( ) ; private initPromise ? : Promise < void > ; private currentTimer ? : Timeout ; constructor ( options ? ) { super ( ) ; this . setMaxListeners ( 100 ) ; if ( options && options . interface ) { let interfaces ; if ( typeof options . interface === "string" ) { interfaces = [ options . interface ] ; } else if ( Array . isArray ( options . interface ) ) { interfaces = options . interface ; } else { throw new Error ( "Found invalid type for 'interfaces' NetworkManager option!" ) ; } const restrictedInterfaces = [ ] ; for ( const iface of interfaces ) { if ( net . isIP ( iface ) ) { const interfaceName = NetworkManager . resolveInterface ( iface ) ; if ( interfaceName ) { restrictedInterfaces . push ( interfaceName ) ; } else { console . log ( "CIAO: Interface was specified as ip (%s), though couldn't find a matching interface for the given address." , options . interface ) ; } } else { restrictedInterfaces . push ( iface ) ; } } if ( restrictedInterfaces . length === 0 ) { console . log ( "CIAO: 'restrictedInterfaces' array was empty. Going to fallback to bind on all available interfaces." ) ; } else { this . restrictedInterfaces = restrictedInterfaces ; } } this . excludeIpv6 = ! ! ( options && options . excludeIpv6 ) ; this . excludeIpv6Only = this . excludeIpv6 || ! ! ( options && options . excludeIpv6Only ) ; if ( options ) { debug ( "Created NetworkManager with options: %s" , JSON . stringify ( options ) ) ; } this . initPromise = new Promise ( resolve => { this . getCurrentNetworkInterfaces ( ) . then ( map => { this . currentInterfaces = map ; const otherInterfaces = Object . keys ( os . networkInterfaces ( ) ) ; const interfaceNames = [ ] ; for ( const name of this . currentInterfaces . keys ( ) ) { interfaceNames . push ( name ) ; const index = otherInterfaces . indexOf ( name ) ; if ( index !== - 1 ) { otherInterfaces . splice ( index , 1 ) ; } } debug ( "Initial networks [%s] ignoring [%s]" , interfaceNames . join ( ", " ) , otherInterfaces . join ( ", " ) ) ; this . initPromise = undefined ; resolve ( ) ; this . scheduleNextJob ( ) ; } ) ; } ) ; } public async waitForInit ( ) { if ( this . initPromise ) { await this . initPromise ; } } public shutdown ( ) { if ( this . currentTimer ) { clearTimeout ( this . currentTimer ) ; this . currentTimer = undefined ; } this . removeAllListeners ( ) ; } public getInterfaceMap ( ) { if ( this . initPromise ) { assert . fail ( "Not yet initialized!" ) ; } return this . currentInterfaces ; } public getInterface ( name ) { if ( this . initPromise ) { assert . fail ( "Not yet initialized!" ) ; } return this . currentInterfaces . get ( name ) ; } public isLoopbackNetaddressV4 ( netaddress ) { for ( const networkInterface of this . loopbackInterfaces . values ( ) ) { if ( networkInterface . ipv4Netaddress === netaddress ) { return true ; } } return false ; } private scheduleNextJob ( ) { this . currentTimer = setTimeout ( this . checkForNewInterfaces . bind ( this ) , NetworkManager . POLLING_TIME ) ; this . currentTimer . unref ( ) ; } private async checkForNewInterfaces ( ) { const latestInterfaces = await this . getCurrentNetworkInterfaces ( ) ; if ( ! this . currentTimer ) { return ; } let added = undefined ; let removed = undefined ; let changes = undefined ; for ( const [ name , networkInterface ] of latestInterfaces ) { const currentInterface = this . currentInterfaces . get ( name ) ; if ( currentInterface ) { if ( ! deepEqual ( currentInterface , networkInterface ) ) { const change = { name : name , } ; if ( currentInterface . ipv4 !== networkInterface . ipv4 ) { if ( currentInterface . ipv4 ) { change . outdatedIpv4 = currentInterface . ipv4 ; } if ( networkInterface . ipv4 ) { change . updatedIpv4 = networkInterface . ipv4 ; } } if ( currentInterface . ipv6 !== networkInterface . ipv6 ) { if ( currentInterface . ipv6 ) { change . outdatedIpv6 = currentInterface . ipv6 ; } if ( networkInterface . ipv6 ) { change . updatedIpv6 = networkInterface . ipv6 ; } } if ( currentInterface . globallyRoutableIpv6 !== networkInterface . globallyRoutableIpv6 ) { if ( currentInterface . globallyRoutableIpv6 ) { change . outdatedGloballyRoutableIpv6 = currentInterface . globallyRoutableIpv6 ; } if ( networkInterface . globallyRoutableIpv6 ) { change . updatedGloballyRoutableIpv6 = networkInterface . globallyRoutableIpv6 ; } } if ( currentInterface . uniqueLocalIpv6 !== networkInterface . uniqueLocalIpv6 ) { if ( currentInterface . uniqueLocalIpv6 ) { change . outdatedUniqueLocalIpv6 = currentInterface . uniqueLocalIpv6 ; } if ( networkInterface . uniqueLocalIpv6 ) { change . updatedUniqueLocalIpv6 = networkInterface . uniqueLocalIpv6 ; } } this . currentInterfaces . set ( name , networkInterface ) ; if ( networkInterface . loopback ) { this . loopbackInterfaces . set ( name , networkInterface ) ; } ( changes ??= [ ] ) . push ( change ) ; } } else { this . currentInterfaces . set ( name , networkInterface ) ; if ( networkInterface . loopback ) { this . currentInterfaces . set ( name , networkInterface ) ; } ( added ??= [ ] ) . push ( networkInterface ) ; } } if ( this . currentInterfaces . size !== latestInterfaces . size ) { for ( const [ name , networkInterface ] of this . currentInterfaces ) { if ( ! latestInterfaces . has ( name ) ) { this . currentInterfaces . delete ( name ) ; this . loopbackInterfaces . delete ( name ) ; ( removed ??= [ ] ) . push ( networkInterface ) ; } } } if ( added || removed || changes ) { const addedString = added ? added . map ( iface => iface . name ) . join ( "," ) : "" ; const removedString = removed ? removed . map ( iface => iface . name ) . join ( "," ) : "" ; const changesString = changes ? changes . map ( iface => { let string = `{ name: ${ iface . name } ` ; if ( iface . outdatedIpv4 || iface . updatedIpv4 ) { string += `, ${ iface . outdatedIpv4 } -> ${ iface . updatedIpv4 } ` ; } if ( iface . outdatedIpv6 || iface . updatedIpv6 ) { string += `, ${ iface . outdatedIpv6 } -> ${ iface . updatedIpv6 } ` ; } if ( iface . outdatedGloballyRoutableIpv6 || iface . updatedGloballyRoutableIpv6 ) { string += `, ${ iface . outdatedGloballyRoutableIpv6 } -> ${ iface . updatedGloballyRoutableIpv6 } ` ; } if ( iface . outdatedUniqueLocalIpv6 || iface . updatedUniqueLocalIpv6 ) { string += `, ${ iface . outdatedUniqueLocalIpv6 } -> ${ iface . updatedUniqueLocalIpv6 } ` ; } return string + "}" ; } ) . join ( "," ) : "" ; debug ( "Detected network changes: added: [%s], removed: [%s], changes: [%s]!" , addedString , removedString , changesString ) ; this . emit ( NetworkManagerEvent . NETWORK_UPDATE , { added : added , removed : removed , changes : changes , } ) ; } this . scheduleNextJob ( ) ; } private async getCurrentNetworkInterfaces ( ) { let names ; if ( this . restrictedInterfaces ) { names = this . restrictedInterfaces ; const loopback = NetworkManager . getLoopbackInterface ( ) ; if ( ! names . includes ( loopback ) ) { names . push ( loopback ) ; } } else { try { names = await NetworkManager . getNetworkInterfaceNames ( ) ; } catch ( error ) { debug ( `WARNING Detecting network interfaces for platform '${ os . platform ( ) }' failed. Trying to assume network interfaces! (${ error . message })` ) ; names = NetworkManager . assumeNetworkInterfaceNames ( ) ; } } const interfaces = new Map ( ) ; const networkInterfaces = os . networkInterfaces ( ) ; for ( const name of names ) { const infos = networkInterfaces [ name ] ; if ( ! infos ) { continue ; } let ipv4Info = undefined ; let ipv6Info = undefined ; let routableIpv6Info = undefined ; let uniqueLocalIpv6Info = undefined ; let internal = false ; for ( const info of infos ) { if ( info . internal ) { internal = true ; } if ( info . family === "IPv4" && ! ipv4Info ) { ipv4Info = info ; } else if ( info . family === "IPv6" ) { if ( this . excludeIpv6 ) { continue ; } if ( info . scopeid && ! ipv6Info ) { ipv6Info = info ; } else if ( info . scopeid === 0 ) { if ( info . address . startsWith ( "fc" ) || info . address . startsWith ( "fd" ) ) { if ( ! uniqueLocalIpv6Info ) { uniqueLocalIpv6Info = info ; } } else if ( ! routableIpv6Info ) { routableIpv6Info = info ; } } } if ( ipv4Info && ipv6Info && routableIpv6Info && uniqueLocalIpv6Info ) { break ; } } assert ( ipv4Info || ipv6Info , "Could not find valid addresses for interface '" + name + "'" ) ; if ( this . excludeIpv6Only && ! ipv4Info ) { continue ; } const networkInterface = { name : name , loopback : internal , mac : ( ipv4Info ?. mac || ipv6Info ?. mac ) ! , } ; if ( ipv4Info ) { networkInterface . ipv4 = ipv4Info . address ; networkInterface . ip4Netmask = ipv4Info . netmask ; networkInterface . ipv4Netaddress = getNetAddress ( ipv4Info . address , ipv4Info . netmask ) ; } if ( ipv6Info ) { networkInterface . ipv6 = ipv6Info . address ; networkInterface . ipv6Netmask = ipv6Info . netmask ; } if ( routableIpv6Info ) { networkInterface . globallyRoutableIpv6 = routableIpv6Info . address ; networkInterface . globallyRoutableIpv6Netmask = routableIpv6Info . netmask ; } if ( uniqueLocalIpv6Info ) { networkInterface . uniqueLocalIpv6 = uniqueLocalIpv6Info . address ; networkInterface . uniqueLocalIpv6Netmask = uniqueLocalIpv6Info . netmask ; } interfaces . set ( name , networkInterface ) ; } return interfaces ; } public static resolveInterface ( address ) { let interfaceName ; outer : for ( const [ name , infoArray ] of Object . entries ( os . networkInterfaces ( ) ) ) { for ( const info of infoArray ) { if ( info . address === address ) { interfaceName = name ; break outer ; } } } return interfaceName ; } private static async getNetworkInterfaceNames ( ) { let promise ; switch ( os . platform ( ) ) { case "win32" : promise = NetworkManager . getWindowsNetworkInterfaces ( ) ; break ; case "linux" : { promise = NetworkManager . getLinuxNetworkInterfaces ( ) ; break ; } case "darwin" : promise = NetworkManager . getDarwinNetworkInterfaces ( ) ; break ; case "freebsd" : { promise = NetworkManager . getFreeBSDNetworkInterfaces ( ) ; break ; } case "openbsd" : case "sunos" : { promise = NetworkManager . getOpenBSD_SUNOS_NetworkInterfaces ( ) ; break ; } default : debug ( "Found unsupported platform %s" , os . platform ( ) ) ; return Promise . reject ( new Error ( "unsupported platform!" ) ) ; } let names ; try { names = await promise ; } catch ( error ) { if ( error . message !== NetworkManager . NOTHING_FOUND_MESSAGE ) { throw error ; } names = [ ] ; } const loopback = NetworkManager . getLoopbackInterface ( ) ; if ( ! names . includes ( loopback ) ) { names . unshift ( loopback ) ; } return promise ; } private static assumeNetworkInterfaceNames ( ) { const names = [ ] ; Object . entries ( os . networkInterfaces ( ) ) . forEach ( ( [ name , infos ] ) => { for ( const info of infos ) { if ( info . internal || info . family === "IPv4" || info . family === "IPv6" && info . scopeid === 0 ) { if ( ! names . includes ( name ) ) { names . push ( name ) ; } break ; } } } ) ; return names ; } private static getLoopbackInterface ( ) { for ( const [ name , infos ] of Object . entries ( os . networkInterfaces ( ) ) ) { for ( const info of infos ) { if ( info . internal ) { return name ; } } } throw new Error ( "Could not detect loopback interface!" ) ; } private static getWindowsNetworkInterfaces ( ) { return new Promise ( ( resolve , reject ) => { childProcess . exec ( "arp -a | findstr /C:\"---\"" , ( error , stdout ) => { if ( error ) { reject ( error ) ; return ; } const lines = stdout . split ( os . EOL ) ; const addresses = [ ] ; for ( let i = 0 ; i < lines . length - 1 ; i ++ ) { const line = lines [ i ] . trim ( ) . split ( " " ) ; if ( line [ line . length - 3 ] ) { addresses . push ( line [ line . length - 3 ] ) ; } else { debug ( `WINDOWS: Failed to read interface name from line ${ i }: '${ lines [ i ] }'` ) ; } } const names = [ ] ; for ( const address of addresses ) { const name = NetworkManager . resolveInterface ( address ) ; if ( name ) { if ( ! names . includes ( name ) ) { names . push ( name ) ; } } else { debug ( `WINDOWS: Couldn't resolve to an interface name from '${ address }'` ) ; } } if ( names . length ) { resolve ( names ) ; } else { reject ( new Error ( NetworkManager . NOTHING_FOUND_MESSAGE ) ) ; } } ) ; } ) ; } private static getDarwinNetworkInterfaces ( ) { return new Promise ( ( resolve , reject ) => { childProcess . exec ( "arp -a -n -l" , async ( error , stdout ) => { if ( error ) { reject ( error ) ; return ; } const lines = stdout . split ( os . EOL ) ; const names = [ ] ; for ( let i = 1 ; i < lines . length - 1 ; i ++ ) { const interfaceName = lines [ i ] . trim ( ) . split ( NetworkManager . SPACE_PATTERN ) [ 4 ] ; if ( ! interfaceName ) { debug ( `DARWIN: Failed to read interface name from line ${ i }: '${ lines [ i ] }'` ) ; continue ; } if ( ! names . includes ( interfaceName ) ) { names . push ( interfaceName ) ; } } const promises = [ ] ; for ( const name of names ) { const promise = NetworkManager . getDarwinWifiNetworkState ( name ) . then ( state => { if ( state !== WifiState . NOT_A_WIFI_INTERFACE && state !== WifiState . CONNECTED ) { const index = names . indexOf ( name ) ; if ( index !== - 1 ) { names . splice ( index , 1 ) ; } } } ) ; promises . push ( promise ) ; } await Promise . all ( promises ) ; if ( names . length ) { resolve ( names ) ; } else { reject ( new Error ( NetworkManager . NOTHING_FOUND_MESSAGE ) ) ; } } ) ; } ) ; } private static getLinuxNetworkInterfaces ( ) { return new Promise ( ( resolve , reject ) => { childProcess . exec ( "ip neigh show" , ( error , stdout ) => { if ( error ) { if ( error . message . includes ( "ip: not found" ) ) { debug ( "LINUX: ip was not found on the system. Falling back to assuming network interfaces!" ) ; resolve ( NetworkManager . assumeNetworkInterfaceNames ( ) ) ; return ; } reject ( error ) ; return ; } const lines = stdout . split ( os . EOL ) ; const names = [ ] ; for ( let i = 0 ; i < lines . length - 1 ; i ++ ) { const parts = lines [ i ] . trim ( ) . split ( NetworkManager . SPACE_PATTERN ) ; let devIndex = 0 ; for ( ; devIndex < parts . length ; devIndex ++ ) { if ( parts [ devIndex ] === "dev" ) { break ; } } if ( devIndex >= parts . length ) { debug ( `LINUX: Out of bounds when reading interface name from line ${ i }: '${ lines [ i ] }'` ) ; continue ; } const interfaceName = parts [ devIndex + 1 ] ; if ( ! interfaceName ) { debug ( `LINUX: Failed to read interface name from line ${ i }: '${ lines [ i ] }'` ) ; continue ; } if ( ! names . includes ( interfaceName ) ) { names . push ( interfaceName ) ; } } if ( names . length ) { resolve ( names ) ; } else { reject ( new Error ( NetworkManager . NOTHING_FOUND_MESSAGE ) ) ; } } ) ; } ) ; } private static getFreeBSDNetworkInterfaces ( ) { return new Promise ( ( resolve , reject ) => { childProcess . exec ( "arp -a -n" , ( error , stdout ) => { if ( error ) { reject ( error ) ; return ; } const lines = stdout . split ( os . EOL ) ; const names = [ ] ; for ( let i = 0 ; i < lines . length - 1 ; i ++ ) { const interfaceName = lines [ i ] . trim ( ) . split ( NetworkManager . SPACE_PATTERN ) [ 5 ] ; if ( ! interfaceName ) { debug ( `FreeBSD: Failed to read interface name from line ${ i }: '${ lines [ i ] }'` ) ; continue ; } if ( ! names . includes ( interfaceName ) ) { names . push ( interfaceName ) ; } } if ( names . length ) { resolve ( names ) ; } else { reject ( new Error ( NetworkManager . NOTHING_FOUND_MESSAGE ) ) ; } } ) ; } ) ; } private static getOpenBSD_SUNOS_NetworkInterfaces ( ) { return new Promise ( ( resolve , reject ) => { childProcess . exec ( "arp -a -n" , ( error , stdout ) => { if ( error ) { reject ( error ) ; return ; } const lines = stdout . split ( os . EOL ) ; const names = [ ] ; for ( let i = 1 ; i < lines . length - 1 ; i ++ ) { const interfaceName = lines [ i ] . trim ( ) . split ( NetworkManager . SPACE_PATTERN ) [ 2 ] ; if ( ! interfaceName ) { debug ( `${ os . platform ( ) }: Failed to read interface name from line ${ i }: '${ lines [ i ] }'` ) ; continue ; } if ( ! names . includes ( interfaceName ) ) { names . push ( interfaceName ) ; } } if ( names . length ) { resolve ( names ) ; } else { reject ( new Error ( NetworkManager . NOTHING_FOUND_MESSAGE ) ) ; } } ) ; } ) ; } private static getDarwinWifiNetworkState ( name ) { return new Promise ( resolve => { childProcess . exec ( "networksetup -getairportnetwork " + name , ( error , stdout ) => { if ( error ) { if ( stdout . includes ( "not a Wi-Fi interface" ) ) { resolve ( WifiState . NOT_A_WIFI_INTERFACE ) ; return ; } console . log ( `CIAO WARN: While checking networksetup for ${ name } encountered an error (${ error . message }) with output: ${ stdout . replace ( os . EOL , "; " ) }` ) ; resolve ( WifiState . UNDEFINED ) ; return ; } let wifiState = WifiState . UNDEFINED ; if ( stdout . includes ( "not a Wi-Fi interface" ) ) { wifiState = WifiState . NOT_A_WIFI_INTERFACE ; } else if ( stdout . includes ( "Current Wi-Fi Network" ) ) { wifiState = WifiState . CONNECTED ; } else if ( stdout . includes ( "not associated" ) ) { wifiState = WifiState . NOT_ASSOCIATED ; } else if ( stdout . includes ( "All Wi-Fi network services are disabled" ) ) { wifiState = WifiState . NOT_A_WIFI_INTERFACE ; } else { console . log ( `CIAO WARN: While checking networksetup for ${ name } encountered an unknown output: ${ stdout . replace ( os . EOL , "; " ) }` ) ; } resolve ( wifiState ) ; } ) ; } ) ; } }