diff --git a/net/gateway/gateway_common.go b/net/gateway/gateway_common.go new file mode 100644 index 0000000..de301ed --- /dev/null +++ b/net/gateway/gateway_common.go @@ -0,0 +1,131 @@ +package gateway + +import ( + "errors" + "net" + "strings" +) + +var errNoGateway = errors.New("no gateway found") + +func parseWindowsRoutePrint(output []byte) (net.IP, string, error) { + // Windows route output format is always like this: + // =========================================================================== + // Active Routes: + // Network Destination Netmask Gateway Interface Metric + // 0.0.0.0 0.0.0.0 192.168.1.1 192.168.1.100 20 + // =========================================================================== + // I'm trying to pick the active route, + // then jump 2 lines and pick the third IP + // Not using regex because output is quite standard from Windows XP to 8 (NEEDS TESTING) + lines := strings.Split(string(output), "\n") + for idx, line := range lines { + if strings.HasPrefix(line, "Active Routes:") { + if len(lines) <= idx+2 { + return nil, "", errNoGateway + } + + fields := strings.Fields(lines[idx+2]) + if len(fields) < 3 { + return nil, "", errNoGateway + } + + ip := net.ParseIP(fields[2]) + if ip != nil { + return ip, fields[3], nil + } + } + } + return nil, "", errNoGateway +} + +func parseLinuxIPRoute(output []byte) (net.IP, string, error) { + // Linux '/usr/bin/ip route show' format looks like this: + // default via 192.168.178.1 dev wlp3s0 metric 303 + // 192.168.178.0/24 dev wlp3s0 proto kernel scope link src 192.168.178.76 metric 303 + lines := strings.Split(string(output), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 3 && fields[0] == "default" { + ip := net.ParseIP(fields[2]) + if ip != nil { + return ip, fields[4], nil + } + } + } + + return nil, "", errNoGateway +} + +func parseLinuxRoute(output []byte) (net.IP, string, error) { + // Linux route out format is always like this: + // Kernel IP routing table + // Destination Gateway Genmask Flags Metric Ref Use Iface + // 0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0 + lines := strings.Split(string(output), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == "0.0.0.0" { + ip := net.ParseIP(fields[1]) + if ip != nil { + return ip, fields[7], nil + } + } + } + + return nil, "", errNoGateway +} + +func parseDarwinRouteGet(output []byte) (net.IP, string, error) { + // Darwin route out format is always like this: + // route to: default + // destination: default + // mask: default + // gateway: 192.168.1.1 + // interface: tun0 + // flags: + lines := strings.Split(string(output), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == "gateway:" { + ip := net.ParseIP(fields[1]) + if ip != nil { + return ip, "", nil + } + } + } + + return nil, "", errNoGateway +} + +func parseBSDSolarisNetstat(output []byte) (net.IP, string, error) { + // netstat -rn produces the following on FreeBSD: + // Routing tables + // + // Internet: + // Destination Gateway Flags Netif Expire + // default 10.88.88.2 UGS em0 + // 10.88.88.0/24 link#1 U em0 + // 10.88.88.148 link#1 UHS lo0 + // 127.0.0.1 link#2 UH lo0 + // + // Internet6: + // Destination Gateway Flags Netif Expire + // ::/96 ::1 UGRS lo0 + // ::1 link#2 UH lo0 + // ::ffff:0.0.0.0/96 ::1 UGRS lo0 + // fe80::/10 ::1 UGRS lo0 + // ... + outputLines := strings.Split(string(output), "\n") + for _, line := range outputLines { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == "default" { + ip := net.ParseIP(fields[1]) + if ip != nil { + return ip, fields[3], nil + } + } + } + + return nil, "", errNoGateway +} diff --git a/net/gateway/gateway_darwin.go b/net/gateway/gateway_darwin.go new file mode 100644 index 0000000..481d9bf --- /dev/null +++ b/net/gateway/gateway_darwin.go @@ -0,0 +1,16 @@ +package gateway + +import ( + "net" + "os/exec" +) + +func DiscoverGateway() (net.IP, string, error) { + routeCmd := exec.Command("netstat", "-rn") + output, err := routeCmd.CombinedOutput() + if err != nil { + return nil, err + } + + return parseDarwinRouteGet(output) +} diff --git a/net/gateway/gateway_freebsd.go b/net/gateway/gateway_freebsd.go new file mode 100644 index 0000000..4942c27 --- /dev/null +++ b/net/gateway/gateway_freebsd.go @@ -0,0 +1,16 @@ +package gateway + +import ( + "net" + "os/exec" +) + +func DiscoverGateway() (ip net.IP, iface string, err error) { + routeCmd := exec.Command("netstat", "-rn") + output, err := routeCmd.CombinedOutput() + if err != nil { + return nil, err + } + + return parseBSDSolarisNetstat(output) +} diff --git a/net/gateway/gateway_linux.go b/net/gateway/gateway_linux.go new file mode 100644 index 0000000..daec1c7 --- /dev/null +++ b/net/gateway/gateway_linux.go @@ -0,0 +1,34 @@ +package gateway + +import ( + "net" + "os/exec" +) + +func DiscoverGateway() (ip net.IP, iface string, err error) { + ip, iface, err = discoverGatewayUsingRoute() + if err != nil { + ip, iface, err = discoverGatewayUsingIp() + } + return +} + +func discoverGatewayUsingIp() (net.IP, string, error) { + routeCmd := exec.Command("ip", "route", "show") + output, err := routeCmd.CombinedOutput() + if err != nil { + return nil, "", err + } + + return parseLinuxIPRoute(output) +} + +func discoverGatewayUsingRoute() (net.IP, string, error) { + routeCmd := exec.Command("route", "-n") + output, err := routeCmd.CombinedOutput() + if err != nil { + return nil, "", err + } + + return parseLinuxRoute(output) +} diff --git a/net/gateway/gateway_solaris.go b/net/gateway/gateway_solaris.go new file mode 100644 index 0000000..035c22e --- /dev/null +++ b/net/gateway/gateway_solaris.go @@ -0,0 +1,16 @@ +package gateway + +import ( + "net" + "os/exec" +) + +func DiscoverGateway() (ip net.IP, iface string, err error) { + routeCmd := exec.Command("netstat", "-rn") + output, err := routeCmd.CombinedOutput() + if err != nil { + return nil, "", err + } + + return parseBSDSolarisNetstat(output) +} diff --git a/net/gateway/gateway_test.go b/net/gateway/gateway_test.go new file mode 100644 index 0000000..e519173 --- /dev/null +++ b/net/gateway/gateway_test.go @@ -0,0 +1,248 @@ +package gateway + +import ( + "net" + "testing" +) + +type testcase struct { + output []byte + ok bool + gateway string +} + +func TestParseWindowsRoutePrint(t *testing.T) { + correctData := []byte(` +IPv4 Route Table +=========================================================================== +Active Routes: +Network Destination Netmask Gateway Interface Metric + 0.0.0.0 0.0.0.0 10.88.88.2 10.88.88.149 10 +=========================================================================== +Persistent Routes: +`) + randomData := []byte(` +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation +`) + noRoute := []byte(` +IPv4 Route Table +=========================================================================== +Active Routes: +`) + badRoute1 := []byte(` +IPv4 Route Table +=========================================================================== +Active Routes: +=========================================================================== +Persistent Routes: +`) + badRoute2 := []byte(` +IPv4 Route Table +=========================================================================== +Active Routes: +Network Destination Netmask Gateway Interface Metric + 0.0.0.0 0.0.0.0 foo 10.88.88.149 10 +=========================================================================== +Persistent Routes: +`) + + testcases := []testcase{ + {correctData, true, "10.88.88.2"}, + {randomData, false, ""}, + {noRoute, false, ""}, + {badRoute1, false, ""}, + {badRoute2, false, ""}, + } + + test(t, testcases, parseWindowsRoutePrint) +} + +func TestParseLinuxIPRoutePrint(t *testing.T) { + correctData := []byte(` +default via 192.168.178.1 dev wlp3s0 metric 303 +192.168.178.0/24 dev wlp3s0 proto kernel scope link src 192.168.178.76 metric 303 +`) + randomData := []byte(` +test +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation +`) + noRoute := []byte(` +192.168.178.0/24 dev wlp3s0 proto kernel scope link src 192.168.178.76 metric 303 +`) + badRoute := []byte(` +default via foo dev wlp3s0 metric 303 +192.168.178.0/24 dev wlp3s0 proto kernel scope link src 192.168.178.76 metric 303 +`) + + testcases := []testcase{ + {correctData, true, "192.168.178.1"}, + {randomData, false, ""}, + {noRoute, false, ""}, + {badRoute, false, ""}, + } + + test(t, testcases, parseLinuxIPRoute) +} + +func TestParseLinuxRoutePrint(t *testing.T) { + correctData := []byte(` +Kernel IP routing table +Destination Gateway Genmask Flags Metric Ref Use Iface +0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0 +`) + randomData := []byte(` +test +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation +`) + noRoute := []byte(` +Kernel IP routing table +Destination Gateway Genmask Flags Metric Ref Use Iface +`) + badRoute := []byte(` +Kernel IP routing table +Destination Gateway Genmask Flags Metric Ref Use Iface +0.0.0.0 foo 0.0.0.0 UG 0 0 0 eth0 +`) + + testcases := []testcase{ + {correctData, true, "192.168.1.1"}, + {randomData, false, ""}, + {noRoute, false, ""}, + {badRoute, false, ""}, + } + + test(t, testcases, parseLinuxRoute) +} + +func TestParseDarwinRouteGet(t *testing.T) { + correctData := []byte(` + route to: 0.0.0.0 +destination: default + mask: default + gateway: 172.16.32.1 + interface: en0 + flags: + recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire + 0 0 0 0 0 0 1500 0 +`) + randomData := []byte(` +test +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation +`) + noRoute := []byte(` + route to: 0.0.0.0 +destination: default + mask: default +`) + badRoute := []byte(` + route to: 0.0.0.0 +destination: default + mask: default + gateway: foo + interface: en0 + flags: + recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire + 0 0 0 0 0 0 1500 0 +`) + + testcases := []testcase{ + {correctData, true, "172.16.32.1"}, + {randomData, false, ""}, + {noRoute, false, ""}, + {badRoute, false, ""}, + } + + test(t, testcases, parseDarwinRouteGet) +} + +func TestParseBSDSolarisNetstat(t *testing.T) { + correctDataFreeBSD := []byte(` +Routing tables + +Internet: +Destination Gateway Flags Netif Expire +default 10.88.88.2 UGS em0 +10.88.88.0/24 link#1 U em0 +10.88.88.148 link#1 UHS lo0 +127.0.0.1 link#2 UH lo0 + +Internet6: +Destination Gateway Flags Netif Expire +::/96 ::1 UGRS lo0 +::1 link#2 UH lo0 +::ffff:0.0.0.0/96 ::1 UGRS lo0 +fe80::/10 ::1 UGRS lo0 +`) + correctDataSolaris := []byte(` +Routing Table: IPv4 + Destination Gateway Flags Ref Use Interface +-------------------- -------------------- ----- ----- ---------- --------- +default 172.16.32.1 UG 2 76419 net0 +127.0.0.1 127.0.0.1 UH 2 36 lo0 +172.16.32.0 172.16.32.17 U 4 8100 net0 + +Routing Table: IPv6 + Destination/Mask Gateway Flags Ref Use If +--------------------------- --------------------------- ----- --- ------- ----- +::1 ::1 UH 3 75382 lo0 +2001:470:deeb:32::/64 2001:470:deeb:32::17 U 3 2744 net0 +fe80::/10 fe80::6082:52ff:fedc:7df0 U 3 8430 net0 +`) + randomData := []byte(` +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna +aliqua. Ut enim ad minim veniam, quis nostrud exercitation +`) + noRoute := []byte(` +Internet: +Destination Gateway Flags Netif Expire +10.88.88.0/24 link#1 U em0 +10.88.88.148 link#1 UHS lo0 +127.0.0.1 link#2 UH lo0 +`) + badRoute := []byte(` +Internet: +Destination Gateway Flags Netif Expire +default foo UGS em0 +10.88.88.0/24 link#1 U em0 +10.88.88.148 link#1 UHS lo0 +127.0.0.1 link#2 UH lo0 +`) + + testcases := []testcase{ + {correctDataFreeBSD, true, "10.88.88.2"}, + {correctDataSolaris, true, "172.16.32.1"}, + {randomData, false, ""}, + {noRoute, false, ""}, + {badRoute, false, ""}, + } + + test(t, testcases, parseBSDSolarisNetstat) +} + +func test(t *testing.T, testcases []testcase, fn func([]byte) (net.IP, string, error)) { + for i, tc := range testcases { + net, iface, err := fn(tc.output) + if tc.ok { + if err != nil { + t.Errorf("Unexpected error in test #%d: %v", i, err) + } + if net.String() != tc.gateway { + t.Errorf("Unexpected gateway address %v != %s", net, tc.gateway) + } + if "" == iface { + t.Errorf("Unexpected interface") + } + } else if err == nil { + t.Errorf("Unexpected nil error in test #%d", i) + } + } +} diff --git a/net/gateway/gateway_unimplemented.go b/net/gateway/gateway_unimplemented.go new file mode 100644 index 0000000..676553e --- /dev/null +++ b/net/gateway/gateway_unimplemented.go @@ -0,0 +1,14 @@ +// +build !darwin,!linux,!windows,!solaris,!freebsd + +package gateway + +import ( + "fmt" + "net" + "runtime" +) + +func DiscoverGateway() (ip net.IP, iface string, err error) { + err = fmt.Errorf("DiscoverGateway not implemented for OS %s", runtime.GOOS) + return +} diff --git a/net/gateway/gateway_windows.go b/net/gateway/gateway_windows.go new file mode 100644 index 0000000..685f2c2 --- /dev/null +++ b/net/gateway/gateway_windows.go @@ -0,0 +1,16 @@ +package gateway + +import ( + "net" + "os/exec" +) + +func DiscoverGateway() (ip net.IP, iface string, err error) { + routeCmd := exec.Command("route", "print", "0.0.0.0") + output, err := routeCmd.CombinedOutput() + if err != nil { + return nil, "", err + } + + return parseWindowsRoutePrint(output) +}