diff --git a/ifaddr.go b/ifaddr.go index 0811b2759..c57429bf5 100644 --- a/ifaddr.go +++ b/ifaddr.go @@ -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" <> | sort "type,size" | include "flag" "forwardable" | attr "address" }}' @@ -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" <> | 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" <> | sort "type,size" | <> | attr "address" }}' +/// ``` +// +// where <> 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 diff --git a/ifaddr_test.go b/ifaddr_test.go index 45a0cc788..a708a8e8d 100644 --- a/ifaddr_test.go +++ b/ifaddr_test.go @@ -118,13 +118,112 @@ func TestGetPublicIPs(t *testing.T) { } func TestGetInterfaceIP(t *testing.T) { - ip, err := sockaddr.GetInterfaceIP(`^.*[\d]$`) - if err != nil { - t.Fatalf("regexp failed: %v", err) + + orig := sockaddr.GetAllInterfaces + sockaddr.GetAllInterfaces = mockGetAllInterfaces() + t.Cleanup(func() { sockaddr.GetAllInterfaces = orig }) + + 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) { + + orig := sockaddr.GetAllInterfaces + sockaddr.GetAllInterfaces = mockGetAllInterfaces() + t.Cleanup(func() { sockaddr.GetAllInterfaces = orig }) + + 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) + } + }) } } @@ -592,3 +691,51 @@ func TestIfAddrMath(t *testing.T) { } } } + +// mockGetAllInterfaces returns a mocked implementation of an otherwise OS provided +// sockaddr.GetAllInterfaces() +// Currently it returns a func so that it could be easily parametrized in order to support +// additional scenarios selectable by adding an argument and the relevant switch logic below +func mockGetAllInterfaces() func() (sockaddr.IfAddrs, error) { + return func() (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 + } +} diff --git a/ifaddrs.go b/ifaddrs.go index 80f61bef6..03d9009d5 100644 --- a/ifaddrs.go +++ b/ifaddrs.go @@ -236,10 +236,14 @@ func IfAttrs(selectorName string, ifAddrs IfAddrs) (string, error) { return attrVal, err } -// GetAllInterfaces iterates over all available network interfaces and finds all +// GetAllInterfaces proxies the calls to the real implementation +// to provide mocking capabilities while testing +var GetAllInterfaces func() (IfAddrs, error) = getAllInterfaces + +// 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) { +func getAllInterfaces() (IfAddrs, error) { ifs, err := net.Interfaces() if err != nil { return nil, err @@ -1214,7 +1218,7 @@ 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 } @@ -1222,7 +1226,6 @@ func parseDefaultIfNameFromIPCmdAndroid(routeOut string) (string, error) { return "", errors.New("No default interface found") } - // parseIfNameFromIPCmd parses interfaces from ip(8) for // Linux. func parseIfNameFromIPCmd(routeOut string) [][]string { diff --git a/template/template.go b/template/template.go index bbed51361..549733e18 100644 --- a/template/template.go +++ b/template/template.go @@ -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,