diff --git a/netutil/netutil.go b/netutil/netutil.go new file mode 100644 index 000000000..a1b7c1d40 --- /dev/null +++ b/netutil/netutil.go @@ -0,0 +1,57 @@ +package netutil + +import ( + "net" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" +) + +var ( + getInterfaceAddrs = (*net.Interface).Addrs +) + +// PrivateNetworkInterfaces lists network interfaces and returns those having an address conformant to RFC1918 +func PrivateNetworkInterfaces(logger log.Logger) []string { + ints, err := net.Interfaces() + if err != nil { + level.Warn(logger).Log("msg", "error getting network interfaces", "err", err) + } + return privateNetworkInterfaces(ints, []string{}, logger) +} + +func PrivateNetworkInterfacesWithFallback(fallback []string, logger log.Logger) []string { + ints, err := net.Interfaces() + if err != nil { + level.Warn(logger).Log("msg", "error getting network interfaces", "err", err) + } + return privateNetworkInterfaces(ints, fallback, logger) +} + +// private testable function that checks each given interface +func privateNetworkInterfaces(all []net.Interface, fallback []string, logger log.Logger) []string { + var privInts []string + for _, i := range all { + addrs, err := getInterfaceAddrs(&i) + if err != nil { + level.Warn(logger).Log("msg", "error getting addresses from network interface", "interface", i.Name, "err", err) + } + for _, a := range addrs { + s := a.String() + ip, _, err := net.ParseCIDR(s) + if err != nil { + level.Warn(logger).Log("msg", "error parsing network interface IP address", "interface", i.Name, "addr", s, "err", err) + continue + } + if ip.IsPrivate() { + privInts = append(privInts, i.Name) + break + } + } + } + if len(privInts) == 0 { + return fallback + } + level.Debug(logger).Log("msg", "found network interfaces with private IP addresses assigned", "interfaces", privInts) + return privInts +} diff --git a/netutil/netutil_test.go b/netutil/netutil_test.go new file mode 100644 index 000000000..72b68f470 --- /dev/null +++ b/netutil/netutil_test.go @@ -0,0 +1,127 @@ +package netutil + +import ( + "net" + "os" + "testing" + + "github.com/go-kit/log" + "github.com/stretchr/testify/assert" +) + +// A type that implements the net.Addr interface +// Only String() is called by netutil logic +type mockAddr struct { + netAddr string +} + +func (ma mockAddr) Network() string { + return "tcp" +} + +func (ma mockAddr) String() string { + return ma.netAddr +} + +// Helper function to test a list of interfaces +func generateTestInterfaces(names []string) []net.Interface { + testInts := []net.Interface{} + for i, j := range names { + k := net.Interface{ + Index: i + 1, + MTU: 1500, + Name: j, + HardwareAddr: []byte{}, + Flags: 0, + } + testInts = append(testInts, k) + } + return testInts +} + +func TestPrivateInterface(t *testing.T) { + testIntsAddrs := map[string][]string{ + "privNetA": {"10.6.19.34/8"}, + "privNetB": {"172.16.0.7/12"}, + "privNetC": {"192.168.3.29/24"}, + "pubNet": {"34.120.177.193/24"}, + "multiPriv": {"10.6.19.34/8", "172.16.0.7/12"}, + "multiMix": {"1.1.1.1/24", "192.168.0.42/24"}, + "multiPub": {"1.1.1.1/24", "34.120.177.193/24"}, + } + defaultOutput := []string{"eth0", "en0"} + type testCases struct { + description string + interfaces []string + expectedOutput []string + } + for _, scenario := range []testCases{ + { + description: "empty interface list", + interfaces: []string{}, + expectedOutput: defaultOutput, + }, + { + description: "single private interface", + interfaces: []string{"privNetA"}, + expectedOutput: []string{"privNetA"}, + }, + { + description: "single public interface", + interfaces: []string{"pubNet"}, + expectedOutput: defaultOutput, + }, + { + description: "single interface multi address private", + interfaces: []string{"multiPriv"}, + expectedOutput: []string{"multiPriv"}, + }, + { + description: "single interface multi address mix", + interfaces: []string{"multiMix"}, + expectedOutput: []string{"multiMix"}, + }, + { + description: "single interface multi address public", + interfaces: []string{"multiPub"}, + expectedOutput: defaultOutput, + }, + { + description: "all private interfaces", + interfaces: []string{"privNetA", "privNetB", "privNetC"}, + expectedOutput: []string{"privNetA", "privNetB", "privNetC"}, + }, + { + description: "mix of public and private interfaces", + interfaces: []string{"pubNet", "privNetA", "privNetB", "privNetC", "multiPriv", "multiMix", "multiPub"}, + expectedOutput: []string{"privNetA", "privNetB", "privNetC", "multiPriv", "multiMix"}, + }, + } { + getInterfaceAddrs = func(i *net.Interface) ([]net.Addr, error) { + addrs := []net.Addr{} + for _, ip := range testIntsAddrs[i.Name] { + addrs = append(addrs, mockAddr{netAddr: ip}) + } + return addrs, nil + } + t.Run(scenario.description, func(t *testing.T) { + privInts := privateNetworkInterfaces( + generateTestInterfaces(scenario.interfaces), + defaultOutput, + log.NewNopLogger(), + ) + assert.Equal(t, privInts, scenario.expectedOutput) + }) + } +} + +func TestPrivateInterfaceError(t *testing.T) { + interfaces := generateTestInterfaces([]string{"eth9"}) + ipaddr := "not_a_parseable_ip_string" + getInterfaceAddrs = func(i *net.Interface) ([]net.Addr, error) { + return []net.Addr{mockAddr{netAddr: ipaddr}}, nil + } + logger := log.NewLogfmtLogger(os.Stdout) + privInts := privateNetworkInterfaces(interfaces, []string{}, logger) + assert.Equal(t, privInts, []string{}) +} diff --git a/ring/lifecycler.go b/ring/lifecycler.go index 68c012238..d0b046c09 100644 --- a/ring/lifecycler.go +++ b/ring/lifecycler.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/dskit/flagext" "github.com/grafana/dskit/kv" + "github.com/grafana/dskit/netutil" "github.com/grafana/dskit/services" ) @@ -53,13 +54,13 @@ type LifecyclerConfig struct { // RegisterFlags adds the flags required to config this to the given FlagSet. // The default values of some flags can be changed; see docs of LifecyclerConfig. -func (cfg *LifecyclerConfig) RegisterFlags(f *flag.FlagSet) { - cfg.RegisterFlagsWithPrefix("", f) +func (cfg *LifecyclerConfig) RegisterFlags(f *flag.FlagSet, logger log.Logger) { + cfg.RegisterFlagsWithPrefix("", f, logger) } // RegisterFlagsWithPrefix adds the flags required to config this to the given FlagSet. // The default values of some flags can be changed; see docs of LifecyclerConfig. -func (cfg *LifecyclerConfig) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) { +func (cfg *LifecyclerConfig) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet, logger log.Logger) { cfg.RingConfig.RegisterFlagsWithPrefix(prefix, f) // In order to keep backwards compatibility all of these need to be prefixed @@ -81,7 +82,7 @@ func (cfg *LifecyclerConfig) RegisterFlagsWithPrefix(prefix string, f *flag.Flag panic(fmt.Errorf("failed to get hostname %s", err)) } - cfg.InfNames = []string{"eth0", "en0"} + cfg.InfNames = netutil.PrivateNetworkInterfacesWithFallback([]string{"eth0", "en0"}, logger) f.Var((*flagext.StringSlice)(&cfg.InfNames), prefix+"lifecycler.interface", "Name of network interface to read address from.") f.StringVar(&cfg.Addr, prefix+"lifecycler.addr", "", "IP address to advertise in the ring.") f.IntVar(&cfg.Port, prefix+"lifecycler.port", 0, "port to advertise in consul (defaults to server.grpc-listen-port).")