blob: 58261407fb35dabe59e6ad4a512aa96a74c70734 [file] [log] [blame]
// 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"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"runtime"
"sort"
"strconv"
"strings"
"time"
"go.fuchsia.dev/fuchsia/tools/net/mdns"
"go.fuchsia.dev/fuchsia/tools/net/netboot"
)
type mDNSResponse struct {
devAddr net.Addr
rxPacket mdns.Packet
}
type mDNSHandler func(*devFinderCmd, mDNSResponse, chan<- *fuchsiaDevice)
type mdnsInterface interface {
AddHandler(f func(net.Addr, mdns.Packet))
AddWarningHandler(f func(net.Addr, error))
AddErrorHandler(f func(error))
SendTo(context.Context, mdns.Packet, *net.UDPAddr) error
Send(context.Context, mdns.Packet) error
Start(ctx context.Context, port int) error
}
type newMDNSFunc func(address string) mdnsInterface
type netbootClientInterface interface {
StartDiscover(context.Context, chan<- *netboot.Target, string) (func() error, error)
}
type newNetbootFunc func(timeout time.Duration) netbootClientInterface
// Contains common command information for embedding in other dev_finder commands.
type devFinderCmd struct {
// Outputs in JSON format if true.
json bool
// The mDNS addresses to connect to.
mdnsAddrs string
// The mDNS ports to connect to.
mdnsPorts string
// The timeout to either give up or to exit the program after finding at
// least one device.
timeout time.Duration
// 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
// The TTL for multicast messages. This is primarily for debugging and testing. Setting
// this to zero restricts all packets to the host machine. Setting this to a negative
// number is ignored (continues default behavior). Setting this to greater than
// 255 is an error.
ttl int
// If set to true, uses netboot protocol.
netboot bool
// If set to true, uses mdns protocol.
mdns bool
ipv4 bool
ipv6 bool
// Used to visit flags after parsing, necessary for complex if-set logic.
flagSet *flag.FlagSet
finders []deviceFinder
// Only for testing.
newMDNSFunc newMDNSFunc
newNetbootFunc newNetbootFunc
output io.Writer
}
type fuchsiaDevice struct {
addr net.IP
// domain is the nodename of the fuchsia target.
domain string
// zone is the IPv6 zone to connect to the target.
zone string
err error
}
func (f *fuchsiaDevice) addrString() string {
addr := net.IPAddr{IP: f.addr, Zone: f.zone}
return addr.String()
}
// outbound returns a copy of the device to containing the preferred outbound
// connection, as is requested when using the `--local` flag.
func (f *fuchsiaDevice) outbound() (*fuchsiaDevice, error) {
var udpProto string
if f.addr.To4() != nil {
udpProto = "udp4"
} else {
udpProto = "udp6"
}
// This is just dialing a nonsense port. No packets are being sent.
tmpConn, err := net.DialUDP(udpProto, nil, &net.UDPAddr{IP: f.addr, Zone: f.zone, Port: 22})
if err != nil {
return nil, fmt.Errorf("getting output ip of %s: %w", f.domain, err)
}
defer tmpConn.Close()
localAddr := tmpConn.LocalAddr().(*net.UDPAddr)
return &fuchsiaDevice{
domain: f.domain,
addr: localAddr.IP,
zone: localAddr.Zone,
}, nil
}
const (
mdnsFlag = "mdns"
netbootFlag = "netboot"
)
func (cmd *devFinderCmd) SetCommonFlags(f *flag.FlagSet) {
cmd.flagSet = f
f.BoolVar(&cmd.json, "json", false, "Outputs in JSON format.")
f.StringVar(&cmd.mdnsAddrs, "addr", "224.0.0.251,ff02::fb", "[linux only] Comma separated list of addresses to issue mDNS queries to.")
f.StringVar(&cmd.mdnsPorts, "port", "5353", "[linux only] Comma separated list of ports to issue mDNS queries to.")
f.DurationVar(&cmd.timeout, "timeout", 2*time.Second, "The duration 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).")
f.IntVar(&cmd.ttl, "ttl", -1, "[linux only] Sets the TTL for outgoing mcast messages. Primarily for debugging and testing. Setting this to zero limits messages to the localhost.")
f.BoolVar(&cmd.mdns, mdnsFlag, true, "Determines whether to use mDNS protocol")
f.BoolVar(&cmd.netboot, netbootFlag, false, "Determines whether to use netboot protocol")
f.BoolVar(&cmd.ipv6, "ipv6", true, "Set whether to query using IPv6. Disabling IPv6 will also disable netboot.")
f.BoolVar(&cmd.ipv4, "ipv4", true, "Set whether to query using IPv4")
}
func (cmd *devFinderCmd) Output() io.Writer {
if cmd.output == nil {
return os.Stdout
}
return cmd.output
}
func (cmd *devFinderCmd) newMDNS(address string) mdnsInterface {
if cmd.newMDNSFunc != nil {
return cmd.newMDNSFunc(address)
}
m := mdns.NewMDNS()
if !cmd.ipv4 && !cmd.ipv6 {
log.Fatalf("either --ipv4 or --ipv6 must be set to true")
}
// Ultimately there can be only one MDNS per address, so either it is
// enabled somewhere in here or nil is returned.
ip := net.ParseIP(address)
if ip.To4() == nil {
if cmd.ipv6 {
m.EnableIPv6()
} else {
return nil
}
} else {
if cmd.ipv4 {
m.EnableIPv4()
} else {
return nil
}
}
m.SetAddress(address)
if err := m.SetMCastTTL(cmd.ttl); err != nil {
log.Fatalf("unable to set mcast TTL: %s", err)
}
return m
}
func (cmd *devFinderCmd) newNetbootClient(timeout time.Duration) netbootClientInterface {
if cmd.newNetbootFunc != nil {
return cmd.newNetbootFunc(timeout)
}
return netboot.NewClient(timeout)
}
func sortDeviceMap(deviceMap map[string]*fuchsiaDevice) []*fuchsiaDevice {
keys := make([]string, 0)
for k := range deviceMap {
keys = append(keys, k)
}
sort.Strings(keys)
res := make([]*fuchsiaDevice, 0)
for _, k := range keys {
if v := deviceMap[k]; v != nil {
res = append(res, v)
}
}
return res
}
func startMDNSHandlers(ctx context.Context, cmd *devFinderCmd, packet mdns.Packet, addrs []string, ports []int, f chan *fuchsiaDevice) error {
for _, addr := range addrs {
for _, p := range ports {
m := cmd.newMDNS(addr)
m.AddHandler(func(addr net.Addr, rxPacket mdns.Packet) {
response := mDNSResponse{devAddr: addr, rxPacket: rxPacket}
listMDNSHandler(cmd, response, f)
})
m.AddErrorHandler(func(err error) {
f <- &fuchsiaDevice{err: err}
})
m.AddWarningHandler(func(addr net.Addr, err error) {
log.Printf("from: %s warn: %s\n", addr, err)
})
if err := m.Start(ctx, p); err != nil {
return fmt.Errorf("starting mdns: %w", err)
}
if err := m.Send(ctx, packet); err != nil {
return fmt.Errorf("sending mdns: %w", err)
}
}
}
return nil
}
func (cmd *devFinderCmd) sendMDNSPacket(ctx context.Context, packet mdns.Packet, f chan *fuchsiaDevice) error {
if cmd.timeout <= 0 {
return fmt.Errorf("invalid timeout value: %s", cmd.timeout)
}
cmdAddrs := strings.Split(cmd.mdnsAddrs, ",")
var ports []int
for _, s := range strings.Split(cmd.mdnsPorts, ",") {
p, err := strconv.ParseUint(s, 10, 16)
if err != nil {
return fmt.Errorf("could not parse port number %s: %w\n", s, err)
}
ports = append(ports, int(p))
}
if len(ports) == 0 {
return fmt.Errorf("no viable ports from %q", cmd.mdnsAddrs)
}
var addrs []string
for _, addr := range cmdAddrs {
ip := net.ParseIP(addr)
if ip == nil {
return fmt.Errorf("%q not a valid IP", addr)
}
if cmd.shouldIgnoreIP(ip) {
continue
}
addrs = append(addrs, addr)
}
if len(addrs) == 0 {
return fmt.Errorf("no viable addresses")
}
return startMDNSHandlers(ctx, cmd, packet, addrs, ports, f)
}
type deviceFinder interface {
list(context.Context, chan *fuchsiaDevice) error
resolve(context.Context, chan *fuchsiaDevice, ...string) error
close()
}
func (cmd *devFinderCmd) close() {
for _, finder := range cmd.finders {
finder.close()
}
}
func (cmd *devFinderCmd) deviceFinders() ([]deviceFinder, error) {
if len(cmd.finders) == 0 {
{
mdnsSet := false
netbootSet := false
cmd.flagSet.Visit(func(f *flag.Flag) {
switch f.Name {
case mdnsFlag:
mdnsSet = true
case netbootFlag:
netbootSet = true
}
})
if !mdnsSet {
if netbootSet {
// Turn off the mDNS default if only netboot is specified.
cmd.mdns = false
} else if cmd.localResolve {
// Local resolution produces the same result regardless of protocol; default to enabling
// both if neither is specified.
cmd.netboot = true
}
}
}
if !cmd.localResolve && cmd.netboot && cmd.mdns {
return nil, errors.New("only one of mdns and netboot may be specified")
}
if cmd.mdns {
if runtime.GOOS == "darwin" {
cmd.finders = append(cmd.finders, newDNSSDFinder(cmd))
} else {
cmd.finders = append(cmd.finders, &mdnsFinder{deviceFinderBase{cmd: cmd}})
}
}
if cmd.netboot && cmd.ipv6 {
cmd.finders = append(cmd.finders, &netbootFinder{deviceFinderBase{cmd: cmd}})
} else if !cmd.mdns {
return nil, errors.New("either mdns or netboot and ipv6 must be specified")
}
}
return cmd.finders, nil
}
func (cmd *devFinderCmd) shouldIgnoreIP(addr net.IP) bool {
return addr.To4() != nil && !cmd.ipv4 || addr.To4() == nil && !cmd.ipv6
}
// filterInboundDevices takes a context and a channel (which has already been passed to some setup
// code that will be writing into it asynchronously), and reads inbound fuchsiaDevice objects
// until a timeout is reached.
//
// This applies all base command filters.
//
// This function executes synchronously.
func (cmd *devFinderCmd) filterInboundDevices(ctx context.Context, f <-chan *fuchsiaDevice, domains ...string) ([]*fuchsiaDevice, error) {
ctx, cancel := context.WithTimeout(ctx, cmd.timeout)
defer cancel()
defer cmd.close()
devices := make(map[string]*fuchsiaDevice)
for _, d := range domains {
devices[d] = nil
}
for {
select {
case <-ctx.Done():
return sortDeviceMap(devices), nil
case device := <-f:
if err := device.err; err != nil {
return nil, err
}
if cmd.shouldIgnoreIP(device.addr) || (isLinkLocal6(device.addr) && device.zone == "") {
continue
}
if d, ok := devices[device.domain]; len(domains) == 0 || ok {
// Bias the results to link local ipv6 addresses
if d == nil || isLinkLocal6(device.addr) {
devices[device.domain] = device
}
}
if cmd.deviceLimit != 0 {
c := cmd.deviceLimit
for _, d := range devices {
if d != nil {
c--
}
}
if c == 0 {
return sortDeviceMap(devices), nil
}
}
// When not looking for specific domains, wait until timeout.
keepWaiting := len(domains) == 0
for _, domain := range domains {
// If any domain does not yet have a desired link-local ipv6 address, keep waiting.
if d, ok := devices[domain]; !ok || d == nil || (cmd.ipv6 && !isLinkLocal6(d.addr)) {
keepWaiting = true
}
}
if !keepWaiting {
return sortDeviceMap(devices), nil
}
}
}
}
func isLinkLocal6(ip net.IP) bool {
return ip.To16() != nil && ip.IsLinkLocalUnicast()
}
func (cmd *devFinderCmd) outputNormal(filteredDevices []*fuchsiaDevice, includeDomain bool) error {
for _, device := range filteredDevices {
if includeDomain {
fmt.Fprintf(cmd.Output(), "%s %s\n", device.addrString(), device.domain)
} else {
fmt.Fprintf(cmd.Output(), "%s\n", device.addrString())
}
}
return nil
}
// jsonOutput represents the output in JSON format.
type jsonOutput struct {
// List of devices found.
Devices []jsonDevice `json:"devices"`
}
type jsonDevice struct {
// Device IP address.
Addr string `json:"addr"`
// Device domain name. Can be omitted.
Domain string `json:"domain,omitempty"`
}
func (cmd *devFinderCmd) outputJSON(filteredDevices []*fuchsiaDevice, includeDomain bool) error {
jsonOut := jsonOutput{Devices: make([]jsonDevice, 0, len(filteredDevices))}
for _, device := range filteredDevices {
dev := jsonDevice{Addr: device.addrString()}
if includeDomain {
dev.Domain = device.domain
}
jsonOut.Devices = append(jsonOut.Devices, dev)
}
e := json.NewEncoder(cmd.Output())
e.SetIndent("", " ")
return e.Encode(jsonOut)
}