[mdns] support config files

This CL adds mdns support for multiple config files. The config files
can be used to suppress host name probing (for faster startup) and to
publish service instances. The latter is used to publish the device
as a "_fuchsia._udp." instance, which was previously hard-coded.

TEST: avahi-browse -t _fuchsia._udp.

Change-Id: I2e3657eefdf75a2626a6a73127fd171182c84d10
diff --git a/garnet/bin/mdns/service/BUILD.gn b/garnet/bin/mdns/service/BUILD.gn
index cbc49f4..15a9c9a 100644
--- a/garnet/bin/mdns/service/BUILD.gn
+++ b/garnet/bin/mdns/service/BUILD.gn
@@ -48,6 +48,8 @@
     "address_prober.h",
     "address_responder.cc",
     "address_responder.h",
+    "config.cc",
+    "config.h",
     "dns_formatting.cc",
     "dns_formatting.h",
     "dns_message.cc",
@@ -99,19 +101,20 @@
     "//garnet/lib/inet",
     "//garnet/public/lib/fostr",
     "//garnet/public/lib/fsl",
+    "//garnet/public/lib/json",
+    "//garnet/public/lib/rapidjson_utils",
     "//garnet/public/lib/svc/cpp",
     "//sdk/fidl/fuchsia.mdns",
     "//sdk/fidl/fuchsia.netstack",
     "//sdk/fidl/fuchsia.sys",
     "//sdk/lib/sys/cpp",
     "//src/lib/fxl",
+    "//third_party/rapidjson",
     "//zircon/public/lib/fit",
   ]
 
   if (enable_mdns_trace) {
-    defines = [
-      "MDNS_TRACE",
-    ]
+    defines = [ "MDNS_TRACE" ]
   }
 }
 
@@ -119,6 +122,7 @@
   output_name = "mdns_tests"
 
   sources = [
+    "test/config_test.cc",
     "test/dns_names_test.cc",
     "test/dns_reading_test.cc",
     "test/interface_transceiver_test.cc",
@@ -156,7 +160,14 @@
     "mdns.config",
   ]
   sources = [
-    rebase_path("service.config"),
+    rebase_path("config/service.config"),
+  ]
+}
+
+config_data("mdns_fuchsia_udp_config") {
+  for_pkg = "mdns"
+  sources = [
+    rebase_path("config/fuchsia_udp.config"),
   ]
 }
 
diff --git a/garnet/bin/mdns/service/config.cc b/garnet/bin/mdns/service/config.cc
new file mode 100644
index 0000000..840ece9
--- /dev/null
+++ b/garnet/bin/mdns/service/config.cc
@@ -0,0 +1,197 @@
+// Copyright 2019 The Fuchsia 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 "garnet/bin/mdns/service/config.h"
+
+#include <sstream>
+
+#include "garnet/bin/mdns/service/mdns_names.h"
+#include "garnet/public/lib/rapidjson_utils/rapidjson_validation.h"
+#include "src/lib/fxl/logging.h"
+
+namespace mdns {
+namespace {
+
+const char kSchema[] = R"({
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "perform_host_name_probe": {
+      "type": "boolean"
+    },
+    "publications": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "additionalProperties": false,
+        "properties": {
+          "service": {
+            "type": "string",
+            "maxLength": 22
+          },
+          "instance": {
+            "type": "string",
+            "maxLength": 63
+          },
+          "port": {
+            "type": "integer",
+            "minimum": 1,
+            "maximum": 65535
+          },
+          "text": {
+            "type": "array",
+            "items": {
+              "type": "string",
+              "maxLength": 255
+            }
+          },
+          "perform_probe": {
+            "type": "boolean"
+          }
+        },
+        "required": ["service","port"]
+      }
+    }
+  }
+})";
+
+const char kPerformHostNameProbeKey[] = "perform_host_name_probe";
+const char kPublicationsKey[] = "publications";
+const char kServiceKey[] = "service";
+const char kInstanceKey[] = "instance";
+const char kPortKey[] = "port";
+const char kTextKey[] = "text";
+const char kPerformProbeKey[] = "perform_probe";
+
+}  // namespace
+
+//  static
+const char Config::kConfigDir[] = "/config/data";
+
+void Config::ReadConfigFiles(const std::string& host_name,
+                             const std::string& config_dir) {
+  FXL_DCHECK(MdnsNames::IsValidHostName(host_name));
+
+  auto schema = rapidjson_utils::InitSchema(kSchema);
+  parser_.ParseFromDirectory(
+      config_dir, [this, &schema, &host_name](rapidjson::Document document) {
+        if (!rapidjson_utils::ValidateSchema(document, *schema)) {
+          parser_.ReportError("Schema validation failure.");
+          return;
+        }
+
+        IntegrateDocument(document, host_name);
+      });
+}
+
+void Config::IntegrateDocument(const rapidjson::Document& document,
+                               const std::string& host_name) {
+  FXL_DCHECK(document.IsObject());
+
+  if (document.HasMember(kPerformHostNameProbeKey)) {
+    FXL_DCHECK(document[kPerformHostNameProbeKey].IsBool());
+    SetPerformHostNameProbe(document[kPerformHostNameProbeKey].GetBool());
+    if (parser_.HasError()) {
+      return;
+    }
+  }
+
+  if (document.HasMember(kPublicationsKey)) {
+    FXL_DCHECK(document[kPublicationsKey].IsArray());
+    for (auto& item : document[kPublicationsKey].GetArray()) {
+      IntegratePublication(item, host_name);
+      if (parser_.HasError()) {
+        return;
+      }
+    }
+  }
+}
+
+void Config::IntegratePublication(const rapidjson::Value& value,
+                                  const std::string& host_name) {
+  FXL_DCHECK(value.IsObject());
+  FXL_DCHECK(value.HasMember(kServiceKey));
+  FXL_DCHECK(value[kServiceKey].IsString());
+  FXL_DCHECK(value.HasMember(kPortKey));
+  FXL_DCHECK(value[kPortKey].IsUint());
+  FXL_DCHECK(value[kPortKey].GetUint() >= 1);
+  FXL_DCHECK(value[kPortKey].GetUint() <= 65535);
+
+  auto service = value[kServiceKey].GetString();
+  if (!MdnsNames::IsValidServiceName(service)) {
+    parser_.ReportError((std::stringstream()
+                         << kServiceKey << " value " << service
+                         << " is not a valid service name.")
+                            .str());
+    return;
+  }
+
+  std::string instance;
+  if (value.HasMember(kInstanceKey)) {
+    instance = value[kInstanceKey].GetString();
+    if (!MdnsNames::IsValidInstanceName(instance)) {
+      parser_.ReportError((std::stringstream()
+                           << kInstanceKey << " value " << instance
+                           << " is not a valid instance name.")
+                              .str());
+      return;
+    }
+  } else {
+    instance = host_name;
+    if (!MdnsNames::IsValidInstanceName(instance)) {
+      parser_.ReportError((std::stringstream()
+                           << "Publication of service " << service
+                           << " specifies that the host name should be "
+                              "used as the instance name, but "
+                           << host_name << "is not a valid instance name.")
+                              .str());
+      return;
+    }
+  }
+
+  std::vector<std::string> text;
+  if (value.HasMember(kTextKey)) {
+    FXL_DCHECK(value[kTextKey].IsArray());
+    for (auto& item : value[kTextKey].GetArray()) {
+      FXL_DCHECK(item.IsString());
+      if (!MdnsNames::IsValidTextString(item.GetString())) {
+        parser_.ReportError((std::stringstream()
+                             << kTextKey << " item value " << item.GetString()
+                             << " is not avalid text string.")
+                                .str());
+        return;
+      }
+
+      text.push_back(item.GetString());
+    }
+  }
+
+  bool perform_probe = true;
+  if (value.HasMember(kPerformProbeKey)) {
+    FXL_DCHECK(value[kPerformProbeKey].IsBool());
+    perform_probe = value[kPerformProbeKey].GetBool();
+  }
+
+  publications_.emplace_back(Publication{
+      .service_ = service,
+      .instance_ = instance,
+      .publication_ = Mdns::Publication::Create(
+          inet::IpPort::From_uint16_t(value[kPortKey].GetUint()), text),
+      .perform_probe_ = perform_probe});
+}
+
+void Config::SetPerformHostNameProbe(bool perform_host_name_probe) {
+  if (perform_host_name_probe_.has_value() &&
+      perform_host_name_probe_.value() != perform_host_name_probe) {
+    parser_.ReportError((std::stringstream()
+                         << "Conflicting " << kPerformHostNameProbeKey
+                         << " value.")
+                            .str());
+    return;
+  }
+
+  perform_host_name_probe_ = perform_host_name_probe;
+}
+
+}  // namespace mdns
diff --git a/garnet/bin/mdns/service/config.h b/garnet/bin/mdns/service/config.h
new file mode 100644
index 0000000..4e47289
--- /dev/null
+++ b/garnet/bin/mdns/service/config.h
@@ -0,0 +1,75 @@
+// Copyright 2019 The Fuchsia 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 GARNET_BIN_MDNS_SERVICE_CONFIG_H_
+#define GARNET_BIN_MDNS_SERVICE_CONFIG_H_
+
+#include <optional>
+
+#include "garnet/bin/mdns/service/mdns.h"
+#include "lib/json/json_parser.h"
+#include "rapidjson/document.h"
+
+namespace mdns {
+
+class Config {
+ public:
+  // Describes a publication from config files.
+  struct Publication {
+    std::string service_;
+    std::string instance_;
+    std::unique_ptr<Mdns::Publication> publication_;
+    bool perform_probe_;
+  };
+
+  Config() = default;
+  ~Config() = default;
+
+  // Reads the config files from |config_dir|. |host_name| is the host name as
+  // defined by the operating system (e.g. the result of posix's |gethostname|).
+  // The default value for |config_dir| is "/config/data".
+  void ReadConfigFiles(const std::string& host_name,
+                       const std::string& config_dir = kConfigDir);
+
+  // Indicates whether the configuration is valid.
+  bool valid() { return !parser_.HasError(); }
+
+  // Returns a string describing the error if |valid()| returns true, otherwise
+  // an empty string.
+  std::string error() { return parser_.error_str(); }
+
+  // Indicates whether a probe should be performed for the hostname.
+  bool perform_host_name_probe() {
+    return perform_host_name_probe_.has_value()
+               ? perform_host_name_probe_.value()
+               : true;
+  }
+
+  // Gets the publications.
+  const std::vector<Publication>& publications() { return publications_; }
+
+ private:
+  static const char kConfigDir[];
+
+  // Integrates the config file represented by |document| into this
+  // configuration.
+  void IntegrateDocument(const rapidjson::Document& document,
+                         const std::string& host_name);
+
+  // Integrates the publication represented by |value| into this configuration.
+  // |value| must be a JSON object.
+  void IntegratePublication(const rapidjson::Value& value,
+                            const std::string& host_name);
+
+  // Sets the value indicating whether a host name probe is required.
+  void SetPerformHostNameProbe(bool perform_host_name_probe);
+
+  json::JSONParser parser_;
+  std::optional<bool> perform_host_name_probe_;
+  std::vector<Publication> publications_;
+};
+
+}  // namespace mdns
+
+#endif  // GARNET_BIN_MDNS_SERVICE_CONFIG_H_
diff --git a/garnet/bin/mdns/service/config/fuchsia_udp.config b/garnet/bin/mdns/service/config/fuchsia_udp.config
new file mode 100644
index 0000000..7cfaa5d
--- /dev/null
+++ b/garnet/bin/mdns/service/config/fuchsia_udp.config
@@ -0,0 +1,9 @@
+{
+  "publications": [
+    {
+        "service": "_fuchsia._udp.",
+        "port": 5353,
+        "perform_probe": false
+    }
+  ]
+}
diff --git a/garnet/bin/mdns/service/service.config b/garnet/bin/mdns/service/config/service.config
similarity index 100%
rename from garnet/bin/mdns/service/service.config
rename to garnet/bin/mdns/service/config/service.config
diff --git a/garnet/bin/mdns/service/instance_responder.cc b/garnet/bin/mdns/service/instance_responder.cc
index 5a42564..6f6a35d 100644
--- a/garnet/bin/mdns/service/instance_responder.cc
+++ b/garnet/bin/mdns/service/instance_responder.cc
@@ -145,25 +145,25 @@
     const Mdns::Publication& publication, const std::string& subtype,
     const ReplyAddress& reply_address) const {
   if (!subtype.empty()) {
-    SendSubtypePtrRecord(subtype, publication.ptr_ttl_seconds, reply_address);
+    SendSubtypePtrRecord(subtype, publication.ptr_ttl_seconds_, reply_address);
   }
 
   auto ptr_resource = std::make_shared<DnsResource>(
       MdnsNames::LocalServiceFullName(service_name_), DnsType::kPtr);
-  ptr_resource->time_to_live_ = publication.ptr_ttl_seconds;
+  ptr_resource->time_to_live_ = publication.ptr_ttl_seconds_;
   ptr_resource->ptr_.pointer_domain_name_ = instance_full_name_;
   SendResource(ptr_resource, MdnsResourceSection::kAnswer, reply_address);
 
   auto srv_resource =
       std::make_shared<DnsResource>(instance_full_name_, DnsType::kSrv);
-  srv_resource->time_to_live_ = publication.srv_ttl_seconds;
+  srv_resource->time_to_live_ = publication.srv_ttl_seconds_;
   srv_resource->srv_.port_ = publication.port_;
   srv_resource->srv_.target_ = host_full_name_;
   SendResource(srv_resource, MdnsResourceSection::kAdditional, reply_address);
 
   auto txt_resource =
       std::make_shared<DnsResource>(instance_full_name_, DnsType::kTxt);
-  txt_resource->time_to_live_ = publication.txt_ttl_seconds;
+  txt_resource->time_to_live_ = publication.txt_ttl_seconds_;
   txt_resource->txt_.strings_ = publication.text_;
   SendResource(txt_resource, MdnsResourceSection::kAdditional, reply_address);
 
@@ -185,9 +185,9 @@
 
 void InstanceResponder::SendGoodbye() const {
   Mdns::Publication publication;
-  publication.ptr_ttl_seconds = 0;
-  publication.srv_ttl_seconds = 0;
-  publication.txt_ttl_seconds = 0;
+  publication.ptr_ttl_seconds_ = 0;
+  publication.srv_ttl_seconds_ = 0;
+  publication.txt_ttl_seconds_ = 0;
 
   SendPublication(publication);
 }
diff --git a/garnet/bin/mdns/service/mdns.cc b/garnet/bin/mdns/service/mdns.cc
index cbc50d7..0fa8564 100644
--- a/garnet/bin/mdns/service/mdns.cc
+++ b/garnet/bin/mdns/service/mdns.cc
@@ -6,9 +6,11 @@
 
 #include <lib/async/cpp/task.h>
 #include <lib/async/default.h>
+
 #include <iostream>
 #include <limits>
 #include <unordered_set>
+
 #include "garnet/bin/mdns/service/address_prober.h"
 #include "garnet/bin/mdns/service/address_responder.h"
 #include "garnet/bin/mdns/service/dns_formatting.h"
@@ -35,7 +37,8 @@
 }
 
 void Mdns::Start(fuchsia::netstack::NetstackPtr netstack,
-                 const std::string& host_name, fit::closure ready_callback) {
+                 const std::string& host_name, bool perform_address_probe,
+                 fit::closure ready_callback) {
   FXL_DCHECK(!host_name.empty());
   FXL_DCHECK(ready_callback);
   FXL_DCHECK(state_ == State::kNotStarted);
@@ -53,23 +56,16 @@
 
   transceiver_.Start(
       std::move(netstack),
-      [this]() {
+      [this, perform_address_probe]() {
         // TODO(dalesat): Link changes that create host name conflicts.
         // Once we have a NIC and we've decided on a unique host name, we
         // don't do any more address probes. This means that we could have link
         // changes that cause two hosts with the same name to be on the same
         // subnet. To improve matters, we need to be prepared to change a host
         // name we've been using for awhile.
-        // TODO(dalesat): Add option to skip address probe.
-        // The mDNS spec is explicit about the need for address probes and
-        // that host names should be user-friendly. Many embedded devices, on
-        // the other hand, use host names that are guaranteed unique by virtue
-        // of including large random values, serial numbers, etc. This mDNS
-        // implementation should offer the option of turning off address probes
-        // for such devices.
         if (state_ == State::kWaitingForInterfaces &&
             transceiver_.has_interfaces()) {
-          StartAddressProbe(original_host_name_);
+          OnInterfacesStarted(original_host_name_, perform_address_probe);
         }
       },
       [this](std::unique_ptr<DnsMessage> message,
@@ -116,7 +112,7 @@
   // The interface monitor may have already found interfaces. In that case,
   // start the address probe in case we don't get any link change notifications.
   if (state_ == State::kWaitingForInterfaces && transceiver_.has_interfaces()) {
-    StartAddressProbe(original_host_name_);
+    OnInterfacesStarted(original_host_name_, perform_address_probe);
   }
 }
 
@@ -179,18 +175,24 @@
 
 void Mdns::LogTraffic() { transceiver_.LogTraffic(); }
 
+void Mdns::OnInterfacesStarted(const std::string& host_name,
+                               bool perform_address_probe) {
+  if (perform_address_probe) {
+    StartAddressProbe(host_name);
+    return;
+  }
+
+  RegisterHostName(host_name);
+  OnReady();
+}
+
 void Mdns::StartAddressProbe(const std::string& host_name) {
   state_ = State::kAddressProbeInProgress;
 
-  host_name_ = host_name;
-  host_full_name_ = MdnsNames::LocalHostFullName(host_name);
-
+  RegisterHostName(host_name);
   std::cerr << "mDNS: Verifying uniqueness of host name " << host_full_name_
             << "\n";
 
-  address_placeholder_ =
-      std::make_shared<DnsResource>(host_full_name_, DnsType::kA);
-
   // Create an address prober to look for host name conflicts. The address
   // prober removes itself immediately before it calls the callback.
   auto address_prober =
@@ -204,25 +206,7 @@
           return;
         }
 
-        std::cerr << "mDNS: Using unique host name " << host_full_name_ << "\n";
-
-        // Start all the agents.
-        state_ = State::kActive;
-
-        // |resource_renewer_| doesn't need to be started, but we do it
-        // anyway in case that changes.
-        resource_renewer_->Start(host_full_name_);
-
-        for (auto agent : agents_awaiting_start_) {
-          AddAgent(agent);
-        }
-
-        agents_awaiting_start_.clear();
-
-        // Let the client know we're ready.
-        FXL_DCHECK(ready_callback_);
-        ready_callback_();
-        ready_callback_ = nullptr;
+        OnReady();
       });
 
   // We don't use |AddAgent| here, because agents added that way don't
@@ -232,6 +216,35 @@
   SendMessages();
 }
 
+void Mdns::RegisterHostName(const std::string& host_name) {
+  host_name_ = host_name;
+  host_full_name_ = MdnsNames::LocalHostFullName(host_name);
+  address_placeholder_ =
+      std::make_shared<DnsResource>(host_full_name_, DnsType::kA);
+}
+
+void Mdns::OnReady() {
+  std::cerr << "mDNS: Using unique host name " << host_full_name_ << "\n";
+
+  // Start all the agents.
+  state_ = State::kActive;
+
+  // |resource_renewer_| doesn't need to be started, but we do it
+  // anyway in case that changes.
+  resource_renewer_->Start(host_full_name_);
+
+  for (auto agent : agents_awaiting_start_) {
+    AddAgent(agent);
+  }
+
+  agents_awaiting_start_.clear();
+
+  // Let the client know we're ready.
+  FXL_DCHECK(ready_callback_);
+  ready_callback_();
+  ready_callback_ = nullptr;
+}
+
 void Mdns::OnHostNameConflict() {
   // TODO(dalesat): Support other renaming strategies?
   std::ostringstream os;
@@ -475,6 +488,14 @@
   return publication;
 }
 
+std::unique_ptr<Mdns::Publication> Mdns::Publication::Clone() {
+  auto result = Create(port_, text_);
+  result->ptr_ttl_seconds_ = ptr_ttl_seconds_;
+  result->srv_ttl_seconds_ = srv_ttl_seconds_;
+  result->txt_ttl_seconds_ = txt_ttl_seconds_;
+  return result;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 
 Mdns::Subscriber::~Subscriber() { Unsubscribe(); }
diff --git a/garnet/bin/mdns/service/mdns.h b/garnet/bin/mdns/service/mdns.h
index de3d80d..9c7867b 100644
--- a/garnet/bin/mdns/service/mdns.h
+++ b/garnet/bin/mdns/service/mdns.h
@@ -8,11 +8,13 @@
 #include <fuchsia/netstack/cpp/fidl.h>
 #include <lib/async/dispatcher.h>
 #include <lib/fit/function.h>
+
 #include <memory>
 #include <queue>
 #include <string>
 #include <unordered_map>
 #include <vector>
+
 #include "garnet/bin/mdns/service/dns_message.h"
 #include "garnet/bin/mdns/service/mdns_agent.h"
 #include "garnet/bin/mdns/service/mdns_transceiver.h"
@@ -36,11 +38,13 @@
         inet::IpPort port,
         const std::vector<std::string>& text = std::vector<std::string>());
 
+    std::unique_ptr<Publication> Clone();
+
     inet::IpPort port_;
     std::vector<std::string> text_;
-    uint32_t ptr_ttl_seconds = 4500;  // default 75 minutes
-    uint32_t srv_ttl_seconds = 120;   // default 2 minutes
-    uint32_t txt_ttl_seconds = 4500;  // default 75 minutes
+    uint32_t ptr_ttl_seconds_ = 4500;  // default 75 minutes
+    uint32_t srv_ttl_seconds_ = 120;   // default 2 minutes
+    uint32_t txt_ttl_seconds_ = 4500;  // default 75 minutes
   };
 
   // Abstract base class for client-supplied subscriber.
@@ -140,7 +144,7 @@
   // calls to |ResolveHostName|, |SubscribeToService| and
   // |PublishServiceInstance|.
   void Start(fuchsia::netstack::NetstackPtr, const std::string& host_name,
-             fit::closure ready_callback);
+             bool perform_address_probe, fit::closure ready_callback);
 
   // Stops the transceiver.
   void Stop();
@@ -204,12 +208,26 @@
     }
   };
 
+  // Starts the address probe or transitions to ready state, depending on
+  // |perform_address_probe|. This method is called the first time a transceiver
+  // becomes ready.
+  void OnInterfacesStarted(const std::string& host_name,
+                           bool perform_address_probe);
+
   // Starts a probe for a conflicting host name. If a conflict is detected, a
   // new name is generated and this method is called again. If no conflict is
   // detected, |host_full_name_| gets set and the service is ready to start
   // other agents.
   void StartAddressProbe(const std::string& host_name);
 
+  // Sets |host_name_|, |host_full_name_| and |address_placeholder_|.
+  void RegisterHostName(const std::string& host_name);
+
+  // Starts agents and calls the ready callback. This method is called when
+  // at least one transceiver is ready and a unique host name has been
+  // established.
+  void OnReady();
+
   // Determines what host name to try next after a conflict is detected and
   // calls |StartAddressProbe| with that name.
   void OnHostNameConflict();
diff --git a/garnet/bin/mdns/service/mdns_fidl_util.cc b/garnet/bin/mdns/service/mdns_fidl_util.cc
index aaf28bf..37dce33 100644
--- a/garnet/bin/mdns/service/mdns_fidl_util.cc
+++ b/garnet/bin/mdns/service/mdns_fidl_util.cc
@@ -69,8 +69,7 @@
 
   fuchsia::net::Ipv4Address ipv4;
   FXL_DCHECK(ipv4.addr.size() == ip_address.byte_count());
-  std::memcpy(ipv4.addr.data(), ip_address.as_bytes(),
-              ipv4.addr.size());
+  std::memcpy(ipv4.addr.data(), ip_address.as_bytes(), ipv4.addr.size());
 
   fuchsia::netstack::SocketAddressPtr result =
       fuchsia::netstack::SocketAddress::New();
@@ -90,8 +89,7 @@
 
   fuchsia::net::Ipv6Address ipv6;
   FXL_DCHECK(ipv6.addr.size() == ip_address.byte_count());
-  std::memcpy(ipv6.addr.data(), ip_address.as_bytes(),
-              ipv6.addr.size());
+  std::memcpy(ipv6.addr.data(), ip_address.as_bytes(), ipv6.addr.size());
 
   fuchsia::netstack::SocketAddressPtr result =
       fuchsia::netstack::SocketAddress::New();
@@ -170,11 +168,11 @@
   auto publication = Mdns::Publication::Create(
       inet::IpPort::From_uint16_t(publication_ptr->port),
       fidl::To<std::vector<std::string>>(publication_ptr->text));
-  publication->ptr_ttl_seconds =
+  publication->ptr_ttl_seconds_ =
       zx::duration(publication_ptr->ptr_ttl).to_secs();
-  publication->srv_ttl_seconds =
+  publication->srv_ttl_seconds_ =
       zx::duration(publication_ptr->srv_ttl).to_secs();
-  publication->txt_ttl_seconds =
+  publication->txt_ttl_seconds_ =
       zx::duration(publication_ptr->txt_ttl).to_secs();
 
   return publication;
diff --git a/garnet/bin/mdns/service/mdns_service_impl.cc b/garnet/bin/mdns/service/mdns_service_impl.cc
index 7c47e8c..a3dae9e 100644
--- a/garnet/bin/mdns/service/mdns_service_impl.cc
+++ b/garnet/bin/mdns/service/mdns_service_impl.cc
@@ -19,8 +19,6 @@
 namespace mdns {
 namespace {
 
-static const std::string kPublishAs = "_fuchsia._udp.";
-static constexpr uint64_t kPublishPort = 5353;
 static const std::string kUnsetHostName = "fuchsia-unset-device-name";
 static constexpr zx::duration kReadyPollingInterval = zx::sec(1);
 
@@ -62,8 +60,16 @@
     return;
   }
 
+  config_.ReadConfigFiles(host_name);
+  if (!config_.valid()) {
+    FXL_LOG(FATAL) << "Invalid config file(s), terminating: "
+                   << config_.error();
+    return;
+  }
+
   mdns_.Start(component_context_->svc()->Connect<fuchsia::netstack::Netstack>(),
-              host_name, fit::bind_member(this, &MdnsServiceImpl::OnReady));
+              host_name, config_.perform_host_name_probe(),
+              fit::bind_member(this, &MdnsServiceImpl::OnReady));
 }
 
 void MdnsServiceImpl::OnBindRequest(
@@ -78,18 +84,18 @@
 void MdnsServiceImpl::OnReady() {
   ready_ = true;
 
-  // Publish this device as "_fuchsia._udp.".
-  // TODO(NET-2188): Make this a config item.
-  DEPRECATEDPublishServiceInstance(kPublishAs, mdns_.host_name(), kPublishPort,
-                                   fidl::VectorPtr<std::string>(), true,
-                                   [this](fuchsia::mdns::Result result) {
-                                     if (result != fuchsia::mdns::Result::OK) {
-                                       FXL_LOG(ERROR)
-                                           << "Failed to publish as "
-                                           << kPublishAs << ", result "
-                                           << static_cast<uint32_t>(result);
-                                     }
-                                   });
+  // Publish as indicated in config files.
+  for (auto& publication : config_.publications()) {
+    PublishServiceInstance(
+        publication.service_, publication.instance_,
+        publication.publication_->Clone(), true,
+        [this, service = publication.service_](fuchsia::mdns::Result result) {
+          if (result != fuchsia::mdns::Result::OK) {
+            FXL_LOG(ERROR) << "Failed to publish as " << service << ", result "
+                           << static_cast<uint32_t>(result);
+          }
+        });
+  }
 
   for (auto& request : pending_binding_requests_) {
     bindings_.AddBinding(this, std::move(request));
@@ -147,17 +153,27 @@
     return;
   }
 
-  auto publisher = std::make_unique<SimplePublisher>(
-      inet::IpPort::From_uint16_t(port), std::move(text), callback.share());
+  if (!PublishServiceInstance(
+          service_name, instance_name,
+          Mdns::Publication::Create(inet::IpPort::From_uint16_t(port),
+                                    std::move(text)),
+          perform_probe, callback.share())) {
+    callback(fuchsia::mdns::Result::ALREADY_PUBLISHED_LOCALLY);
+  }
+}
+
+bool MdnsServiceImpl::PublishServiceInstance(
+    std::string service_name, std::string instance_name,
+    std::unique_ptr<Mdns::Publication> publication, bool perform_probe,
+    PublishServiceInstanceCallback callback) {
+  auto publisher = std::make_unique<SimplePublisher>(std::move(publication),
+                                                     callback.share());
 
   if (!mdns_.PublishServiceInstance(service_name, instance_name, perform_probe,
                                     publisher.get())) {
-    callback(fuchsia::mdns::Result::ALREADY_PUBLISHED_LOCALLY);
-    return;
+    return false;
   }
 
-  MdnsNames::LocalInstanceFullName(instance_name, service_name);
-
   std::string instance_full_name =
       MdnsNames::LocalInstanceFullName(instance_name, service_name);
 
@@ -168,6 +184,8 @@
 
   publishers_by_instance_full_name_.emplace(instance_full_name,
                                             std::move(publisher));
+
+  return true;
 }
 
 void MdnsServiceImpl::DEPRECATEDUnpublishServiceInstance(
@@ -328,9 +346,9 @@
 // MdnsServiceImpl::SimplePublisher implementation
 
 MdnsServiceImpl::SimplePublisher::SimplePublisher(
-    inet::IpPort port, std::vector<std::string> text,
+    std::unique_ptr<Mdns::Publication> publication,
     PublishServiceInstanceCallback callback)
-    : port_(port), text_(std::move(text)), callback_(std::move(callback)) {}
+    : publication_(std::move(publication)), callback_(std::move(callback)) {}
 
 void MdnsServiceImpl::SimplePublisher::ReportSuccess(bool success) {
   callback_(success ? fuchsia::mdns::Result::OK
@@ -341,7 +359,7 @@
     bool query, const std::string& subtype,
     fit::function<void(std::unique_ptr<Mdns::Publication>)> callback) {
   FXL_DCHECK(subtype.empty() || MdnsNames::IsValidSubtypeName(subtype));
-  callback(Mdns::Publication::Create(port_, text_));
+  callback(publication_->Clone());
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/garnet/bin/mdns/service/mdns_service_impl.h b/garnet/bin/mdns/service/mdns_service_impl.h
index 6edcfbe..2a05a16 100644
--- a/garnet/bin/mdns/service/mdns_service_impl.h
+++ b/garnet/bin/mdns/service/mdns_service_impl.h
@@ -13,6 +13,7 @@
 
 #include <unordered_map>
 
+#include "garnet/bin/mdns/service/config.h"
 #include "garnet/bin/mdns/service/mdns.h"
 #include "src/lib/fxl/macros.h"
 
@@ -107,7 +108,7 @@
   // Publisher for PublishServiceInstance.
   class SimplePublisher : public Mdns::Publisher {
    public:
-    SimplePublisher(inet::IpPort port, std::vector<std::string> text,
+    SimplePublisher(std::unique_ptr<Mdns::Publication> publication,
                     PublishServiceInstanceCallback callback);
 
    private:
@@ -118,8 +119,7 @@
                         fit::function<void(std::unique_ptr<Mdns::Publication>)>
                             callback) override;
 
-    inet::IpPort port_;
-    std::vector<std::string> text_;
+    std::unique_ptr<Mdns::Publication> publication_;
     PublishServiceInstanceCallback callback_;
 
     // Disallow copy, assign and move.
@@ -162,7 +162,15 @@
   // Handles the ready callback from |mdns_|.
   void OnReady();
 
+  // Publishes a service instance using |SimplePublisher|.
+  bool PublishServiceInstance(std::string service_name,
+                              std::string instance_name,
+                              std::unique_ptr<Mdns::Publication> publication,
+                              bool perform_probe,
+                              PublishServiceInstanceCallback callback);
+
   sys::ComponentContext* component_context_;
+  Config config_;
   bool ready_ = false;
   std::vector<fidl::InterfaceRequest<fuchsia::mdns::Controller>>
       pending_binding_requests_;
diff --git a/garnet/bin/mdns/service/meta/mdns.cmx b/garnet/bin/mdns/service/meta/mdns.cmx
index 7d9c955..8183d87 100644
--- a/garnet/bin/mdns/service/meta/mdns.cmx
+++ b/garnet/bin/mdns/service/meta/mdns.cmx
@@ -3,6 +3,9 @@
         "binary": "bin/mdns"
     },
     "sandbox": {
+        "features": [
+            "config-data"
+        ],
         "services": [
             "fuchsia.net.SocketProvider",
             "fuchsia.netstack.Netstack"
diff --git a/garnet/bin/mdns/service/meta/mdns_tests.cmx b/garnet/bin/mdns/service/meta/mdns_tests.cmx
index 4b74b97..3cce1c0 100644
--- a/garnet/bin/mdns/service/meta/mdns_tests.cmx
+++ b/garnet/bin/mdns/service/meta/mdns_tests.cmx
@@ -1,5 +1,10 @@
 {
     "program": {
         "binary": "test/mdns_tests"
+    },
+    "sandbox": {
+        "features": [
+            "system-temp"
+        ]
     }
 }
diff --git a/garnet/bin/mdns/service/test/config_test.cc b/garnet/bin/mdns/service/test/config_test.cc
new file mode 100644
index 0000000..2524542
--- /dev/null
+++ b/garnet/bin/mdns/service/test/config_test.cc
@@ -0,0 +1,189 @@
+// Copyright 2019 The Fuchsia 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 "garnet/bin/mdns/service/config.h"
+
+#include "gtest/gtest.h"
+#include "src/lib/files/directory.h"
+#include "src/lib/files/file.h"
+#include "src/lib/files/path.h"
+
+namespace mdns {
+namespace test {
+
+const char kTestDir[] = "/tmp/mdns_config_test";
+const char kHostName[] = "test-host-name";
+
+bool WriteFile(const std::string& file, const std::string& to_write) {
+  return files::WriteFile(std::string(kTestDir) + std::string("/") + file,
+                          to_write.c_str(), to_write.length());
+}
+
+bool operator==(const std::unique_ptr<Mdns::Publication>& lhs,
+                const std::unique_ptr<Mdns::Publication>& rhs) {
+  return (lhs == nullptr && rhs == nullptr) ||
+         (lhs != nullptr && rhs != nullptr && lhs->port_ == rhs->port_ &&
+          lhs->text_ == rhs->text_ &&
+          lhs->ptr_ttl_seconds_ == rhs->ptr_ttl_seconds_ &&
+          lhs->srv_ttl_seconds_ == rhs->srv_ttl_seconds_ &&
+          lhs->txt_ttl_seconds_ == rhs->txt_ttl_seconds_);
+}
+
+bool operator==(const Config::Publication& lhs,
+                const Config::Publication& rhs) {
+  return lhs.service_ == rhs.service_ && lhs.instance_ == rhs.instance_ &&
+         lhs.publication_ == rhs.publication_ &&
+         lhs.perform_probe_ == rhs.perform_probe_;
+}
+
+// Tests behavior when there are no config files.
+TEST(ConfigTest, Empty) {
+  EXPECT_TRUE(files::CreateDirectory(kTestDir));
+
+  Config under_test;
+  under_test.ReadConfigFiles(kHostName, kTestDir);
+  EXPECT_TRUE(under_test.valid());
+  EXPECT_EQ("", under_test.error());
+  EXPECT_TRUE(under_test.perform_host_name_probe());
+  EXPECT_TRUE(under_test.publications().empty());
+
+  EXPECT_TRUE(files::DeletePath(kTestDir, true));
+}
+
+// Tests behavior when there is one valid config file.
+TEST(ConfigTest, OneValidFile) {
+  EXPECT_TRUE(files::CreateDirectory(kTestDir));
+  EXPECT_TRUE(WriteFile("valid", R"({
+    "perform_host_name_probe": false,
+    "publications" : [
+      {"service" : "_fuchsia._udp.", "port" : 5353, "perform_probe" : false,
+       "text": ["chins=2", "thumbs=10"]}
+    ]
+  })"));
+
+  Config under_test;
+  under_test.ReadConfigFiles(kHostName, kTestDir);
+  EXPECT_TRUE(under_test.valid());
+  EXPECT_EQ("", under_test.error());
+  EXPECT_FALSE(under_test.perform_host_name_probe());
+  EXPECT_EQ(1u, under_test.publications().size());
+  EXPECT_TRUE(
+      (Config::Publication{
+          .service_ = "_fuchsia._udp.",
+          .instance_ = kHostName,
+          .publication_ = std::make_unique<Mdns::Publication>(
+              Mdns::Publication{.port_ = inet::IpPort::From_uint16_t(5353),
+                                .text_ = {"chins=2", "thumbs=10"}}),
+          .perform_probe_ = false}) == under_test.publications()[0]);
+
+  EXPECT_TRUE(files::DeletePath(kTestDir, true));
+}
+
+// Tests behavior when there is one valid config file.
+TEST(ConfigTest, OneInvalidFile) {
+  EXPECT_TRUE(files::CreateDirectory(kTestDir));
+  EXPECT_TRUE(WriteFile("invalid", R"({
+    "dwarves": 7
+  })"));
+
+  Config under_test;
+  under_test.ReadConfigFiles(kHostName, kTestDir);
+  EXPECT_FALSE(under_test.valid());
+  EXPECT_NE("", under_test.error());
+
+  EXPECT_TRUE(files::DeletePath(kTestDir, true));
+}
+
+// Tests behavior when there is one valid and one invalid config file.
+TEST(ConfigTest, OneValidOneInvalidFile) {
+  EXPECT_TRUE(files::CreateDirectory(kTestDir));
+  EXPECT_TRUE(WriteFile("valid", R"({
+    "perform_host_name_probe": false,
+    "publications" : [
+      {"service" : "_fuchsia._udp.", "port" : 5353, "perform_probe" : false}
+    ]
+  })"));
+  EXPECT_TRUE(WriteFile("invalid", R"({
+    "dwarves": 7
+  })"));
+
+  Config under_test;
+  under_test.ReadConfigFiles(kHostName, kTestDir);
+  EXPECT_FALSE(under_test.valid());
+  EXPECT_NE("", under_test.error());
+
+  EXPECT_TRUE(files::DeletePath(kTestDir, true));
+}
+
+// Tests behavior when there are two valid config files.
+TEST(ConfigTest, TwoValidFiles) {
+  EXPECT_TRUE(files::CreateDirectory(kTestDir));
+  EXPECT_TRUE(WriteFile("valid1", R"({
+    "perform_host_name_probe": false,
+    "publications" : [
+      {"service" : "_fuchsia._udp.", "port" : 5353, "perform_probe" : false}
+    ]
+  })"));
+  EXPECT_TRUE(WriteFile("valid2", R"({
+    "publications" : [
+      {"service" : "_footstool._udp.", "instance": "puffy", "port" : 1234}
+    ]
+  })"));
+
+  Config under_test;
+  under_test.ReadConfigFiles(kHostName, kTestDir);
+  EXPECT_TRUE(under_test.valid());
+  EXPECT_EQ("", under_test.error());
+  EXPECT_FALSE(under_test.perform_host_name_probe());
+  EXPECT_EQ(2u, under_test.publications().size());
+
+  size_t fuchsia_index =
+      (under_test.publications()[0].service_ == "_fuchsia._udp.") ? 0 : 1;
+
+  EXPECT_TRUE(
+      (Config::Publication{
+          .service_ = "_fuchsia._udp.",
+          .instance_ = kHostName,
+          .publication_ = std::make_unique<Mdns::Publication>(
+              Mdns::Publication{.port_ = inet::IpPort::From_uint16_t(5353)}),
+          .perform_probe_ = false}) ==
+      under_test.publications()[fuchsia_index]);
+  EXPECT_TRUE(
+      (Config::Publication{
+          .service_ = "_footstool._udp.",
+          .instance_ = "puffy",
+          .publication_ = std::make_unique<Mdns::Publication>(
+              Mdns::Publication{.port_ = inet::IpPort::From_uint16_t(1234)}),
+          .perform_probe_ = true}) ==
+      under_test.publications()[1 - fuchsia_index]);
+
+  EXPECT_TRUE(files::DeletePath(kTestDir, true));
+}
+
+// Tests behavior when there are two valid config files that conflict.
+TEST(ConfigTest, TwoConflictingValidFiles) {
+  EXPECT_TRUE(files::CreateDirectory(kTestDir));
+  EXPECT_TRUE(WriteFile("valid1", R"({
+    "perform_host_name_probe": false,
+    "publications" : [
+      {"service" : "_fuchsia._udp.", "port" : 5353, "perform_probe" : false}
+    ]
+  })"));
+  EXPECT_TRUE(WriteFile("valid2", R"({
+    "perform_host_name_probe": true,
+    "publications" : [
+      {"service" : "_footstool._udp.", "instance": "puffy", "port" : 1234}
+    ]
+  })"));
+
+  Config under_test;
+  under_test.ReadConfigFiles(kHostName, kTestDir);
+  EXPECT_FALSE(under_test.valid());
+  EXPECT_NE("", under_test.error());
+
+  EXPECT_TRUE(files::DeletePath(kTestDir, true));
+}
+
+}  // namespace test
+}  // namespace mdns
diff --git a/garnet/packages/config/BUILD.gn b/garnet/packages/config/BUILD.gn
index b439008..18606f0 100644
--- a/garnet/packages/config/BUILD.gn
+++ b/garnet/packages/config/BUILD.gn
@@ -47,6 +47,7 @@
   testonly = true
   public_deps = [
     "//garnet/bin/mdns/service:mdns_config",
+    "//garnet/bin/mdns/service:mdns_fuchsia_udp_config",
     "//garnet/packages/config:network",
   ]
 }