blob: f56c6e5ac9688844364512fbdd0daef19a13f198 [file] [log] [blame]
// 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 targets
import (
"context"
"errors"
"fmt"
"log"
"net"
"time"
"github.com/kr/pretty"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"go.fuchsia.dev/fuchsia/tools/net/mdns"
)
// Interval at which resolveIP will send a question packet as long as it doesn't
// receive a response. If it receives a response it may send packets more
// quickly.
const mDNSQuestionInterval = 2 * time.Second
func getLocalDomain(nodename string) string {
return nodename + ".local"
}
// resolveIP returns the IPv4 and IPv6 addresses of a fuchsia node via mDNS.
//
// It makes a best effort at returning *both* the IPv4 and IPv6 addresses, but if
// both interfaces do not come up within a reasonable amount of time, it returns
// only the first one that it finds (and no error).
func ResolveIP(ctx context.Context, nodename string) (net.IP, net.IPAddr, error) {
// Keep track of the start time to log the duration after which a timeout
// occurred.
startTime := time.Now()
m := mdns.NewMDNS()
defer m.Close()
m.EnableIPv4()
m.EnableIPv6()
out := make(chan net.IPAddr, 1)
domain := getLocalDomain(nodename)
m.AddHandler(func(addr net.Addr, packet mdns.Packet) {
logger.Debugf(ctx, "mdns packet from %s: %# v", addr, pretty.Formatter(packet))
var zone string
switch addr := addr.(type) {
case *net.IPAddr:
zone = addr.Zone
case *net.UDPAddr:
zone = addr.Zone
}
for _, records := range [][]mdns.Record{
packet.Answers,
packet.Additional,
} {
for _, record := range records {
if record.Class == mdns.IN && record.Domain == domain {
switch record.Type {
case mdns.A, mdns.AAAA:
out <- net.IPAddr{
IP: net.IP(record.Data),
Zone: zone,
}
return
}
}
}
}
})
m.AddWarningHandler(func(addr net.Addr, err error) {
logger.Infof(ctx, "from: %s; warn: %s", addr, err)
})
errs := make(chan error, 1)
m.AddErrorHandler(func(err error) {
errs <- err
})
ctx, cancel := context.WithCancel(ctx)
defer cancel() // Clean up any goroutines launched by the mDNS client.
if err := m.Start(ctx, mdns.DefaultPort); err != nil {
return nil, net.IPAddr{}, fmt.Errorf("could not start mDNS client: %w", err)
}
var ipv4Addr net.IP
var ipv6Addr net.IPAddr
t := time.NewTicker(mDNSQuestionInterval)
defer t.Stop()
for {
if err := m.Send(ctx, mdns.QuestionPacket(domain)); err != nil {
return nil, net.IPAddr{}, fmt.Errorf("could not send mDNS question: %w", err)
}
for {
select {
case <-ctx.Done():
// A timeout/cancelation is only considered an error if we
// failed to resolve either address in the allotted time.
// If we've already resolved at least one address, then we can
// proceed since it's not the end of the world if we haven't
// resolved both.
if ipv4Addr == nil && ipv6Addr.IP == nil {
err := ctx.Err()
if errors.Is(err, context.DeadlineExceeded) {
err = fmt.Errorf("%w after %s", err, time.Since(startTime))
}
return nil, net.IPAddr{}, err
}
return ipv4Addr, ipv6Addr, nil
case err := <-errs:
return ipv4Addr, ipv6Addr, err
case addr := <-out:
if addr.IP.To4() != nil {
ipv4Addr = addr.IP
} else if addr.IP.To16() != nil {
ipv6Addr = addr
} else {
log.Panicf("IP address %q is neither IPv4 nor IPv6", addr)
}
// Got both addresses, so we're done.
if ipv4Addr != nil && ipv6Addr.IP != nil {
return ipv4Addr, ipv6Addr, nil
}
// `select` again to see if we can get the other IP address
// without resending the question.
continue
case <-t.C:
// If we already have one IP address and one more attempt didn't
// get us the second one, we'll assume that it's not going to
// come up (or at least not any time soon) and exit early with
// just the first IP we resolved.
if ipv4Addr != nil || ipv6Addr.IP != nil {
return ipv4Addr, ipv6Addr, nil
}
// Fallthrough to resend the question.
}
break
}
}
}