util: NetworkMonitor: macOS version of fine-grained signals on interface state changes

Change-Id: I6da12356baa0038d08ff256d0d3ff726023d804b
Refs: #3817
diff --git a/.waf-tools/osx-frameworks.py b/.waf-tools/osx-frameworks.py
index 881d3e1..31c2bad 100644
--- a/.waf-tools/osx-frameworks.py
+++ b/.waf-tools/osx-frameworks.py
@@ -13,6 +13,12 @@
 int main() {}
 '''
 
+OSX_SYSTEMCONFIGURATION_CODE = '''
+#include <CoreFoundation/CoreFoundation.h>
+#include <SystemConfiguration/SystemConfiguration.h>
+int main() {}
+'''
+
 @conf
 def check_osx_frameworks(conf, *k, **kw):
     if Utils.unversioned_sys_platform() == "darwin":
@@ -24,10 +30,13 @@
             conf.check_cxx(framework_name='Security', uselib_store='OSX_SECURITY',
                            use='OSX_COREFOUNDATION', fragment=OSX_SECURITY_CODE,
                            mandatory=True)
+            conf.check_cxx(framework_name='SystemConfiguration', uselib_store='OSX_SYSTEMCONFIGURATION',
+                           use='OSX_COREFOUNDATION', fragment=OSX_SYSTEMCONFIGURATION_CODE,
+                           mandatory=True)
 
             conf.define('HAVE_OSX_FRAMEWORKS', 1)
             conf.env['HAVE_OSX_FRAMEWORKS'] = True
         except:
-            Logs.warn("Compiling on OSX, but CoreFoundation, CoreServices, or Security " +
+            Logs.warn("Compiling on OSX, but CoreFoundation, CoreServices, Security, or SystemConfiguration " +
                       "framework is not functional.")
             Logs.warn("The frameworks are known to work only with the Apple clang compiler")
diff --git a/src/security/tpm/helper-osx.hpp b/src/security/tpm/helper-osx.hpp
index 96c25c4..9271e68 100644
--- a/src/security/tpm/helper-osx.hpp
+++ b/src/security/tpm/helper-osx.hpp
@@ -32,8 +32,6 @@
 #include <Security/Security.h>
 
 namespace ndn {
-namespace security {
-namespace tpm {
 
 /**
  * @brief Helper class to wrap CoreFoundation object pointers
@@ -145,10 +143,14 @@
   T m_typeRef;
 };
 
-typedef CFReleaser<SecKeyRef> KeyRefOsx;
+namespace security {
+namespace tpm {
+
+using KeyRefOsx = CFReleaser<SecKeyRef>;
 
 } // namespace tpm
 } // namespace security
+
 } // namespace ndn
 
 #endif // NDN_SECURITY_TPM_HELPER_OSX_HPP
diff --git a/src/util/detail/network-monitor-impl-osx.cpp b/src/util/detail/network-monitor-impl-osx.cpp
index 327a103..4824bab 100644
--- a/src/util/detail/network-monitor-impl-osx.cpp
+++ b/src/util/detail/network-monitor-impl-osx.cpp
@@ -53,28 +53,37 @@
 #include "ndn-cxx-config.hpp"
 
 #include "network-monitor-impl-osx.hpp"
-#include "../network-interface.hpp"
+#include "../../name.hpp"
+#include "../logger.hpp"
+#include "../network-address.hpp"
+
+#include <ifaddrs.h>       // for getifaddrs()
+#include <arpa/inet.h>     // for inet_ntop()
+#include <netinet/in.h>    // for struct sockaddr_in{,6}
+#include <net/if_dl.h>     // for struct sockaddr_dl
+#include <net/if_types.h>  // for IFT_* constants
+
+#include <boost/asio.hpp>
 
 namespace ndn {
 namespace util {
 
+NDN_LOG_INIT(ndn.NetworkMonitor);
+
 NetworkMonitor::Impl::Impl(NetworkMonitor& nm, boost::asio::io_service& io)
   : m_nm(nm)
   , m_scheduler(io)
   , m_cfLoopEvent(m_scheduler)
+  , m_context{0, this, nullptr, nullptr, nullptr}
+  , m_scStore(SCDynamicStoreCreate(nullptr, CFSTR("net.named-data.ndn-cxx.NetworkMonitor"),
+                                   &Impl::onConfigChanged, &m_context))
+  , m_loopSource(SCDynamicStoreCreateRunLoopSource(nullptr, m_scStore.get(), 0))
+  , m_nullUdpSocket(io, boost::asio::ip::udp::v4())
+
 {
   scheduleCfLoop();
 
-  // Potentially useful System Configuration regex patterns:
-  //
-  // State:/Network/Interface/.*/Link
-  // State:/Network/Interface/.*/IPv4
-  // State:/Network/Interface/.*/IPv6
-  //
-  // State:/Network/Global/DNS
-  // State:/Network/Global/IPv4
-  //
-  // Potentially useful notifications from Darwin Notify Center:
+  // Notifications from Darwin Notify Center:
   //
   // com.apple.system.config.network_change
   //
@@ -84,10 +93,33 @@
                                   CFSTR("com.apple.system.config.network_change"),
                                   nullptr, // object to observe
                                   CFNotificationSuspensionBehaviorDeliverImmediately);
+
+  io.post([this] { enumerateInterfaces(); });
+
+  CFRunLoopAddSource(CFRunLoopGetCurrent(), m_loopSource.get(), kCFRunLoopDefaultMode);
+
+  // Notifications from SystemConfiguration:
+  //
+  // State:/Network/Interface/.*/Link
+  // State:/Network/Interface/.*/IPv4
+  // State:/Network/Interface/.*/IPv6
+  // State:/Network/Global/DNS
+  // State:/Network/Global/IPv4
+  //
+  auto patterns = CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks);
+  CFArrayAppendValue(patterns, CFSTR("State:/Network/Interface/.*/Link"));
+  CFArrayAppendValue(patterns, CFSTR("State:/Network/Interface/.*/IPv4"));
+  CFArrayAppendValue(patterns, CFSTR("State:/Network/Interface/.*/IPv6"));
+  // CFArrayAppendValue(patterns, CFSTR("State:/Network/Global/DNS"));
+  // CFArrayAppendValue(patterns, CFSTR("State:/Network/Global/IPv4"));
+
+  SCDynamicStoreSetNotificationKeys(m_scStore.get(), nullptr, patterns);
 }
 
 NetworkMonitor::Impl::~Impl()
 {
+  CFRunLoopRemoveSource(CFRunLoopGetCurrent(), m_loopSource.get(), kCFRunLoopDefaultMode);
+
   CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDarwinNotifyCenter(),
                                           static_cast<void*>(this));
 }
@@ -95,13 +127,25 @@
 shared_ptr<NetworkInterface>
 NetworkMonitor::Impl::getNetworkInterface(const std::string& ifname) const
 {
-  return nullptr;
+  auto it = m_interfaces.find(ifname);
+  if (it != m_interfaces.end()) {
+    return it->second;
+  }
+  else {
+    return nullptr;
+  }
 }
 
 std::vector<shared_ptr<NetworkInterface>>
 NetworkMonitor::Impl::listNetworkInterfaces() const
 {
-  return {};
+  std::vector<shared_ptr<NetworkInterface>> v;
+  v.reserve(m_interfaces.size());
+
+  for (const auto& e : m_interfaces) {
+    v.push_back(e.second);
+  }
+  return v;
 }
 
 void
@@ -130,5 +174,280 @@
   scheduleCfLoop();
 }
 
+void
+NetworkMonitor::Impl::addNewInterface(const std::string& ifName)
+{
+  shared_ptr<NetworkInterface> interface(new NetworkInterface);
+
+  interface->setName(ifName);
+  interface->setState(getInterfaceState(interface->getName()));
+  updateInterfaceInfo(*interface);
+  if (interface->getType() == InterfaceType::UNKNOWN) {
+    NDN_LOG_DEBUG("ignoring " << ifName << " because it has unhandled interface type");
+    return;
+  }
+
+  NDN_LOG_DEBUG("adding interface " << interface->getName());
+  m_interfaces.insert(make_pair(interface->getName(), interface));
+  m_nm.onInterfaceAdded(interface);
+}
+
+void
+NetworkMonitor::Impl::enumerateInterfaces()
+{
+  for (const auto& ifName : getInterfaceNames()) {
+    addNewInterface(ifName);
+  }
+  m_nm.onEnumerationCompleted();
+}
+
+static std::string
+convertToStdString(CFStringRef cfString)
+{
+  const char* cStr = CFStringGetCStringPtr(cfString, kCFStringEncodingASCII);
+  if (cStr != nullptr) {
+    return cStr;
+  }
+
+  size_t stringSize =  CFStringGetLength(cfString);
+  char* buffer = new char[stringSize + 1];
+  CFStringGetCString(cfString, buffer, sizeof(buffer), kCFStringEncodingASCII);
+  std::string retval = buffer;
+  delete [] buffer;
+  return retval;
+}
+
+std::set<std::string>
+NetworkMonitor::Impl::getInterfaceNames()
+{
+  CFReleaser<CFDictionaryRef> dict = (CFDictionaryRef)SCDynamicStoreCopyValue(m_scStore.get(), CFSTR("State:/Network/Interface"));
+  CFArrayRef interfaces = (CFArrayRef)CFDictionaryGetValue(dict.get(), CFSTR("Interfaces"));
+
+  std::set<std::string> ifNames;
+  size_t count = CFArrayGetCount(interfaces);
+  for (size_t i = 0; i != count; ++i) {
+    auto ifName = (CFStringRef)CFArrayGetValueAtIndex(interfaces, i);
+    ifNames.insert(convertToStdString(ifName));
+  }
+  return ifNames;
+}
+
+InterfaceState
+NetworkMonitor::Impl::getInterfaceState(const std::string& ifName)
+{
+  CFReleaser<CFStringRef> linkName = CFStringCreateWithCString(nullptr,
+                                                               ("State:/Network/Interface/" + ifName + "/Link").c_str(),
+                                                               kCFStringEncodingASCII);
+
+  CFReleaser<CFDictionaryRef> dict = (CFDictionaryRef)SCDynamicStoreCopyValue(m_scStore.get(), linkName.get());
+  if (dict.get() == nullptr) {
+    return InterfaceState::UNKNOWN;
+  }
+
+  CFBooleanRef isActive = (CFBooleanRef)CFDictionaryGetValue(dict.get(), CFSTR("Active"));
+  if (isActive == nullptr) {
+    return InterfaceState::UNKNOWN;
+  }
+
+  return CFBooleanGetValue(isActive) ? InterfaceState::RUNNING : InterfaceState::DOWN;
+}
+
+void
+NetworkMonitor::Impl::updateInterfaceInfo(NetworkInterface& netif)
+{
+  ifaddrs* ifa_list = nullptr;
+  if (::getifaddrs(&ifa_list) < 0) {
+    BOOST_THROW_EXCEPTION(Error(std::string("getifaddrs() failed: ") + strerror(errno)));
+  }
+
+  for (ifaddrs* ifa = ifa_list; ifa != nullptr; ifa = ifa->ifa_next) {
+    if (ifa->ifa_name != netif.getName()) {
+      continue;
+    }
+
+    netif.setFlags(ifa->ifa_flags);
+    netif.setMtu(getInterfaceMtu(netif.getName()));
+
+    if (ifa->ifa_addr == nullptr)
+      continue;
+
+    NetworkAddress address;
+
+    switch (ifa->ifa_addr->sa_family) {
+      case AF_INET: {
+        address.m_family = AddressFamily::V4;
+
+        const sockaddr_in* sin = reinterpret_cast<sockaddr_in*>(ifa->ifa_addr);
+        boost::asio::ip::address_v4::bytes_type bytes;
+        std::copy_n(reinterpret_cast<const unsigned char*>(&sin->sin_addr), bytes.size(), bytes.begin());
+        address.m_ip = boost::asio::ip::address_v4(bytes);
+
+        const sockaddr_in* sinMask = reinterpret_cast<sockaddr_in*>(ifa->ifa_netmask);
+        std::copy_n(reinterpret_cast<const unsigned char*>(&sinMask->sin_addr), bytes.size(), bytes.begin());
+        uint8_t mask = 0;
+        for (auto byte : bytes) {
+          while (byte != 0) {
+            ++mask;
+            byte <<= 1;
+          }
+        }
+        address.m_prefixLength = mask;
+        break;
+      }
+
+      case AF_INET6: {
+        address.m_family = AddressFamily::V6;
+
+        const sockaddr_in6* sin6 = reinterpret_cast<sockaddr_in6*>(ifa->ifa_addr);
+        boost::asio::ip::address_v6::bytes_type bytes;
+        std::copy_n(reinterpret_cast<const unsigned char*>(&sin6->sin6_addr), bytes.size(), bytes.begin());
+        address.m_ip = boost::asio::ip::address_v6(bytes);
+
+        const sockaddr_in6* sinMask = reinterpret_cast<sockaddr_in6*>(ifa->ifa_netmask);
+        std::copy_n(reinterpret_cast<const unsigned char*>(&sinMask->sin6_addr), bytes.size(), bytes.begin());
+        uint8_t mask = 0;
+        for (auto byte : bytes) {
+          while (byte != 0) {
+            ++mask;
+            byte <<= 1;
+          }
+        }
+        address.m_prefixLength = mask;
+        break;
+      }
+
+      case AF_LINK: {
+        const sockaddr_dl* sdl = reinterpret_cast<sockaddr_dl*>(ifa->ifa_addr);
+        netif.setIndex(sdl->sdl_index);
+        if (sdl->sdl_type == IFT_ETHER && sdl->sdl_alen == ethernet::ADDR_LEN) {
+          netif.setType(InterfaceType::ETHERNET);
+          netif.setEthernetAddress(ethernet::Address(reinterpret_cast<uint8_t*>(LLADDR(sdl))));
+          NDN_LOG_TRACE(netif.getName() << ": set Ethernet address " << netif.getEthernetAddress());
+        }
+        else if (sdl->sdl_type == IFT_LOOP) {
+          netif.setType(InterfaceType::LOOPBACK);
+        }
+        else {
+          netif.setType(InterfaceType::UNKNOWN);
+        }
+        break;
+      }
+
+      default:
+        continue;
+    }
+
+    if (netif.canBroadcast() && ifa->ifa_broadaddr != nullptr) {
+      const sockaddr_in* sin = reinterpret_cast<sockaddr_in*>(ifa->ifa_broadaddr);
+      boost::asio::ip::address_v4::bytes_type bytes;
+      std::copy_n(reinterpret_cast<const unsigned char*>(&sin->sin_addr), bytes.size(), bytes.begin());
+      address.m_broadcast = boost::asio::ip::address_v4(bytes);
+      NDN_LOG_TRACE(netif.getName() << ": set IPv4 broadcast address " << address.m_broadcast);
+    }
+
+    if (netif.canBroadcast()) {
+      netif.setEthernetBroadcastAddress(ethernet::getBroadcastAddress());
+    }
+
+    netif.addNetworkAddress(address);
+  }
+
+  ::freeifaddrs(ifa_list);
+}
+
+size_t
+NetworkMonitor::Impl::getInterfaceMtu(const std::string& ifName)
+{
+  ifreq ifr{};
+  std::strncpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name) - 1);
+
+  if (::ioctl(m_nullUdpSocket.native_handle(), SIOCGIFMTU, &ifr) == 0) {
+    return static_cast<size_t>(ifr.ifr_mtu);
+  }
+
+  NDN_LOG_WARN("Failed to get interface MTU: " << std::strerror(errno));
+  return ethernet::MAX_DATA_LEN;
+}
+
+void
+NetworkMonitor::Impl::onConfigChanged(SCDynamicStoreRef m_scStore, CFArrayRef changedKeys, void* context)
+{
+  static_cast<Impl*>(context)->onConfigChanged(changedKeys);
+}
+
+void
+NetworkMonitor::Impl::onConfigChanged(CFArrayRef changedKeys)
+{
+  size_t count = CFArrayGetCount(changedKeys);
+  for (size_t i = 0; i != count; ++i) {
+    std::string keyName = convertToStdString((CFStringRef)CFArrayGetValueAtIndex(changedKeys, i));
+    Name key(keyName);
+    std::string ifName = key.at(-2).toUri();
+
+    auto ifIt = m_interfaces.find(ifName);
+    if (ifIt == m_interfaces.end()) {
+      addNewInterface(ifName);
+      return;
+    }
+
+    NetworkInterface& netif = *ifIt->second;
+
+    auto removeInterface = [&] {
+      NDN_LOG_DEBUG("removing interface " << ifName);
+      shared_ptr<NetworkInterface> removedInterface = ifIt->second;
+      m_interfaces.erase(ifIt);
+      m_nm.onInterfaceRemoved(removedInterface);
+    };
+
+    if (key.at(-1).toUri() == "Link") {
+      auto newState = getInterfaceState(ifName);
+
+      if (newState == InterfaceState::UNKNOWN) {
+        // check if it is really unknown or interface removed
+        if (getInterfaceNames().count(ifName) == 0) {
+          // newState = InterfaceState::DOWN;
+          removeInterface();
+          return;
+        }
+      }
+
+      NDN_LOG_TRACE("Status of " << ifName << " changed from " << netif.getState() << " to " << newState);
+      netif.setState(newState);
+    }
+
+    if (key.at(-1).toUri() == "IPv4" || key.at(-1).toUri() == "IPv6") {
+      NetworkInterface updatedInterface;
+      updatedInterface.setName(ifName);
+      updateInterfaceInfo(updatedInterface);
+      if (updatedInterface.getType() == InterfaceType::UNKNOWN) {
+        // somehow, type of interface changed to unknown
+        NDN_LOG_DEBUG("Removing " << ifName << " because it changed to unhandled interface type");
+        removeInterface();
+        return;
+      }
+
+      const auto& newAddrs = updatedInterface.getNetworkAddresses();
+      const auto& oldAddrs = netif.getNetworkAddresses();
+
+      std::set<NetworkAddress> added;
+      std::set<NetworkAddress> removed;
+
+      std::set_difference(newAddrs.begin(), newAddrs.end(),
+                          oldAddrs.begin(), oldAddrs.end(), std::inserter(added, added.end()));
+
+      std::set_difference(oldAddrs.begin(), oldAddrs.end(),
+                          newAddrs.begin(), newAddrs.end(), std::inserter(removed, removed.end()));
+
+      for (const auto& addr : removed) {
+        netif.removeNetworkAddress(addr);
+      }
+
+      for (const auto& addr : added) {
+        netif.addNetworkAddress(addr);
+      }
+    }
+  }
+}
+
 } // namespace util
 } // namespace ndn
diff --git a/src/util/detail/network-monitor-impl-osx.hpp b/src/util/detail/network-monitor-impl-osx.hpp
index bd0a3c6..52c3468 100644
--- a/src/util/detail/network-monitor-impl-osx.hpp
+++ b/src/util/detail/network-monitor-impl-osx.hpp
@@ -29,12 +29,16 @@
 #error "This file should not be compiled ..."
 #endif
 
+#include "../network-interface.hpp"
 #include "../scheduler.hpp"
 #include "../scheduler-scoped-event-id.hpp"
+#include "../../security/tpm/helper-osx.hpp"
 
 #include <CoreFoundation/CoreFoundation.h>
 #include <SystemConfiguration/SystemConfiguration.h>
 
+#include <boost/asio/ip/udp.hpp>
+
 namespace ndn {
 namespace util {
 
@@ -48,8 +52,8 @@
   uint32_t
   getCapabilities() const
   {
-    return NetworkMonitor::CAP_NONE;
-    /// \todo #3817 change to CAP_ENUM | CAP_IF_ADD_REMOVE | CAP_STATE_CHANGE | CAP_ADDR_ADD_REMOVE
+    return NetworkMonitor::CAP_ENUM | NetworkMonitor::CAP_IF_ADD_REMOVE |
+      NetworkMonitor::CAP_STATE_CHANGE | NetworkMonitor::CAP_ADDR_ADD_REMOVE;
   }
 
   shared_ptr<NetworkInterface>
@@ -72,11 +76,42 @@
   void
   pollCfLoop();
 
+  void
+  addNewInterface(const std::string& ifName);
+
+  void
+  enumerateInterfaces();
+
+  std::set<std::string>
+  getInterfaceNames();
+
+  InterfaceState
+  getInterfaceState(const std::string& ifName);
+
+  void
+  updateInterfaceInfo(NetworkInterface& netif);
+
+  size_t
+  getInterfaceMtu(const std::string& ifName);
+
+  static void
+  onConfigChanged(SCDynamicStoreRef store, CFArrayRef changedKeys, void* context);
+
+  void
+  onConfigChanged(CFArrayRef changedKeys);
+
 private:
   NetworkMonitor& m_nm;
+  std::map<std::string /*ifname*/, shared_ptr<NetworkInterface>> m_interfaces; ///< interface map
 
   Scheduler m_scheduler;
   scheduler::ScopedEventId m_cfLoopEvent;
+
+  SCDynamicStoreContext m_context;
+  CFReleaser<SCDynamicStoreRef> m_scStore;
+  CFReleaser<CFRunLoopSourceRef> m_loopSource;
+
+  boost::asio::ip::udp::socket m_nullUdpSocket;
 };
 
 } // namespace util
diff --git a/src/util/network-monitor.hpp b/src/util/network-monitor.hpp
index 59b9dc7..629a80e 100644
--- a/src/util/network-monitor.hpp
+++ b/src/util/network-monitor.hpp
@@ -49,11 +49,9 @@
  *
  * @note Implementation of this class is platform dependent and not all supported platforms
  *       are supported:
- *       - OS X: CFNotificationCenterAddObserver (incomplete)
+ *       - OS X: SystemConfiguration and CFNotificationCenterAddObserver notifications (no
+ *         notification on MTU change)
  *       - Linux: rtnetlink notifications
- *
- * @todo macOS implementation needs to be updated to emit the new signals and keep track of
- *       interfaces (links) and addresses
  */
 class NetworkMonitor : noncopyable
 {
diff --git a/tests/integrated/network-monitor.README.md b/tests/integrated/network-monitor.README.md
index b99f26c..a9a9af1 100644
--- a/tests/integrated/network-monitor.README.md
+++ b/tests/integrated/network-monitor.README.md
@@ -1,6 +1,6 @@
-## NetworkMonitor test
+# NetworkMonitor test
 
-These instructions are only for Linux.
+These instructions are only for Linux and macOS.
 
 Run the network-monitor integrated test binary, e.g.:
 ```
@@ -14,6 +14,8 @@
 `onEnumerationCompleted` is printed, along with a summary of all interfaces
 discovered thus far.
 
+## Linux
+
 [The following commands assume eth0 is the name of an ethernet interface
 on the machine. If your interfaces are named differently, replace eth0
 with the name of any ethernet interface that you have available.]
@@ -40,3 +42,29 @@
 eth0: onStateChanged no-carrier -> running
 nmtest0: onStateChanged no-carrier -> running
 ```
+
+## macOS
+
+[The following commands assume en0 is the name of an ethernet interface
+on the machine. If your interfaces are named differently, replace en0
+with the name of any ethernet interface that you have available.]
+
+Command | Expected output
+--------|----------------
+sudo ifconfig vlan1 create | `vlan1: onInterfaceAdded`
+sudo ifconfig vlan1 vlan 1 vlandev en0 | `vlan1: onStateChanged down -> running`
+sudo ifconfig vlan1 198.51.100.100/24 | `vlan1: onAddressAdded 198.51.100.100/24`
+sudo ifconfig vlan1 198.51.100.100/24 remove | `vlan1: onAddressRemoved 198.51.100.100/24`
+sudo ifconfig vlan1 inet6 2001:db8::1/80 | `vlan1: onAddressAdded 2001:db8::1/80` (and potentially link-local addresses)
+sudo ifconfig vlan1 inet6 2001:db8::1/80 remove | `vlan1: onAddressRemoved 2001:db8::1/80`
+sudo ifconfig vlan1 destroy | `vlan1: onInterfaceRemoved`
+
+If you unplug the ethernet cable from your network card, you should see:
+```
+en6: onStateChanged running -> down
+```
+
+Plugging the cable back in should produce the following messages:
+```
+en6: onStateChanged down -> running
+```
diff --git a/tests/unit-tests/util/network-monitor.t.cpp b/tests/unit-tests/util/network-monitor.t.cpp
index b6d7330..8e82086 100644
--- a/tests/unit-tests/util/network-monitor.t.cpp
+++ b/tests/unit-tests/util/network-monitor.t.cpp
@@ -57,7 +57,6 @@
     io.post([&] { nm.reset(); });
   });
   nm->onEnumerationCompleted.connect([&] {
-    BOOST_CHECK_EQUAL(nm->listNetworkInterfaces().size(), 0);
     // make sure the test case terminates even if we have zero interfaces
     io.post([&] { nm.reset(); });
   });
diff --git a/wscript b/wscript
index 50f148c..e414340 100644
--- a/wscript
+++ b/wscript
@@ -175,7 +175,7 @@
 
     if bld.env['HAVE_OSX_FRAMEWORKS']:
         libndn_cxx['source'] += bld.path.ant_glob('src/**/*-osx.cpp')
-        libndn_cxx['use'] += " OSX_COREFOUNDATION OSX_CORESERVICES OSX_SECURITY"
+        libndn_cxx['use'] += " OSX_COREFOUNDATION OSX_CORESERVICES OSX_SECURITY OSX_SYSTEMCONFIGURATION"
 
     if bld.env['HAVE_RTNETLINK']:
         libndn_cxx['source'] += bld.path.ant_glob('src/**/*-rtnl.cpp')
@@ -213,7 +213,7 @@
 
     EXTRA_FRAMEWORKS = ""
     if bld.env['HAVE_OSX_FRAMEWORKS']:
-        EXTRA_FRAMEWORKS = "-framework CoreFoundation -framework CoreServices -framework Security"
+        EXTRA_FRAMEWORKS = "-framework CoreFoundation -framework CoreServices -framework Security -framework SystemConfiguration"
 
     def uniq(alist):
         seen = set()