[botanist] Expose IPv4 address to subprocess

This change computes the IPv4 address of a fuchsia node via mDNS and
includes it in the environment of `botanist run`'s subprocess.

Test: was able to successfully resolve the IP address of
my NUC locally

Change-Id: I38a7171e5618d805c17556e5a9bae0fa9691970d
diff --git a/botanist/ip.go b/botanist/ip.go
new file mode 100644
index 0000000..dbd3c4b
--- /dev/null
+++ b/botanist/ip.go
@@ -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.
+
+package botanist
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"time"
+
+	"fuchsia.googlesource.com/tools/logger"
+	"fuchsia.googlesource.com/tools/mdns"
+	"fuchsia.googlesource.com/tools/retry"
+)
+
+// Interval at which ResolveIP will wait for a response to a question packet.
+const mDNSTimeout time.Duration = 2 * time.Second
+
+func getLocalDomain(nodename string) string {
+	return nodename + ".local"
+}
+
+// ResolveIP returns the IPv4 address of a fuchsia node via mDNS.
+//
+// TODO(joshuaseaton): Refactor dev_finder to share 'resolve' logic with botanist.
+func ResolveIPv4(ctx context.Context, nodename string, timeout time.Duration) (net.IP, error) {
+	var m mdns.MDNS
+	out := make(chan net.IP)
+	domain := getLocalDomain(nodename)
+	m.AddHandler(func(iface net.Interface, addr net.Addr, packet mdns.Packet) {
+		for _, a := range packet.Answers {
+			if a.Class == mdns.IN && a.Type == mdns.A && a.Domain == domain {
+				out <- net.IP(a.Data)
+				return
+			}
+		}
+	})
+	m.AddWarningHandler(func(addr net.Addr, err error) {
+		logger.Infof(ctx, "from: %v; warn: %v", addr, err)
+	})
+	errs := make(chan error)
+	m.AddErrorHandler(func(err error) {
+		errs <- err
+	})
+
+	ctx, cancel := context.WithTimeout(ctx, timeout)
+	defer cancel()
+
+	if err := m.Start(ctx, mdns.DefaultPort); err != nil {
+		return nil, fmt.Errorf("could not start mDNS client: %v", err)
+	}
+
+	// Send question packets to the mDNS server at intervals of mDNSTimeout for a total of
+	// |timeout|; retry, as it takes time for the netstack and server to be brought up.
+	var ip net.IP
+	var err error
+	err = retry.Retry(ctx, &retry.ZeroBackoff{}, func() error {
+		m.Send(mdns.QuestionPacket(domain))
+		ctx, cancel := context.WithTimeout(context.Background(), mDNSTimeout)
+		defer cancel()
+
+		select {
+		case <-ctx.Done():
+			return fmt.Errorf("timeout")
+		case err = <-errs:
+			return err
+		case ip = <-out:
+			return nil
+		}
+	}, nil)
+
+	return ip, err
+}
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index 81ce2aa..de8f6bd 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -23,6 +23,8 @@
 	"golang.org/x/crypto/ssh"
 )
 
+const netstackTimeout time.Duration = 1 * time.Minute
+
 // RunCommand is a Command implementation for booting a device and running a
 // given command locally.
 type RunCommand struct {
@@ -131,10 +133,16 @@
 		return err
 	}
 
+	ip, err := botanist.ResolveIPv4(ctx, nodename, netstackTimeout)
+	if err != nil {
+		return fmt.Errorf("could not resolve IP address: %v", err)
+	}
+
 	env := append(
 		os.Environ(),
 		fmt.Sprintf("FUCHSIA_NODENAME=%s", nodename),
-		fmt.Sprintf("FUCHSIA_SSH_KEY=%s", string(privKeys[0])),
+		fmt.Sprintf("FUCHSIA_IPV4_ADDR=%s", ip),
+		fmt.Sprintf("FUCHSIA_SSH_KEY=%s", privKeys[0]),
 	)
 
 	// Run command.
diff --git a/mdns/mdns.go b/mdns/mdns.go
index 7233b9a..61baced 100644
--- a/mdns/mdns.go
+++ b/mdns/mdns.go
@@ -19,6 +19,9 @@
 	"golang.org/x/net/ipv4"
 )
 
+// DefaultPort is the mDNS port required of the spec, though this library is port-agnostic.
+const DefaultPort int = 5353
+
 type Header struct {
 	ID      uint16
 	Flags   uint16