Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/get interface ip nonforwardable network adapters handling #38

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions ifaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ func GetPublicIPs() (string, error) {
return strings.Join(ips, " "), nil
}

// GetInterfaceIP returns a string with a single IP address sorted by the size
// of the network (i.e. IP addresses with a smaller netmask, larger network
// size, are sorted first). This function is the `eval` equivalent of:
// GetInterfaceIP returns a string with a single forwardable IP address sorted
// by the size of the network (i.e. IP addresses with a smaller netmask, larger
// network size, are sorted first). This function is the `eval` equivalent of:
//
// ```
// $ sockaddr eval -r '{{GetAllInterfaces | include "name" <<ARG>> | sort "type,size" | include "flag" "forwardable" | attr "address" }}'
Expand All @@ -153,17 +153,54 @@ func GetInterfaceIP(namedIfRE string) (string, error) {
if err != nil {
return "", err
}
flags := []string{
"forwardable",
}
return getInterfaceIP(namedIfRE, flags, ifAddrs)
}

ifAddrs, _, err = IfByName(namedIfRE, ifAddrs)
// GetInterfaceIPWithoutInterfaceFlags returns a string with a single IP address sorted
// by the size of the network (i.e. IP addresses with a smaller netmask, larger
// network size, are sorted first). This function is the `eval` equivalent of:
//
// ```
// $ sockaddr eval -r '{{GetAllInterfaces | include "name" <<ARG>> | sort "type,size" | attr "address" }}'
/// ```
func GetInterfaceIPWithoutInterfaceFlags(namedIfRE string) (string, error) {
ifAddrs, err := GetAllInterfaces()
if err != nil {
return "", err
}
return getInterfaceIP(namedIfRE, nil, ifAddrs)
}

ifAddrs, _, err = IfByFlag("forwardable", ifAddrs)
// getInterfaceIP returns a string with a single IP address sorted
// by the size of the network (i.e. IP addresses with a smaller netmask, larger
// network size, are sorted first). This function is the `eval` equivalent of:
//
// ```
// $ sockaddr eval -r '{{GetAllInterfaces | include "name" <<ARG>> | sort "type,size" | <<FLAGS>> | attr "address" }}'
/// ```
//
// where <<FLAGS>> represents a logical AND between all the supplied flags.
//
// For example:
//
// with flags:=[]string{"forwardable", "broadcast"} => `include "flag" "forwardable" | include "flag" "broadcast"`
///
func getInterfaceIP(namedIfRE string, flags []string, ifAddrs IfAddrs) (string, error) {
ifAddrs, _, err := IfByName(namedIfRE, ifAddrs)
if err != nil {
return "", err
}

for _, flag := range flags {
ifAddrs, _, err = IfByFlag(flag, ifAddrs)
if err != nil {
return "", err
}
}

ifAddrs, err = SortIfBy("+type,+size", ifAddrs)
if err != nil {
return "", err
Expand Down
169 changes: 164 additions & 5 deletions ifaddr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,114 @@ func TestGetPublicIPs(t *testing.T) {
}

func TestGetInterfaceIP(t *testing.T) {
ip, err := sockaddr.GetInterfaceIP(`^.*[\d]$`)
if err != nil {
t.Fatalf("regexp failed: %v", err)

// setting up a "fake environment" by temporarily controlling what's returned by
// `sockaddr.GetAllInterfaces`
teardown := overrideOsNetProvider(&fakeNetProvider{})
defer teardown()

type args struct {
namedIfRE string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "loopback => empty string",
args: args{namedIfRE: "lo"},
want: "",
wantErr: false,
},
{
name: "private (RFC1918) => 172.16.0.0",
args: args{namedIfRE: "eth0"},
want: "172.16.0.0",
wantErr: false,
},
{
name: "dummy (RFC3927) => empty string",
args: args{namedIfRE: "dummy"},
want: "",
wantErr: false,
},
{
name: "dummyv6 (RFC4291) IPv6 => empty string",
args: args{namedIfRE: "dummyv6"},
want: "",
wantErr: false,
},
}

if ip == "" {
t.Skip("it's hard to test this reliably")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sockaddr.GetInterfaceIP(tt.args.namedIfRE)
if (err != nil) != tt.wantErr {
t.Errorf("GetInterfaceIP() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetInterfaceIP() = %v, want %v", got, tt.want)
}
})
}
}

func TestGetInterfaceIPWithoutInterfaceFlags(t *testing.T) {

// setting up a "fake environment" by temporarily controlling what's returned by
// `sockaddr.GetAllInterfaces`
teardown := overrideOsNetProvider(&fakeNetProvider{})
defer teardown()

type args struct {
namedIfRE string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "loopback => 127.0.0.0",
args: args{namedIfRE: "lo"},
want: "127.0.0.0",
wantErr: false,
},
{
name: "private (RFC1918) => 172.16.0.0",
args: args{namedIfRE: "eth0"},
want: "172.16.0.0",
wantErr: false,
},
{
name: "dummy (RFC3927) => 169.254.0.0",
args: args{namedIfRE: "dummy"},
want: "169.254.0.0",
wantErr: false,
},
{
name: "dummyv6 (RFC4291) IPv6 => fe80::",
args: args{namedIfRE: "dummyv6"},
want: "fe80::",
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sockaddr.GetInterfaceIPWithoutInterfaceFlags(tt.args.namedIfRE)
if (err != nil) != tt.wantErr {
t.Errorf("GetInterfaceIPWithoutInterfaceFlags() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetInterfaceIPWithoutInterfaceFlags() = %v, want %v", got, tt.want)
}
})
}
}

Expand Down Expand Up @@ -592,3 +693,61 @@ func TestIfAddrMath(t *testing.T) {
}
}
}

// overrideOsNetProvider allows overriding the sockaddr.NetProvider
//
// It returns a cleanup/teardown func that restores the default OSNetProvider after use
//
// Usage:
// teardown := overrideOsNetProvider(fakeNetProvider{})
// defer teardown()
func overrideOsNetProvider(override sockaddr.NetworkInterfacesProvider) func() {
sockaddr.NetProvider = override
return func() {
sockaddr.NetProvider = &sockaddr.OSNetProvider{}
}
}

type fakeNetProvider struct{}

func (n *fakeNetProvider) GetAllInterfaces() (sockaddr.IfAddrs, error) {
ifAddrs := sockaddr.IfAddrs{
{
SockAddr: sockaddr.MustIPv4Addr("127.0.0.0/8"),
Interface: net.Interface{
Index: 1,
MTU: 65536,
Name: "lo",
Flags: net.FlagUp | net.FlagLoopback,
},
},
{
SockAddr: sockaddr.MustIPv4Addr("172.16.0.0/12"),
Interface: net.Interface{
Index: 2,
MTU: 1500,
Name: "eth0",
Flags: net.FlagUp,
},
},
{
SockAddr: sockaddr.MustIPv4Addr("169.254.0.0/16"),
Interface: net.Interface{
Index: 3,
MTU: 1500,
Name: "dummy",
Flags: net.FlagBroadcast,
},
},
{
SockAddr: sockaddr.MustIPv6Addr("fe80::/10"),
Interface: net.Interface{
Index: 3,
MTU: 1500,
Name: "dummyv6",
Flags: net.FlagBroadcast,
},
},
}
return ifAddrs, nil
}
43 changes: 11 additions & 32 deletions ifaddrs.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,38 +236,18 @@ func IfAttrs(selectorName string, ifAddrs IfAddrs) (string, error) {
return attrVal, err
}

// GetAllInterfaces iterates over all available network interfaces and finds all
// available IP addresses on each interface and converts them to
// sockaddr.IPAddrs, and returning the result as an array of IfAddr.
func GetAllInterfaces() (IfAddrs, error) {
kisunji marked this conversation as resolved.
Show resolved Hide resolved
ifs, err := net.Interfaces()
if err != nil {
return nil, err
}

ifAddrs := make(IfAddrs, 0, len(ifs))
for _, intf := range ifs {
addrs, err := intf.Addrs()
if err != nil {
return nil, err
}

for _, addr := range addrs {
var ipAddr IPAddr
ipAddr, err = NewIPAddr(addr.String())
if err != nil {
return IfAddrs{}, fmt.Errorf("unable to create an IP address from %q", addr.String())
}
// NetworkInterfacesProvider abstracts the way we gather network interfaces in order
// to improve testability
type NetworkInterfacesProvider interface {
GetAllInterfaces() (IfAddrs, error)
}

ifAddr := IfAddr{
SockAddr: ipAddr,
Interface: intf,
}
ifAddrs = append(ifAddrs, ifAddr)
}
}
// NetProvider implements NetworkInterfacesProvider and is defined here with its default
var NetProvider NetworkInterfacesProvider = &OSNetProvider{}

return ifAddrs, nil
// GetAllInterfaces proxies the call to the NetProvider
func GetAllInterfaces() (IfAddrs, error) {
return NetProvider.GetAllInterfaces()
}

// GetDefaultInterfaces returns IfAddrs of the addresses attached to the default
Expand Down Expand Up @@ -1214,15 +1194,14 @@ func parseDefaultIfNameFromIPCmd(routeOut string) (string, error) {
// Android.
func parseDefaultIfNameFromIPCmdAndroid(routeOut string) (string, error) {
parsedLines := parseIfNameFromIPCmd(routeOut)
if (len(parsedLines) > 0) {
if len(parsedLines) > 0 {
ifName := strings.TrimSpace(parsedLines[0][4])
return ifName, nil
}

return "", errors.New("No default interface found")
}


// parseIfNameFromIPCmd parses interfaces from ip(8) for
// Linux.
func parseIfNameFromIPCmd(routeOut string) [][]string {
Expand Down
42 changes: 42 additions & 0 deletions os_net_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package sockaddr

import (
"fmt"
"net"
)

type OSNetProvider struct{}

// GetAllInterfaces iterates over all available network interfaces and finds all
// available IP addresses on each interface and converts them to
// sockaddr.IPAddrs, and returning the result as an array of IfAddr.
func (n *OSNetProvider) GetAllInterfaces() (IfAddrs, error) {
ifs, err := net.Interfaces()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @deblasis thanks for following up on the team's latest -- we really appreciate your efforts here and receptiveness to feedback.

I may not have been super clear in a previous comment but the line we want to stub out is this one!

We probably want all of the code here to be executed during the tests, except for this line.

This line should look something like

ifs, err := netProvider.Interfaces()

and in the testing we can set up a FakeNetProvider. What do you think?

Copy link

@kisunji kisunji Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'm not sure if we can easily inject a netProvider-like dependency here since the exported functions are func(string) (string, error).

It will probably be easiest to declare a global function variable

packages sockaddr

var netInterfaces = net.Interfaces

and stub out global variable sockaddr.netInterfaces in tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi there, sorry for the delayed reply, I have been very busy freelancing.
I had a quick look and I believe we are oversimplifying. I'll try to explain what I mean:

We can sure stub out net.Interfaces but that means that we are stubbing out the hardware information about our interfaces, what about the software part?
We are forgetting the calls to intf.Addrs() that look like they are returning network protocol and IP Addresses for the NICs by querying the RIB (Routing Information Base) which returns the address table.

https://github.com/hashicorp/go-sockaddr/pull/38/files#diff-9753b911177155deeb161b6f775d09bdbb6cb51fb315e9b6e9bc3130affd0b46R21

that internally call net.interfaceAddrTable
image

and net.addrTable
image

As far as I understand, in order to be able to stub out all the system calls that are involved in computing the return value of our sockaddr.GetAllInterfaces(), I think we should also look into stubbing these other system calls as well...

Please correct me if I am wrong and let me know how to proceed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great point @deblasis, I agree that the entire GetAllInterfaces function should be mocked.

I think exporting new interfaces and implementation structs to mock it are overkill; could we perhaps simply do

var GetAllInterfaces func() (IfAddrs, error) = getAllInterfaces

func getAllInterfaces() (IfAddrs, error) {
        // implementation ...
}

// ifaddr_test.go
func TestGetInterfaceIP(t *testing.T) {

	orig := sockaddr.GetAllInterfaces
        sockaddr.GetAllInterfaces = mockFunc()
        t.Cleanup(func(){ sockaddr.GetAllInterfaces = orig })

        // tests ...
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kisunji!

I apologize for the delay.
Sounds good, I have updated the PR accordingly.

if err != nil {
return nil, err
}

ifAddrs := make(IfAddrs, 0, len(ifs))
for _, intf := range ifs {
addrs, err := intf.Addrs()
if err != nil {
return nil, err
}

for _, addr := range addrs {
var ipAddr IPAddr
ipAddr, err = NewIPAddr(addr.String())
if err != nil {
return IfAddrs{}, fmt.Errorf("unable to create an IP address from %q", addr.String())
}

ifAddr := IfAddr{
SockAddr: ipAddr,
Interface: intf,
}
ifAddrs = append(ifAddrs, ifAddr)
}
}

return ifAddrs, nil
}
3 changes: 3 additions & 0 deletions template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ func init() {
// the largest network size.
"GetInterfaceIP": sockaddr.GetInterfaceIP,

// Returns the first IP address of the named interfaces, regardless of any interface flag
"GetInterfaceIPWithoutInterfaceFlags": sockaddr.GetInterfaceIPWithoutInterfaceFlags,

// Return all IP addresses on the named interface, sorted by the largest
// network size.
"GetInterfaceIPs": sockaddr.GetInterfaceIPs,
Expand Down