[mdns] Add bin/dev_finder command
This adds device discovery tools to the SDK that use mDNS.
Commands include:
--resolve Looks up a device based on its hostname w/ ".local" on the
end.
--list Looks up all devices that are running the _fuchsia._udp.local
service and lists then.
TEST: Manually with a Fuchsia device using edgerouter
DX-655 #comment
Change-Id: Ibc9411d3dff51e85522a9de6ea9a481defcd5a81
diff --git a/bin/dev_finder/BUILD.gn b/bin/dev_finder/BUILD.gn
new file mode 100644
index 0000000..9ca25f0
--- /dev/null
+++ b/bin/dev_finder/BUILD.gn
@@ -0,0 +1,26 @@
+# Copyright 2018 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.
+
+import("//build/go/go_library.gni")
+import("//build/go/go_binary.gni")
+
+go_library("dev_finder_lib") {
+ name = "dev_finder"
+
+ deps = [
+ "//tools/mdns:mdns_lib",
+ "//garnet/public/go/third_party:github.com/google/subcommands",
+ ]
+}
+
+go_binary("dev_finder") {
+ output_name = "dev_finder"
+ gopackage = "dev_finder"
+
+ sdk_category = "partner"
+
+ deps = [
+ ":dev_finder_lib",
+ ]
+}
diff --git a/bin/dev_finder/common.go b/bin/dev_finder/common.go
new file mode 100644
index 0000000..cec22c8
--- /dev/null
+++ b/bin/dev_finder/common.go
@@ -0,0 +1,133 @@
+// Copyright 2018 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.
+package main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "net"
+ "time"
+
+ "mdns_lib"
+)
+
+type mDNSResponse struct {
+ rxIface net.Interface
+ devAddr net.Addr
+ rxPacket mdns.Packet
+}
+
+func (m *mDNSResponse) getReceiveIP() (net.IP, error) {
+ if unicastAddrs, err := m.rxIface.Addrs(); err != nil {
+ return nil, err
+ } else {
+ for _, addr := range unicastAddrs {
+ var ip net.IP
+ switch v := addr.(type) {
+ case *net.IPNet:
+ ip = v.IP
+ case *net.IPAddr:
+ ip = v.IP
+ }
+ if ip == nil || ip.To4() == nil {
+ continue
+ }
+ return ip, nil
+ }
+ }
+ return nil, fmt.Errorf("no IPv4 unicast addresses found on iface %v", m.rxIface)
+}
+
+type mDNSHandler func(mDNSResponse, bool, chan<- *fuchsiaDevice, chan<- error)
+
+// Contains common command information for embedding in other dev_finder commands.
+type devFinderCmd struct {
+ // The mDNS port to connect to.
+ mdnsPort int
+ // The timeout in ms to either give up or to exit the program after finding at least one
+ // device.
+ timeout int
+ // Determines whether to return the address of the address of the interface that
+ // established a connection to the Fuchsia device (rather than the address of the
+ // Fuchsia device on its own).
+ localResolve bool
+ // The limit of devices to discover. If this number of devices has been discovered before
+ // the timeout has been reached the program will exit successfully.
+ deviceLimit int
+
+ mdnsHandler mDNSHandler
+}
+
+type fuchsiaDevice struct {
+ addr net.IP
+ domain string
+}
+
+func (cmd *devFinderCmd) SetCommonFlags(f *flag.FlagSet) {
+ f.IntVar(&cmd.mdnsPort, "port", 5353, "The port your mDNS servers operate on.")
+ f.IntVar(&cmd.timeout, "timeout", 2000, "The number of milliseconds before declaring a timeout.")
+ f.BoolVar(&cmd.localResolve, "local", false, "Returns the address of the interface to the host when doing service lookup/domain resolution.")
+ f.IntVar(&cmd.deviceLimit, "device_limit", 0, "Exits before the timeout at this many devices per resolution (zero means no limit).")
+}
+
+// Extracts the IP from its argument, returning an error if the type is unsupported.
+func addrToIP(addr net.Addr) (net.IP, error) {
+ switch v := addr.(type) {
+ case *net.IPNet:
+ return v.IP, nil
+ case *net.IPAddr:
+ return v.IP, nil
+ case *net.UDPAddr:
+ return v.IP, nil
+ }
+ return nil, errors.New("unsupported address type")
+}
+
+func (cmd *devFinderCmd) sendMDNSPacket(ctx context.Context, packet mdns.Packet) ([]*fuchsiaDevice, error) {
+ if cmd.mdnsHandler == nil {
+ return nil, fmt.Errorf("packet handler is nil")
+ }
+ if cmd.timeout <= 0 {
+ return nil, fmt.Errorf("invalid timeout value: %v", cmd.timeout)
+ }
+
+ var m mdns.MDNS
+ errChan := make(chan error)
+ devChan := make(chan *fuchsiaDevice)
+ m.AddHandler(func(recv net.Interface, addr net.Addr, rxPacket mdns.Packet) {
+ response := mDNSResponse{recv, addr, rxPacket}
+ cmd.mdnsHandler(response, cmd.localResolve, devChan, errChan)
+ })
+ m.AddErrorHandler(func(err error) {
+ errChan <- err
+ })
+ m.AddWarningHandler(func(addr net.Addr, err error) {
+ fmt.Printf("from: %v warn: %v\n", addr, err)
+ })
+ ctx, cancel := context.WithTimeout(ctx, time.Duration(cmd.timeout)*time.Millisecond)
+ defer cancel()
+ if err := m.Start(ctx, cmd.mdnsPort); err != nil {
+ errChan <- fmt.Errorf("starting mdns: %v", err)
+ }
+ m.Send(packet)
+ devices := make([]*fuchsiaDevice, 0)
+ for {
+ select {
+ case <-ctx.Done():
+ if len(devices) == 0 {
+ return nil, fmt.Errorf("timeout")
+ }
+ return devices, nil
+ case err := <-errChan:
+ return nil, err
+ case device := <-devChan:
+ devices = append(devices, device)
+ if cmd.deviceLimit != 0 && len(devices) == cmd.deviceLimit {
+ return devices, nil
+ }
+ }
+ }
+}
diff --git a/bin/dev_finder/list.go b/bin/dev_finder/list.go
new file mode 100644
index 0000000..6bd8249
--- /dev/null
+++ b/bin/dev_finder/list.go
@@ -0,0 +1,116 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/google/subcommands"
+ "mdns_lib"
+)
+
+const (
+ fuchsiaService = "_fuchsia._udp.local"
+)
+
+type listCmd struct {
+ devFinderCmd
+
+ // Determines whether or not to print the full device info.
+ fullInfo bool
+ // Filters domains that match this string when listing devices.
+ domainFilter string
+}
+
+func (*listCmd) Name() string {
+ return "list"
+}
+
+func (*listCmd) Usage() string {
+ return "list [flags...]\n\nflags:\n"
+}
+
+func (*listCmd) Synopsis() string {
+ return "lists all Fuchsia devices on the network"
+}
+
+func (cmd *listCmd) SetFlags(f *flag.FlagSet) {
+ cmd.SetCommonFlags(f)
+ f.StringVar(&cmd.domainFilter, "domain_filter", "", "When using the \"list\" command, returns only devices that match this domain name.")
+ f.BoolVar(&cmd.fullInfo, "full", false, "Print device address and domain")
+}
+
+func listMDNSHandler(resp mDNSResponse, localResolve bool, devChan chan<- *fuchsiaDevice, errChan chan<- error) {
+ for _, a := range resp.rxPacket.Answers {
+ if a.Class == mdns.IN && a.Type == mdns.PTR {
+ // This is a bit convoluted: the domain param is being used
+ // as a "service", and the Data field actually contains the
+ // domain of the device.
+ fuchsiaDomain := strings.Trim(string(a.Data[:]), "\n\r\f ")
+ if localResolve {
+ recvIP, err := resp.getReceiveIP()
+ if err != nil {
+ errChan <- err
+ return
+ }
+ devChan <- &fuchsiaDevice{recvIP, fuchsiaDomain}
+ continue
+ }
+ if ip, err := addrToIP(resp.devAddr); err != nil {
+ errChan <- fmt.Errorf("could not find addr for %v: %v", resp.devAddr, err)
+ } else {
+ devChan <- &fuchsiaDevice{
+ addr: ip,
+ domain: fuchsiaDomain,
+ }
+ }
+ }
+ }
+}
+
+func (cmd *listCmd) execute(ctx context.Context) error {
+ listPacket := mdns.Packet{
+ Header: mdns.Header{QDCount: 1},
+ Questions: []mdns.Question{
+ mdns.Question{
+ Domain: fuchsiaService,
+ Type: mdns.PTR,
+ Class: mdns.IN,
+ Unicast: false,
+ },
+ },
+ }
+ devices, err := cmd.sendMDNSPacket(ctx, listPacket)
+ if err != nil {
+ return fmt.Errorf("sending/receiving mdns packets: %v", err)
+ }
+ filteredDevices := make([]*fuchsiaDevice, 0)
+ for _, device := range devices {
+ if strings.Contains(device.domain, cmd.domainFilter) {
+ filteredDevices = append(filteredDevices, device)
+ }
+ }
+ if len(filteredDevices) == 0 {
+ return fmt.Errorf("no devices with domain matching '%v'", cmd.domainFilter)
+ }
+
+ for _, device := range filteredDevices {
+ if cmd.fullInfo {
+ fmt.Printf("%v %v\n", device.addr, device.domain)
+ } else {
+ fmt.Printf("%v\n", device.addr)
+ }
+ }
+ return nil
+}
+
+func (cmd *listCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+ cmd.mdnsHandler = listMDNSHandler
+ if err := cmd.execute(ctx); err != nil {
+ log.Print(err)
+ return subcommands.ExitFailure
+ }
+ return subcommands.ExitSuccess
+}
diff --git a/bin/dev_finder/main.go b/bin/dev_finder/main.go
new file mode 100644
index 0000000..72525e3
--- /dev/null
+++ b/bin/dev_finder/main.go
@@ -0,0 +1,25 @@
+// Copyright 2018 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.
+package main
+
+// Uses mDNS for Fuchsia device discovery.
+
+import (
+ "context"
+ "flag"
+ "os"
+
+ "github.com/google/subcommands"
+)
+
+func main() {
+ subcommands.Register(subcommands.HelpCommand(), "")
+ subcommands.Register(subcommands.CommandsCommand(), "")
+ subcommands.Register(subcommands.FlagsCommand(), "")
+ subcommands.Register(&listCmd{}, "")
+ subcommands.Register(&resolveCmd{}, "")
+
+ flag.Parse()
+ os.Exit(int(subcommands.Execute(context.Background())))
+}
diff --git a/bin/dev_finder/resolve.go b/bin/dev_finder/resolve.go
new file mode 100644
index 0000000..ee92350
--- /dev/null
+++ b/bin/dev_finder/resolve.go
@@ -0,0 +1,93 @@
+// Copyright 2018 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.
+package main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "net"
+
+ "github.com/google/subcommands"
+ "mdns_lib"
+)
+
+const (
+ ipv4AddrLength = 4
+)
+
+type resolveCmd struct {
+ devFinderCmd
+}
+
+func (*resolveCmd) Name() string {
+ return "resolve"
+}
+
+func (*resolveCmd) Usage() string {
+ return "resolve [flags...] [domains...]\n\nflags:\n"
+}
+
+func (*resolveCmd) Synopsis() string {
+ return "Attempts to resolve all passed Fuchsia domain names on the network"
+}
+
+func (cmd *resolveCmd) SetFlags(f *flag.FlagSet) {
+ cmd.SetCommonFlags(f)
+}
+
+func resolveMDNSHandler(resp mDNSResponse, localResolve bool, devChan chan<- *fuchsiaDevice, errChan chan<- error) {
+ for _, a := range resp.rxPacket.Answers {
+ if a.Class == mdns.IN && a.Type == mdns.A &&
+ len(a.Data) == ipv4AddrLength {
+ if localResolve {
+ recvIP, err := resp.getReceiveIP()
+ if err != nil {
+ errChan <- err
+ return
+ }
+ devChan <- &fuchsiaDevice{recvIP, a.Domain}
+ continue
+ }
+ devChan <- &fuchsiaDevice{net.IP(a.Data), a.Domain}
+ }
+ }
+}
+
+func (cmd *resolveCmd) execute(ctx context.Context, domains ...string) error {
+ if len(domains) == 0 {
+ return errors.New("no domains supplied")
+ }
+ for _, domain := range domains {
+ devices, err := cmd.sendMDNSPacket(ctx, mdns.QuestionPacket(domain))
+ if err != nil {
+ return fmt.Errorf("sending/receiving mdns packets during resolve of domain '%s': %v", domain, err)
+ }
+ filteredDevices := make([]*fuchsiaDevice, 0)
+ for _, device := range devices {
+ if device.domain == domain {
+ filteredDevices = append(filteredDevices, device)
+ }
+ }
+ if len(filteredDevices) == 0 {
+ return fmt.Errorf("no devices with domain %v", domain)
+ }
+
+ for _, device := range filteredDevices {
+ fmt.Printf("%v\n", device.addr)
+ }
+ }
+ return nil
+}
+
+func (cmd *resolveCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+ cmd.mdnsHandler = resolveMDNSHandler
+ if err := cmd.execute(ctx, f.Args()...); err != nil {
+ log.Print(err)
+ return subcommands.ExitFailure
+ }
+ return subcommands.ExitSuccess
+}
diff --git a/public/sdk/BUILD.gn b/public/sdk/BUILD.gn
index e9cbb85..1edfb2b 100644
--- a/public/sdk/BUILD.gn
+++ b/public/sdk/BUILD.gn
@@ -104,6 +104,7 @@
"//build/images:images_sdk",
"//garnet/bin/far:bin_sdk($host_toolchain)",
"//garnet/bin/zxdb:zxdb_sdk($host_toolchain)",
+ "//garnet/bin/dev_finder:dev_finder_sdk($host_toolchain)",
"//garnet/go/src/fidl:fidlgen_sdk($host_toolchain)",
"//garnet/go/src/pm:pm_bin_sdk($host_toolchain)",
"//garnet/lib/vulkan:vulkan_layers",