From c494b7d508473e3dce074fcfcda573743e827204 Mon Sep 17 00:00:00 2001 From: ale <ale@incal.net> Date: Sat, 17 Oct 2015 10:25:19 +0100 Subject: [PATCH] autodetect public IP address and network interface This provides some discovery capabilities to radiod and redirectord, so that in most cases fewer command-line options are required for operation. --- cmd/radiod/radiod.go | 12 ++- cmd/redirectord/redirectord.go | 8 +- util/flag.go | 7 -- util/net_detect.go | 168 +++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 util/net_detect.go diff --git a/cmd/radiod/radiod.go b/cmd/radiod/radiod.go index ffce1854..cadd9473 100644 --- a/cmd/radiod/radiod.go +++ b/cmd/radiod/radiod.go @@ -16,9 +16,9 @@ import ( var ( name = flag.String("name", shortHostname(), "Name for this node") - publicIPs = util.IPList("ip", "Public IP for this machine (may be specified more than once)") - internalIPs = util.IPList("internal-ip", "Internal IP for this machine (within the cluster), if different than --ip") - netDev = flag.String("interface", "eth0", "Network interface to monitor for utilization") + publicIPs = util.IPList("ip", "Public IP for this machine (may be specified more than once). If unset, the program will try to resolve the local hostname, or it will fall back to inspecting network devices.") + internalIPs = util.IPList("internal-ip", "Internal IP for this machine (within the cluster), if different from --ip") + netDev = flag.String("interface", "", "Network interface to monitor for utilization. If unset, default to the interface associated with --ip.") bwLimit = flag.Int("bwlimit", 100, "Bandwidth usage limit (Mbps)") maxClients = flag.Int("max-clients", 1000, "Maximum number of connected clients") ) @@ -35,11 +35,15 @@ func main() { log.SetFlags(0) flag.Parse() + if err := util.DetectPublicNetworkParams(publicIPs, *internalIPs, netDev); err != nil { + log.Fatal(err) + } + instrumentation.NewCounter("radiod.restarts").Incr() client := autoradio.NewEtcdClient(true) bwLimitBytes := float64(*bwLimit * 1000000 / 8) - n := node.NewRadioNode(*name, util.IPListWithDefault(*publicIPs, "127.0.0.1"), *internalIPs, *netDev, bwLimitBytes, *maxClients, client) + n := node.NewRadioNode(*name, *publicIPs, *internalIPs, *netDev, bwLimitBytes, *maxClients, client) // Set up a clean shutdown function on SIGTERM. stopch := make(chan os.Signal) diff --git a/cmd/redirectord/redirectord.go b/cmd/redirectord/redirectord.go index 79135ada..e8659053 100644 --- a/cmd/redirectord/redirectord.go +++ b/cmd/redirectord/redirectord.go @@ -15,7 +15,7 @@ import ( var ( domain = flag.String("domain", "", "DNS domain to serve") - publicIps = util.IPList("ip", "Public IP for this machine (may be specified more than once)") + publicIPs = util.IPList("ip", "Public IP for this machine (may be specified more than once). If unset, the program will try to resolve the local hostname, or it will fall back to inspecting network devices.") dnsPort = flag.Int("dns-port", 53, "DNS port") httpPort = flag.Int("http-port", 80, "HTTP port") staticDir = flag.String("static-dir", "/usr/share/autoradio/htdocs/static", "Static content directory") @@ -34,11 +34,15 @@ func main() { log.Fatal("Must specify --domain") } + if err := util.DetectPublicNetworkParams(publicIPs, nil, nil); err != nil { + log.Fatal(err) + } + instrumentation.NewCounter("redirectord.restarts").Incr() client := autoradio.NewClient(autoradio.NewEtcdClient(false)) - dnsRed := fe.NewDNSRedirector(client, *domain, util.IPListWithDefault(*publicIps, "127.0.0.1"), dnsTtl) + dnsRed := fe.NewDNSRedirector(client, *domain, *publicIPs, dnsTtl) dnsRed.Start(fmt.Sprintf(":%d", *dnsPort)) red, err := fe.NewHTTPRedirector(client, *domain, *lbPolicy, *staticDir, *templateDir) diff --git a/util/flag.go b/util/flag.go index 0f6823cd..d3c4c9e5 100644 --- a/util/flag.go +++ b/util/flag.go @@ -31,10 +31,3 @@ func IPList(name, help string) *[]net.IP { flag.Var(&l, name, help) return (*[]net.IP)(&l) } - -func IPListWithDefault(l []net.IP, deflt string) []net.IP { - if len(l) == 0 { - return []net.IP{net.ParseIP(deflt)} - } - return l -} diff --git a/util/net_detect.go b/util/net_detect.go new file mode 100644 index 00000000..af306131 --- /dev/null +++ b/util/net_detect.go @@ -0,0 +1,168 @@ +package util + +import ( + "errors" + "fmt" + "log" + "net" + "os" +) + +// DetectPublicNetworkParams tries to be smart and find a public IP +// address and network interface for the local host. +// +// If *publicIPs is nil, attempts to resolve the local hostname and +// look for non-local addresses. If none are found, figures out the IP +// from the network interface, if given, or finds the first +// non-loopback interface. +// +// If *netDev is an empty string, tries to figure out the network +// device to use from either internalIPs or *publicIPs, whichever is +// not nil. The netDev pointer itself can be nil, in which case no +// network device detection is attempted. +// +func DetectPublicNetworkParams(publicIPs *[]net.IP, internalIPs []net.IP, netDev *string) error { + if len(*publicIPs) == 0 && netDev != nil && *netDev != "" { + ips, err := findIPsForInterface(*netDev) + if err != nil { + log.Printf("Warning: %v", err) + } + if len(ips) > 0 { + log.Printf("autodetected IP addresses %v on %s", ips, *netDev) + *publicIPs = ips + } + } + if len(*publicIPs) == 0 { + // Try to guess the public IP from the hostname. + ips, err := guessPublicIPs() + if err != nil { + log.Printf("Warning: %v", err) + dev, ips, err := findFirstNonLoopbackInterface() + if err != nil { + log.Printf("Warning: no non-loopback interfaces found") + } else { + if netDev != nil { + *netDev = dev + } + *publicIPs = ips + log.Printf("autodetected network device %s and IP addresses %v", dev, ips) + } + } else { + log.Printf("autodetected IP addresses %v", ips) + *publicIPs = ips + } + } + if len(*publicIPs) == 0 { + return errors.New("public IP address is not set and autodetection has failed, can't start") + } + if netDev != nil && *netDev == "" { + ips := internalIPs + if len(ips) == 0 { + ips = *publicIPs + } + dev, err := findInterfaceForIPs(ips) + if err != nil { + log.Printf("Warning: %v", err) + log.Printf("will assume interface = eth0") + dev = "eth0" + } else { + log.Printf("autodetected network interface %s", dev) + } + *netDev = dev + } + return nil +} + +func filterUnicast(ips []net.IP) []net.IP { + var out []net.IP + for _, ip := range ips { + if ip.IsGlobalUnicast() { + out = append(out, ip) + } + } + return out +} + +func guessPublicIPs() ([]net.IP, error) { + // Resolve our hostname and try to find non-local addresses. + hostname, _ := os.Hostname() + addrs, err := net.LookupIP(hostname) + if err != nil || len(addrs) == 0 { + return nil, fmt.Errorf("could not resolve own hostname: %v", err) + } + addrs = filterUnicast(addrs) + if len(addrs) == 0 { + return nil, fmt.Errorf("no non-loopback addresses for %v", hostname) + } + return addrs, nil +} + +func getActiveInterfaces() []net.Interface { + ifs, err := net.Interfaces() + if err != nil { + log.Printf("Warning: %v", err) + return nil + } + var out []net.Interface + for _, i := range ifs { + if (i.Flags&net.FlagLoopback) == net.FlagLoopback || (i.Flags&net.FlagUp) == 0 { + continue + } + out = append(out, i) + } + return out +} + +func interfaceIPs(i net.Interface) []net.IP { + var out []net.IP + addrs, err := i.Addrs() + if err != nil { + return nil + } + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err == nil { + out = append(out, ip) + } + } + return out +} + +func findInterfaceForIP(ip net.IP) (string, error) { + for _, i := range getActiveInterfaces() { + for _, ifIP := range filterUnicast(interfaceIPs(i)) { + log.Printf("found interface address %s %s", i.Name, ifIP) + if ifIP.Equal(ip) { + return i.Name, nil + } + } + } + return "", errors.New("not found") +} + +func findInterfaceForIPs(ips []net.IP) (string, error) { + for _, ip := range ips { + if result, err := findInterfaceForIP(ip); err == nil { + return result, nil + } + } + return "", errors.New("no interfaces found") +} + +func findIPsForInterface(dev string) ([]net.IP, error) { + i, err := net.InterfaceByName(dev) + if err != nil { + return nil, err + } + return filterUnicast(interfaceIPs(*i)), nil +} + +func findFirstNonLoopbackInterface() (string, []net.IP, error) { + for _, i := range getActiveInterfaces() { + addrs := interfaceIPs(i) + if len(addrs) > 0 { + return i.Name, addrs, nil + } + } + return "", nil, errors.New("no non-loopback interfaces found") +} -- GitLab