Skip to content

Commit

Permalink
autodetect interfaces on private networks (#126)
Browse files Browse the repository at this point in the history
* autodetect interfaces on private networks

* removed debug code, improved tests

* cleaned up and improved unit tests

* lifecycler uses interface autodetect

* Merge branch 'add-netutil-package' of github.com:grafana/dskit into add-netutil-package
  • Loading branch information
aldernero authored Feb 10, 2022
1 parent cab7cc8 commit 624e792
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 4 deletions.
57 changes: 57 additions & 0 deletions netutil/netutil.go
Original file line number Diff line number Diff line change
@@ -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
}
127 changes: 127 additions & 0 deletions netutil/netutil_test.go
Original file line number Diff line number Diff line change
@@ -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{})
}
9 changes: 5 additions & 4 deletions ring/lifecycler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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).")
Expand Down

0 comments on commit 624e792

Please sign in to comment.