DnsConfigService and a posix implementation

Contributed by: Szymon Jakubczak <[email protected]>

BUG=90881

TEST=./net_unittests --gtest_filter="DnsConfigServiceTest*"


Review URL: http://codereview.chromium.org/7518028

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@97282 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/net/dns/dns_config_service.cc b/net/dns/dns_config_service.cc
new file mode 100644
index 0000000..f32f293
--- /dev/null
+++ b/net/dns/dns_config_service.cc
@@ -0,0 +1,32 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/dns/dns_config_service.h"
+
+#include "net/base/ip_endpoint.h"
+
+namespace net {
+
+// Default values are taken from glibc resolv.h.
+DnsConfig::DnsConfig()
+  : ndots(1),
+    timeout(base::TimeDelta::FromSeconds(5)),
+    attempts(2),
+    rotate(false),
+    edns0(false) {}
+
+DnsConfig::~DnsConfig() {}
+
+bool DnsConfig::Equals(const DnsConfig& d) const {
+  return (nameservers == d.nameservers) &&
+         (search == d.search) &&
+         (ndots == d.ndots) &&
+         (timeout == d.timeout) &&
+         (attempts == d.attempts) &&
+         (rotate == d.rotate) &&
+         (edns0 == d.edns0);
+}
+
+}  // namespace net
+
diff --git a/net/dns/dns_config_service.h b/net/dns/dns_config_service.h
new file mode 100644
index 0000000..cd4d84c1
--- /dev/null
+++ b/net/dns/dns_config_service.h
@@ -0,0 +1,91 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_DNS_DNS_CONFIG_SERVICE_H_
+#define NET_DNS_DNS_CONFIG_SERVICE_H_
+#pragma once
+
+#include <list>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/time.h"
+#include "net/base/net_export.h"
+
+namespace net {
+
+class IPEndPoint;
+
+// DnsConfig stores configuration of the system resolver.
+struct NET_EXPORT_PRIVATE DnsConfig {
+  DnsConfig();
+  virtual ~DnsConfig();
+
+  bool Equals(const DnsConfig& d) const;
+
+  bool Valid() const {
+    return !nameservers.empty();
+  }
+
+  // List of name server addresses.
+  std::vector<IPEndPoint> nameservers;
+  // Suffix search list; used on first lookup when number of dots in given name
+  // is less than |ndots|.
+  std::vector<std::string> search;
+
+  // Resolver options; see man resolv.conf.
+  // TODO(szym): use |ndots| and |search| to determine the sequence of FQDNs
+  // to query given a specific name.
+
+  // Minimum number of dots before global resolution precedes |search|.
+  int ndots;
+  // Time between retransmissions, see res_state.retrans.
+  base::TimeDelta timeout;
+  // Maximum number of retries, see res_state.retry.
+  int attempts;
+  // Round robin entries in |nameservers| for subsequent requests.
+  bool rotate;
+  // Enable EDNS0 extensions.
+  bool edns0;
+};
+
+// Service for watching when the system DNS settings have changed.
+// Depending on the platform, watches files in /etc/ or win registry.
+class NET_EXPORT_PRIVATE DnsConfigService {
+ public:
+  // Callback interface for the client. The observer is called on the same
+  // thread as Watch(). Observer must outlive the service.
+  class Observer {
+   public:
+    virtual ~Observer() {}
+
+    // Called only when |dns_config| is different from the last check.
+    virtual void OnConfigChanged(const DnsConfig& dns_config) = 0;
+  };
+
+  // Creates the platform-specific DnsConfigService.
+  static DnsConfigService* CreateSystemService();
+
+  DnsConfigService() {}
+  virtual ~DnsConfigService() {}
+
+  // Immediately starts watching system configuration for changes and attempts
+  // to read the configuration. For some platform implementations, the current
+  // thread must have an IO loop (for base::files::FilePathWatcher).
+  virtual void Watch() = 0;
+
+  // If a config is available, |observer| will immediately be called with
+  // OnConfigChanged.
+  virtual void AddObserver(Observer* observer) = 0;
+  virtual void RemoveObserver(Observer* observer) = 0;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(DnsConfigService);
+};
+
+}  // namespace net
+
+#endif  // NET_DNS_DNS_CONFIG_SERVICE_H_
+
diff --git a/net/dns/dns_config_service_posix.cc b/net/dns/dns_config_service_posix.cc
new file mode 100644
index 0000000..d610b1f3
--- /dev/null
+++ b/net/dns/dns_config_service_posix.cc
@@ -0,0 +1,277 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/dns/dns_config_service_posix.h"
+
+#include "base/bind.h"
+#include "base/compiler_specific.h"
+#include "base/file_path.h"
+#include "base/files/file_path_watcher.h"
+#include "base/memory/ref_counted.h"
+#include "base/message_loop.h"
+#include "base/message_loop_proxy.h"
+#include "base/observer_list.h"
+#include "base/scoped_ptr.h"
+#include "base/threading/worker_pool.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/net_util.h"
+
+#ifndef _PATH_RESCONF  // Normally defined in <resolv.h>
+#define _PATH_RESCONF "/etc/resolv.conf"
+#endif
+
+namespace net {
+
+// FilePathWatcher::Delegate is refcounted, so we separate it from the Service
+// It also hosts callbacks on the WorkerPool.
+class DnsConfigServicePosix::WatcherDelegate
+  : public base::files::FilePathWatcher::Delegate {
+ public:
+  // Takes ownership of |lib|.
+  WatcherDelegate(DnsConfigServicePosix* service,
+                  DnsConfigServicePosix::ResolverLib* lib)
+    : service_(service),
+      resolver_lib_(lib),
+      message_loop_(base::MessageLoopProxy::current()),
+      reading_(false),
+      read_pending_(false) {}
+
+  void Cancel() {
+    DCHECK(message_loop_->BelongsToCurrentThread());
+    service_ = NULL;
+  }
+
+  void RescheduleWatch() {
+    DCHECK(message_loop_->BelongsToCurrentThread());
+    // Retry Watch in 100ms or so.
+    message_loop_->PostDelayedTask(
+        FROM_HERE, base::Bind(&WatcherDelegate::StartWatch, this), 100);
+  }
+
+  // FilePathWatcher::Delegate interface
+  virtual void OnFilePathChanged(const FilePath& path) OVERRIDE {
+    DCHECK(message_loop_->BelongsToCurrentThread());
+    if (!service_)
+      return;
+    ScheduleRead();
+  }
+
+  virtual void OnFilePathError(const FilePath& path) OVERRIDE {
+    DCHECK(message_loop_->BelongsToCurrentThread());
+    StartWatch();
+  }
+
+ private:
+  virtual ~WatcherDelegate() {}
+
+  // Unless already scheduled, post DoRead to WorkerPool.
+  void ScheduleRead() {
+    if (reading_) {
+      // Mark that we need to re-read after DoRead posts results.
+      read_pending_ = true;
+    } else {
+      if (!base::WorkerPool::PostTask(FROM_HERE, base::Bind(
+          &WatcherDelegate::DoRead, this), false)) {
+        // See worker_pool_posix.cc.
+        NOTREACHED() << "WorkerPool::PostTask is not expected to fail on posix";
+      }
+      reading_ = true;
+      read_pending_ = false;
+    }
+  }
+
+  // Reads DnsConfig and posts OnResultAvailable to |message_loop_|.
+  // Must be called on the worker thread.
+  void DoRead() {
+    DnsConfig config;
+    struct __res_state res;
+    bool success = false;
+    if (resolver_lib_->ninit(&res) == 0) {
+      success = ConvertResToConfig(res, &config);
+      resolver_lib_->nclose(&res);
+    }
+    // If this fails, the loop is gone, so there is no point retrying.
+    message_loop_->PostTask(FROM_HERE, base::Bind(
+        &WatcherDelegate::OnResultAvailable, this, config, success));
+  }
+
+  // Communicates result to the service. Must be called on the the same thread
+  // that constructed WatcherDelegate.
+  void OnResultAvailable(const DnsConfig &config, bool success) {
+    DCHECK(message_loop_->BelongsToCurrentThread());
+    if (!service_)
+      return;
+    reading_ = false;
+    if (read_pending_) {
+      // Discard this result and re-schedule.
+      ScheduleRead();
+      return;
+    }
+    if (!success) {
+      VLOG(1) << "Failed to read DnsConfig";
+    } else {
+      service_->OnConfigRead(config);
+    }
+  }
+
+  // To avoid refcounting the service, use this method in tasks/callbacks.
+  void StartWatch() {
+    if (!service_)
+      return;
+    service_->StartWatch();
+  }
+
+  DnsConfigServicePosix* service_;
+  scoped_ptr<DnsConfigServicePosix::ResolverLib> resolver_lib_;
+  // Message loop for the thread on which Watch is called (of TYPE_IO).
+  scoped_refptr<base::MessageLoopProxy> message_loop_;
+  // True after DoRead before OnResultsAvailable.
+  bool reading_;
+  // True after OnFilePathChanged fires while |reading_| is true.
+  bool read_pending_;
+};
+
+DnsConfigServicePosix::DnsConfigServicePosix()
+  : have_config_(false),
+    resolver_lib_(new ResolverLib()),
+    watcher_factory_(new FilePathWatcherFactory()) {
+}
+
+DnsConfigServicePosix::~DnsConfigServicePosix() {
+  DCHECK(CalledOnValidThread());
+  if (watcher_delegate_.get())
+    watcher_delegate_->Cancel();
+}
+
+void DnsConfigServicePosix::AddObserver(Observer* observer) {
+  DCHECK(CalledOnValidThread());
+  observers_.AddObserver(observer);
+  if (have_config_) {
+    observer->OnConfigChanged(dns_config_);
+  }
+}
+
+void DnsConfigServicePosix::RemoveObserver(Observer* observer) {
+  DCHECK(CalledOnValidThread());
+  observers_.RemoveObserver(observer);
+}
+
+void DnsConfigServicePosix::Watch() {
+  DCHECK(CalledOnValidThread());
+  DCHECK(!watcher_delegate_.get());
+  DCHECK(!resolv_file_watcher_.get());
+  DCHECK(resolver_lib_.get());
+  DCHECK(watcher_factory_.get());
+
+  watcher_delegate_ = new WatcherDelegate(this, resolver_lib_.release());
+  StartWatch();
+}
+
+void DnsConfigServicePosix::OnConfigRead(const DnsConfig& config) {
+  DCHECK(CalledOnValidThread());
+  if (!config.Equals(dns_config_)) {
+    dns_config_ = config;
+    have_config_ = true;
+    FOR_EACH_OBSERVER(Observer, observers_, OnConfigChanged(config));
+  }
+}
+
+void DnsConfigServicePosix::StartWatch() {
+  DCHECK(CalledOnValidThread());
+  DCHECK(watcher_delegate_.get());
+
+  FilePath path(FILE_PATH_LITERAL(_PATH_RESCONF));
+
+  // FilePathWatcher allows only one Watch per lifetime, so we need a new one.
+  resolv_file_watcher_.reset(watcher_factory_->CreateFilePathWatcher());
+  if (resolv_file_watcher_->Watch(path, watcher_delegate_.get())) {
+    // Make the initial read after watch is installed.
+    watcher_delegate_->OnFilePathChanged(path);
+  } else {
+    VLOG(1) << "Watch failed, scheduling restart";
+    watcher_delegate_->RescheduleWatch();
+  }
+}
+
+int DnsConfigServicePosix::ResolverLib::ninit(res_state statp) {
+  return ::res_ninit(statp);
+}
+
+void DnsConfigServicePosix::ResolverLib::nclose(res_state statp) {
+  return ::res_nclose(statp);
+}
+
+DnsConfigServicePosix::FilePathWatcherShim::FilePathWatcherShim()
+  : watcher_(new base::files::FilePathWatcher()) {}
+DnsConfigServicePosix::FilePathWatcherShim::~FilePathWatcherShim() {}
+
+bool DnsConfigServicePosix::FilePathWatcherShim::Watch(
+    const FilePath& path,
+    base::files::FilePathWatcher::Delegate* delegate) {
+  return watcher_->Watch(path, delegate);
+}
+
+DnsConfigServicePosix::FilePathWatcherShim*
+DnsConfigServicePosix::FilePathWatcherFactory::CreateFilePathWatcher() {
+  return new FilePathWatcherShim();
+}
+
+// static
+DnsConfigService* DnsConfigService::CreateSystemService() {
+  return new DnsConfigServicePosix();
+}
+
+bool ConvertResToConfig(const struct __res_state& res, DnsConfig* dns_config) {
+  CHECK(dns_config != NULL);
+  DCHECK(res.options & RES_INIT);
+
+  dns_config->nameservers.clear();
+
+#if OS_LINUX
+  // Initially, glibc stores IPv6 in _ext.nsaddrs and IPv4 in nsaddr_list.
+  // Next (res_send.c::__libc_res_nsend), it copies nsaddr_list after nsaddrs.
+  // If RES_ROTATE is enabled, the list is shifted left after each res_send.
+  // However, if nsaddr_list changes, it will refill nsaddr_list (IPv4) but
+  // leave the IPv6 entries in nsaddr in the same (shifted) order.
+
+  // Put IPv6 addresses ahead of IPv4.
+  for (int i = 0; i < res._u._ext.nscount6; ++i) {
+    IPEndPoint ipe;
+    if (ipe.FromSockAddr(
+        reinterpret_cast<const struct sockaddr*>(res._u._ext.nsaddrs[i]),
+        sizeof *res._u._ext.nsaddrs[i])) {
+      dns_config->nameservers.push_back(ipe);
+    } else {
+      return false;
+    }
+  }
+#endif
+
+  for (int i = 0; i < res.nscount; ++i) {
+    IPEndPoint ipe;
+    if (ipe.FromSockAddr(
+        reinterpret_cast<const struct sockaddr*>(&res.nsaddr_list[i]),
+        sizeof res.nsaddr_list[i])) {
+      dns_config->nameservers.push_back(ipe);
+    } else {
+      return false;
+    }
+  }
+
+  dns_config->search.clear();
+  for (int i = 0; (i < MAXDNSRCH) && res.dnsrch[i]; ++i) {
+    dns_config->search.push_back(std::string(res.dnsrch[i]));
+  }
+
+  dns_config->ndots = res.ndots;
+  dns_config->timeout = base::TimeDelta::FromSeconds(res.retrans);
+  dns_config->attempts = res.retry;
+  dns_config->rotate = res.options & RES_ROTATE;
+  dns_config->edns0 = res.options & RES_USE_EDNS0;
+
+  return true;
+}
+
+}  // namespace net
+
diff --git a/net/dns/dns_config_service_posix.h b/net/dns/dns_config_service_posix.h
new file mode 100644
index 0000000..10c04ef
--- /dev/null
+++ b/net/dns/dns_config_service_posix.h
@@ -0,0 +1,115 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_DNS_DNS_CONFIG_SERVICE_POSIX_H_
+#define NET_DNS_DNS_CONFIG_SERVICE_POSIX_H_
+#pragma once
+
+#include <resolv.h>
+
+#include "base/compiler_specific.h"
+#include "base/files/file_path_watcher.h"
+#include "base/memory/ref_counted.h"
+#include "base/observer_list.h"
+#include "base/scoped_ptr.h"
+#include "base/threading/non_thread_safe.h"
+#include "net/base/net_export.h"
+#include "net/dns/dns_config_service.h"
+
+namespace net {
+
+class NET_EXPORT_PRIVATE DnsConfigServicePosix
+  : public NON_EXPORTED_BASE(DnsConfigService),
+    public NON_EXPORTED_BASE(base::NonThreadSafe) {
+ public:
+  class ResolverLib;
+  class FilePathWatcherShim;
+  class FilePathWatcherFactory;
+  class WatcherDelegate;
+
+  DnsConfigServicePosix();
+  virtual ~DnsConfigServicePosix();
+
+  virtual void AddObserver(Observer* observer) OVERRIDE;
+  virtual void RemoveObserver(Observer* observer) OVERRIDE;
+  virtual void Watch() OVERRIDE;
+
+  // Takes ownership of |lib|. Must be set before Watch.
+  void set_resolver_lib(ResolverLib* lib) {
+    DCHECK(!watcher_delegate_.get());
+    resolver_lib_.reset(lib);
+  }
+
+  // Takes ownership of |factory|. Must be set before Watch.
+  void set_watcher_factory(FilePathWatcherFactory* factory) {
+    DCHECK(!watcher_delegate_.get());
+    watcher_factory_.reset(factory);
+  }
+
+ private:
+  void OnConfigRead(const DnsConfig& config);
+
+  // Configure a FilePathWatcher and install watcher_delegate_. Executed each
+  // time FilePathWatcher fails.
+  void StartWatch();
+
+  DnsConfig dns_config_;
+  // True after first OnConfigChanged, that is, dns_config_ is valid.
+  bool have_config_;
+
+  scoped_ptr<ResolverLib> resolver_lib_;
+  scoped_ptr<FilePathWatcherFactory> watcher_factory_;
+  scoped_ptr<FilePathWatcherShim> resolv_file_watcher_;
+  scoped_refptr<WatcherDelegate> watcher_delegate_;
+  ObserverList<Observer> observers_;
+
+  DISALLOW_COPY_AND_ASSIGN(DnsConfigServicePosix);
+};
+
+
+// Allows mocking res_ninit.
+class NET_EXPORT_PRIVATE DnsConfigServicePosix::ResolverLib
+  : public NON_EXPORTED_BASE(base::NonThreadSafe) {
+ public:
+  ResolverLib() {}
+  virtual ~ResolverLib() {}
+  virtual int ninit(res_state statp);
+  virtual void nclose(res_state statp);
+ private:
+  DISALLOW_COPY_AND_ASSIGN(ResolverLib);
+};
+
+// Allows mocking FilePathWatcher
+class NET_EXPORT_PRIVATE DnsConfigServicePosix::FilePathWatcherShim
+  : public NON_EXPORTED_BASE(base::NonThreadSafe) {
+ public:
+  FilePathWatcherShim();
+  virtual ~FilePathWatcherShim();
+
+  virtual bool Watch(
+      const FilePath& path,
+      base::files::FilePathWatcher::Delegate* delegate) WARN_UNUSED_RESULT;
+ private:
+  scoped_ptr<base::files::FilePathWatcher> watcher_;
+  DISALLOW_COPY_AND_ASSIGN(FilePathWatcherShim);
+};
+
+class NET_EXPORT_PRIVATE DnsConfigServicePosix::FilePathWatcherFactory
+  : public NON_EXPORTED_BASE(base::NonThreadSafe) {
+ public:
+  FilePathWatcherFactory() {}
+  virtual ~FilePathWatcherFactory() {}
+  virtual FilePathWatcherShim* CreateFilePathWatcher();
+ private:
+  DISALLOW_COPY_AND_ASSIGN(FilePathWatcherFactory);
+};
+
+// Fills in |dns_config| from |res|. Exposed for tests.
+bool NET_EXPORT_PRIVATE ConvertResToConfig(const struct __res_state& res,
+                                           DnsConfig* dns_config);
+
+}  // namespace net
+
+#endif  // NET_DNS_DNS_CONFIG_SERVICE_POSIX_H_
+
diff --git a/net/dns/dns_config_service_posix_unittest.cc b/net/dns/dns_config_service_posix_unittest.cc
new file mode 100644
index 0000000..9c81e14
--- /dev/null
+++ b/net/dns/dns_config_service_posix_unittest.cc
@@ -0,0 +1,472 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <arpa/inet.h>
+#include <resolv.h>
+
+#include "base/bind.h"
+#include "base/compiler_specific.h"
+#include "base/message_loop.h"
+#include "base/message_loop_proxy.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/threading/thread.h"
+#include "net/base/ip_endpoint.h"
+#include "net/dns/dns_config_service_posix.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+
+namespace {
+
+void CompareConfig(const struct __res_state &res, const DnsConfig& config) {
+  EXPECT_EQ(config.ndots, static_cast<int>(res.ndots));
+  EXPECT_EQ(config.edns0, (res.options & RES_USE_EDNS0) != 0);
+  EXPECT_EQ(config.rotate, (res.options & RES_ROTATE) != 0);
+  EXPECT_EQ(config.timeout.InSeconds(), res.retrans);
+  EXPECT_EQ(config.attempts, res.retry);
+
+  // Compare nameservers. IPv6 precede IPv4.
+#if OS_LINUX
+  size_t nscount6 = res._u._ext.nscount6;
+#else
+  size_t nscount6 = 0;
+#endif
+  size_t nscount4 = res.nscount;
+  ASSERT_EQ(config.nameservers.size(), nscount6 + nscount4);
+#if OS_LINUX
+  for (size_t i = 0; i < nscount6; ++i) {
+    IPEndPoint ipe;
+    EXPECT_TRUE(ipe.FromSockAddr(
+        reinterpret_cast<const struct sockaddr*>(res._u._ext.nsaddrs[i]),
+        sizeof *res._u._ext.nsaddrs[i]));
+    EXPECT_EQ(config.nameservers[i], ipe);
+  }
+#endif
+  for (size_t i = 0; i < nscount4; ++i) {
+    IPEndPoint ipe;
+    EXPECT_TRUE(ipe.FromSockAddr(
+        reinterpret_cast<const struct sockaddr*>(&res.nsaddr_list[i]),
+        sizeof res.nsaddr_list[i]));
+    EXPECT_EQ(config.nameservers[nscount6 + i], ipe);
+  }
+
+  ASSERT_TRUE(config.search.size() <= MAXDNSRCH);
+  EXPECT_TRUE(res.dnsrch[config.search.size()] == NULL);
+  for (size_t i = 0; i < config.search.size(); ++i) {
+    EXPECT_EQ(config.search[i], res.dnsrch[i]);
+  }
+}
+
+// Fills in |res| with sane configuration. Change |generation| to add diversity.
+void InitializeResState(res_state res, int generation) {
+  memset(res, 0, sizeof(*res));
+  res->options = RES_INIT | RES_ROTATE;
+  res->ndots = 2;
+  res->retrans = 8;
+  res->retry = 7;
+
+  const char kDnsrch[] = "chromium.org" "\0" "example.com";
+  memcpy(res->defdname, kDnsrch, sizeof(kDnsrch));
+  res->dnsrch[0] = res->defdname;
+  res->dnsrch[1] = res->defdname + sizeof("chromium.org");
+
+  const char* ip4addr[3] = {
+      "8.8.8.8",
+      "192.168.1.1",
+      "63.1.2.4",
+  };
+
+  for (int i = 0; i < 3; ++i) {
+    struct sockaddr_in sa;
+    sa.sin_family = AF_INET;
+    sa.sin_port = htons(NS_DEFAULTPORT + i - generation);
+    inet_pton(AF_INET, ip4addr[i], &sa.sin_addr);
+    res->nsaddr_list[i] = sa;
+  }
+  res->nscount = 3;
+
+#if OS_LINUX
+  const char* ip6addr[2] = {
+      "2001:db8:0::42",
+      "::FFFF:129.144.52.38",
+  };
+
+  for (int i = 0; i < 2; ++i) {
+    // Must use malloc to mimick res_ninit.
+    struct sockaddr_in6 *sa6;
+    sa6 = (struct sockaddr_in6 *)malloc(sizeof(*sa6));
+    sa6->sin6_family = AF_INET6;
+    sa6->sin6_port = htons(NS_DEFAULTPORT - i);
+    inet_pton(AF_INET6, ip6addr[i], &sa6->sin6_addr);
+    res->_u._ext.nsaddrs[i] = sa6;
+  }
+  res->_u._ext.nscount6 = 2;
+#endif
+}
+
+void CloseResState(res_state res) {
+#if OS_LINUX
+  for (int i = 0; i < res->_u._ext.nscount6; ++i) {
+    ASSERT_TRUE(res->_u._ext.nsaddrs[i] != NULL);
+    free(res->_u._ext.nsaddrs[i]);
+  }
+#endif
+}
+
+class DnsConfigServiceTest : public testing::Test,
+                             public DnsConfigService::Observer {
+ public:
+  // Mocks
+
+  // DnsConfigService owns the instances of ResolverLib and
+  // FilePathWatcherFactory that it gets, so use simple proxies to call
+  // DnsConfigServiceTest.
+
+  // ResolverLib is owned by WatcherDelegate which is posted to WorkerPool so
+  // it must be canceled before the test is over.
+  class MockResolverLib : public DnsConfigServicePosix::ResolverLib {
+   public:
+    explicit MockResolverLib(DnsConfigServiceTest *test) : test_(test) {}
+    virtual ~MockResolverLib() {
+      base::AutoLock lock(lock_);
+      if (test_) {
+        EXPECT_TRUE(test_->IsComplete());
+      }
+    }
+    virtual int ninit(res_state res) OVERRIDE {
+      base::AutoLock lock(lock_);
+      if (test_)
+        return test_->OnNinit(res);
+      else
+        return -1;
+    }
+    virtual void nclose(res_state res) OVERRIDE {
+      CloseResState(res);
+    }
+    void Cancel() {
+      base::AutoLock lock(lock_);
+      test_ = NULL;
+    }
+   private:
+    base::Lock lock_;
+    DnsConfigServiceTest *test_;
+  };
+
+  class MockFilePathWatcherShim
+    : public DnsConfigServicePosix::FilePathWatcherShim {
+   public:
+    typedef base::files::FilePathWatcher::Delegate Delegate;
+
+    explicit MockFilePathWatcherShim(DnsConfigServiceTest* t) : test_(t) {}
+    virtual ~MockFilePathWatcherShim() {
+      test_->OnShimDestroyed(this);
+    }
+
+    // Enforce one-Watch-per-lifetime as the original FilePathWatcher
+    virtual bool Watch(const FilePath& path,
+                       Delegate* delegate) OVERRIDE {
+      EXPECT_TRUE(path_.empty()) << "Only one-Watch-per-lifetime allowed.";
+      EXPECT_TRUE(!delegate_.get());
+      path_ = path;
+      delegate_ = delegate;
+      return test_->OnWatch();
+    }
+
+    void PathChanged() {
+      delegate_->OnFilePathChanged(path_);
+    }
+
+    void PathError() {
+      delegate_->OnFilePathError(path_);
+    }
+
+   private:
+    FilePath path_;
+    scoped_refptr<Delegate> delegate_;
+    DnsConfigServiceTest* test_;
+  };
+
+  class MockFilePathWatcherFactory
+    : public DnsConfigServicePosix::FilePathWatcherFactory {
+   public:
+    explicit MockFilePathWatcherFactory(DnsConfigServiceTest* t) : test(t) {}
+    virtual ~MockFilePathWatcherFactory() {
+      EXPECT_TRUE(test->IsComplete());
+    }
+    virtual DnsConfigServicePosix::FilePathWatcherShim*
+        CreateFilePathWatcher() OVERRIDE {
+      return test->CreateFilePathWatcher();
+    }
+    DnsConfigServiceTest* test;
+  };
+
+  // Helpers for mocks.
+
+  DnsConfigServicePosix::FilePathWatcherShim* CreateFilePathWatcher() {
+    watcher_shim_ = new MockFilePathWatcherShim(this);
+    return watcher_shim_;
+  }
+
+  void OnShimDestroyed(MockFilePathWatcherShim* destroyed_shim) {
+    // Precaution to avoid segfault.
+    if (watcher_shim_ == destroyed_shim)
+      watcher_shim_ = NULL;
+  }
+
+  // On each event, post QuitTask to allow use of MessageLoop::Run() to
+  // synchronize the threads.
+
+  bool OnWatch() {
+    EXPECT_TRUE(message_loop_ == MessageLoop::current());
+    watch_called_ = true;
+    BreakNow("OnWatch");
+    return !fail_on_watch_;
+  }
+
+  int OnNinit(res_state res) {
+    { // Check that res_ninit is executed serially.
+      base::AutoLock lock(ninit_lock_);
+      EXPECT_FALSE(ninit_running_) << "res_ninit is not called serially!";
+      ninit_running_ = true;
+    }
+    BreakNow("OnNinit");
+    ninit_allowed_.Wait();
+    // Calling from another thread is a bit dirty, but it's protected.
+    int rv = OnNinitNonThreadSafe(res);
+    // This lock might be destroyed after ninit_called_ is signalled.
+    {
+      base::AutoLock lock(ninit_lock_);
+      ninit_running_ = false;
+    }
+    ninit_called_.Signal();
+    return rv;
+  }
+
+  virtual void OnConfigChanged(const DnsConfig& new_config) OVERRIDE {
+    EXPECT_TRUE(message_loop_ == MessageLoop::current());
+    CompareConfig(res_, new_config);
+    EXPECT_FALSE(new_config.Equals(last_config_)) <<
+        "Config must be different from last call.";
+    last_config_ = new_config;
+    got_config_ = true;
+    BreakNow("OnConfigChanged");
+  }
+
+  bool IsComplete() {
+    return complete_;
+  }
+
+ protected:
+  friend class BreakTask;
+  class BreakTask : public Task {
+   public:
+    BreakTask(DnsConfigServiceTest* test, std::string breakpoint)
+      : test_(test), breakpoint_(breakpoint) {}
+    virtual ~BreakTask() {}
+    virtual void Run() OVERRIDE {
+      test_->breakpoint_ = breakpoint_;
+      MessageLoop::current()->QuitNow();
+    }
+   private:
+    DnsConfigServiceTest* test_;
+    std::string breakpoint_;
+  };
+
+  void BreakNow(std::string b) {
+    message_loop_->PostTask(FROM_HERE, new BreakTask(this, b));
+  }
+
+  void RunUntilBreak(std::string b) {
+    message_loop_->Run();
+    ASSERT_EQ(breakpoint_, b);
+  }
+
+  DnsConfigServiceTest()
+      : res_lib_(new MockResolverLib(this)),
+        watcher_shim_(NULL),
+        res_generation_(1),
+        watch_called_(false),
+        got_config_(false),
+        fail_on_watch_(false),
+        fail_on_ninit_(false),
+        complete_(false),
+        ninit_allowed_(false, false),
+        ninit_called_(false, false),
+        ninit_running_(false) {
+  }
+
+  // This is on WorkerPool, but protected by ninit_allowed_.
+  int OnNinitNonThreadSafe(res_state res) {
+    if (fail_on_ninit_)
+      return -1;
+    InitializeResState(res, res_generation_);
+    // Store a (deep) copy in the fixture to later verify correctness.
+    CloseResState(&res_);
+    InitializeResState(&res_, res_generation_);
+    return 0;
+  }
+
+  // Helpers for tests.
+
+  // Lets OnNinit run OnNinitNonThreadSafe and waits for it to complete.
+  // Might get OnConfigChanged scheduled on the loop but we have no certainty.
+  void WaitForNinit() {
+    RunUntilBreak("OnNinit");
+    ninit_allowed_.Signal();
+    ninit_called_.Wait();
+  }
+
+  // test::Test methods
+  virtual void SetUp() OVERRIDE {
+    message_loop_ = MessageLoop::current();
+    service_.reset(new DnsConfigServicePosix());
+    service_->set_resolver_lib(res_lib_);
+    service_->set_watcher_factory(new MockFilePathWatcherFactory(this));
+    memset(&res_, 0, sizeof(res_));
+  }
+
+  virtual void TearDown() OVERRIDE {
+    // res_lib_ could outlive the test, so make sure it doesn't call it.
+    res_lib_->Cancel();
+    CloseResState(&res_);
+    // Allow service_ to clean up ResolverLib and FilePathWatcherFactory.
+    complete_ = true;
+  }
+
+  MockResolverLib* res_lib_;
+  MockFilePathWatcherShim* watcher_shim_;
+  // Adds variety to the content of res_state.
+  int res_generation_;
+  struct __res_state res_;
+  DnsConfig last_config_;
+
+  bool watch_called_;
+  bool got_config_;
+  bool fail_on_watch_;
+  bool fail_on_ninit_;
+  bool complete_;
+
+  // Ninit is called on WorkerPool so we need to synchronize with it.
+  base::WaitableEvent ninit_allowed_;
+  base::WaitableEvent ninit_called_;
+
+  // Protected by ninit_lock_. Used to verify that Ninit calls are serialized.
+  bool ninit_running_;
+  base::Lock ninit_lock_;
+
+  // Loop for this thread.
+  MessageLoop* message_loop_;
+
+  // Service under test.
+  scoped_ptr<DnsConfigServicePosix> service_;
+
+  std::string breakpoint_;
+};
+
+TEST(DnsConfigTest, ResolverConfigConvertAndEquals) {
+  struct __res_state res[2];
+  DnsConfig config[2];
+  for (int i = 0; i < 2; ++i) {
+    InitializeResState(&res[i], i);
+    ASSERT_TRUE(ConvertResToConfig(res[i], &config[i]));
+  }
+  for (int i = 0; i < 2; ++i) {
+    CompareConfig(res[i], config[i]);
+    CloseResState(&res[i]);
+  }
+  EXPECT_TRUE(config[0].Equals(config[0]));
+  EXPECT_FALSE(config[0].Equals(config[1]));
+  EXPECT_FALSE(config[1].Equals(config[0]));
+}
+
+TEST_F(DnsConfigServiceTest, FilePathWatcherFailures) {
+  // For these tests, disable ninit.
+  res_lib_->Cancel();
+
+  fail_on_watch_ = true;
+  service_->Watch();
+  RunUntilBreak("OnWatch");
+  EXPECT_TRUE(watch_called_) << "Must call FilePathWatcher::Watch().";
+
+  fail_on_watch_ = false;
+  watch_called_ = false;
+  RunUntilBreak("OnWatch");  // Due to backoff this will take 100ms.
+  EXPECT_TRUE(watch_called_) <<
+      "Must restart on FilePathWatcher::Watch() failure.";
+
+  watch_called_ = false;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathError();
+  RunUntilBreak("OnWatch");
+  EXPECT_TRUE(watch_called_) <<
+      "Must restart on FilePathWatcher::Delegate::OnFilePathError().";
+
+  // Worker thread could still be posting OnResultAvailable to the message loop
+}
+
+TEST_F(DnsConfigServiceTest, NotifyOnValidAndDistinctConfig) {
+  service_->AddObserver(this);
+  service_->Watch();
+  RunUntilBreak("OnWatch");
+  fail_on_ninit_ = true;
+  WaitForNinit();
+
+  // If OnNinit posts OnResultAvailable before the next call, then this test
+  // verifies that failure on ninit should not cause OnConfigChanged.
+  // Otherwise, this only verifies that ninit calls are serialized.
+
+  fail_on_ninit_ = false;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_);
+
+  message_loop_->AssertIdle();
+
+  got_config_ = false;
+  // Forget about the config to test if we get it again on AddObserver.
+  last_config_ = DnsConfig();
+  service_->RemoveObserver(this);
+  service_->AddObserver(this);
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_) << "Did not get config after AddObserver.";
+
+  // Simulate spurious FilePathChanged.
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+
+  // OnConfigChanged will catch that the config did not actually change.
+
+  got_config_ = false;
+  ++res_generation_;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_) << "Did not get config after change";
+
+  message_loop_->AssertIdle();
+
+  // Schedule two calls. OnNinit checks if it is called serially.
+  ++res_generation_;
+  ASSERT_TRUE(watcher_shim_);
+  watcher_shim_->PathChanged();
+  // ninit is blocked, so this will have to induce read_pending
+  watcher_shim_->PathChanged();
+  WaitForNinit();
+  WaitForNinit();
+  RunUntilBreak("OnConfigChanged");
+  EXPECT_TRUE(got_config_) << "Did not get config after change";
+
+  // We should be done with all tasks.
+  message_loop_->AssertIdle();
+}
+
+}  // namespace
+
+}  // namespace net
+
diff --git a/net/net.gyp b/net/net.gyp
index 8010016..40bade8 100644
--- a/net/net.gyp
+++ b/net/net.gyp
@@ -290,6 +290,10 @@
         'disk_cache/trace.h',
         'dns/async_host_resolver.cc',
         'dns/async_host_resolver.h',
+        'dns/dns_config_service.cc',
+        'dns/dns_config_service.h',
+        'dns/dns_config_service_posix.cc',
+        'dns/dns_config_service_posix.h',
         'dns/dns_query.cc',
         'dns/dns_query.h',
         'dns/dns_response.cc',
@@ -941,6 +945,7 @@
         'disk_cache/mapped_file_unittest.cc',
         'disk_cache/storage_block_unittest.cc',
         'dns/async_host_resolver_unittest.cc',
+        'dns/dns_config_service_posix_unittest.cc',
         'dns/dns_query_unittest.cc',
         'dns/dns_response_unittest.cc',
         'dns/dns_transaction_unittest.cc',
@@ -1108,6 +1113,7 @@
         ],
         [ 'OS == "win"', {
             'sources!': [
+              'dns/dns_config_service_posix_unittest.cc',
               'http/http_auth_gssapi_posix_unittest.cc',
             ],
             # This is needed to trigger the dll copy step on windows.