From 1a8d05ad0f1f1bbd402d17dccd078116a4ec03d4 Mon Sep 17 00:00:00 2001 From: Will Scott Date: Thu, 5 Mar 2020 16:44:46 -0800 Subject: [PATCH] Partial build and structure of Router abstraction --- README.md | 25 ++++ go.mod | 9 ++ go.sum | 12 ++ netroute_darwin.go | 285 ++++++++++++++++++++++++++++++++++++++++++++ netroute_linux.go | 13 ++ netroute_test.go | 14 +++ netroute_windows.go | 146 +++++++++++++++++++++++ 7 files changed, 504 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 netroute_darwin.go create mode 100644 netroute_linux.go create mode 100644 netroute_test.go create mode 100644 netroute_windows.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..70ff4df --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Go Netroute +=== + +A cross-platform implementation of the `gopacket/routing.Router` interface. + +This library uses `gopacket` for linux, `x/net/route` +for mac, and `iphlpapi.dll` for windows. + +Usage +--- + +``` +import ( + netroute "github.com/willscott/go-netroute" +) + +func main() { + r, err := netroute.New() + if err != nil { + panic(err) + } + iface, gw, src, err := r.Route(net.IPv4(127, 0, 0, 1)) + fmt.Printf("%v, %v, %v, %v\n", iface, gw, src, err) +} +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1b1d922 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/willscott/go-netroute + +go 1.13 + +require ( + github.com/google/gopacket v1.1.17 + github.com/libp2p/go-sockaddr v0.0.1 // indirect + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..99fa000 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/libp2p/go-sockaddr v0.0.1 h1:yM3fy0n5oommws7FmCeOh+IoyEFpmlO77+2eXiaWIko= +github.com/libp2p/go-sockaddr v0.0.1/go.mod h1:EBeYKMYs5LFgoaBSN2nA2jPQVmnA4gv7WkY64CBqYqQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 h1:1Fzlr8kkDLQwqMP8GxrhptBLqZG/EDpiATneiZHY998= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/netroute_darwin.go b/netroute_darwin.go new file mode 100644 index 0000000..2ac0e10 --- /dev/null +++ b/netroute_darwin.go @@ -0,0 +1,285 @@ +// Copyright 2012 Google, Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +// +build darwin dragonfly freebsd netbsd openbsd + +// This is a copy of +// https://github.com/google/gopacket/blob/master/routing/routing.go +// but with RIB parsing following the route format described in +// https://github.com/freebsd/freebsd/blob/master/sys/net/route.h +package netroute + +import ( + "bytes" + "errors" + "fmt" + "net" + "sort" + "strings" + "syscall" + + "github.com/google/gopacket/routing" + "golang.org/x/net/route" +) + +// Pulled from http://man7.org/linux/man-pages/man7/rtnetlink.7.html +// See the section on RTM_NEWROUTE, specifically 'struct rtmsg'. +type routeInfoInMemory struct { + Family byte + DstLen byte + SrcLen byte + TOS byte + + Table byte + Protocol byte + Scope byte + Type byte + + Flags uint32 +} + +// rtInfo contains information on a single route. +type rtInfo struct { + Src, Dst *net.IPNet + Gateway, PrefSrc net.IP + // We currently ignore the InputIface. + InputIface, OutputIface uint32 + Priority uint32 +} + +// routeSlice implements sort.Interface to sort routes by Priority. +type routeSlice []*rtInfo + +func (r routeSlice) Len() int { + return len(r) +} +func (r routeSlice) Less(i, j int) bool { + return r[i].Priority < r[j].Priority +} +func (r routeSlice) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +type router struct { + ifaces []net.Interface + addrs []ipAddrs + v4, v6 routeSlice +} + +func (r *router) String() string { + strs := []string{"ROUTER", "--- V4 ---"} + for _, route := range r.v4 { + strs = append(strs, fmt.Sprintf("%+v", *route)) + } + strs = append(strs, "--- V6 ---") + for _, route := range r.v6 { + strs = append(strs, fmt.Sprintf("%+v", *route)) + } + return strings.Join(strs, "\n") +} + +type ipAddrs struct { + v4, v6 net.IP +} + +func (r *router) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { + return r.RouteWithSrc(nil, nil, dst) +} + +func (r *router) RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { + var ifaceIndex int + switch { + case dst.To4() != nil: + ifaceIndex, gateway, preferredSrc, err = r.route(r.v4, input, src, dst) + case dst.To16() != nil: + ifaceIndex, gateway, preferredSrc, err = r.route(r.v6, input, src, dst) + default: + err = errors.New("IP is not valid as IPv4 or IPv6") + return + } + if err != nil { + return + } + + // Interfaces are 1-indexed, but we store them in a 0-indexed array. + ifaceIndex-- + + iface = &r.ifaces[ifaceIndex] + if preferredSrc == nil { + switch { + case dst.To4() != nil: + preferredSrc = r.addrs[ifaceIndex].v4 + case dst.To16() != nil: + preferredSrc = r.addrs[ifaceIndex].v6 + } + } + return +} + +func (r *router) route(routes routeSlice, input net.HardwareAddr, src, dst net.IP) (iface int, gateway, preferredSrc net.IP, err error) { + var inputIndex uint32 + if input != nil { + for i, iface := range r.ifaces { + if bytes.Equal(input, iface.HardwareAddr) { + // Convert from zero- to one-indexed. + inputIndex = uint32(i + 1) + break + } + } + } + for _, rt := range routes { + if rt.InputIface != 0 && rt.InputIface != inputIndex { + continue + } + if src != nil && rt.Src != nil && !rt.Src.Contains(src) { + fmt.Printf("ignoring %v b/c src %v\n", rt, src) + continue + } + if rt.Dst != nil && !rt.Dst.Contains(dst) { + fmt.Printf("ignoring %v b/c dst\n", rt) + continue + } + fmt.Printf("Using route: %v\n", rt) + return int(rt.OutputIface), rt.Gateway, rt.PrefSrc, nil + } + err = fmt.Errorf("no route found for %v", dst) + return +} + +// Begin modifications + +func toIPAddr(a route.Addr) (net.IP, error) { + switch t := a.(type) { + case *route.Inet4Addr: + ip := net.IPv4(t.IP[0], t.IP[1], t.IP[2], t.IP[3]) + return ip, nil + case *route.Inet6Addr: + ip := make(net.IP, net.IPv6len) + copy(ip, t.IP[:]) + return ip, nil + default: + return net.IP{}, fmt.Errorf("unknown family: %T", t) + } +} + +// selected BSD Route flags. +const ( + RTF_UP = 0x1 + RTF_GATEWAY = 0x2 + RTF_HOST = 0x4 + RTF_REJECT = 0x8 + RTF_DYNAMIC = 0x10 + RTF_MODIFIED = 0x20 + RTF_STATIC = 0x800 + RTF_BLACKHOLE = 0x1000 + RTF_LOCAL = 0x200000 + RTF_BROADCAST = 0x400000 + RTF_MULTICAST = 0x800000 +) + +func New() (routing.Router, error) { + rtr := &router{} + tab, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0) + if err != nil { + return nil, err + } + msgs, err := route.ParseRIB(route.RIBTypeRoute, tab) + if err != nil { + return nil, err + } + var ipn *net.IPNet + for _, msg := range msgs { + m := msg.(*route.RouteMessage) + routeInfo := new(rtInfo) + + if m.Version < 3 || m.Version > 5 { + return nil, fmt.Errorf("Unexpected RIB message version: %d", m.Version) + } + if m.Type != 4 /* RTM_GET */ { + return nil, fmt.Errorf("Unexpected RIB message type: %d", m.Type) + } + + if m.Flags&RTF_UP == 0 || + m.Flags&(RTF_REJECT|RTF_BLACKHOLE) != 0 { + continue + } + if m.Err != nil { + continue + } + + dst, err := toIPAddr(m.Addrs[0]) + if err == nil { + mask, _ := toIPAddr(m.Addrs[2]) + if mask == nil { + mask = net.IP(net.CIDRMask(0, 8*len(dst))) + } + ipn = &net.IPNet{IP: dst, Mask: net.IPMask(mask)} + if m.Flags&RTF_HOST != 0 { + ipn.Mask = net.CIDRMask(8*len(ipn.IP), 8*len(ipn.IP)) + } + routeInfo.Dst = ipn + } else { + return nil, fmt.Errorf("Unexpected RIB destination: %v", err) + } + + if m.Flags&RTF_GATEWAY != 0 { + if gw, err := toIPAddr(m.Addrs[1]); err == nil { + routeInfo.Gateway = gw + } + } + if src, err := toIPAddr(m.Addrs[5]); err == nil { + ipn = &net.IPNet{IP: src, Mask: net.CIDRMask(8*len(src), 8*len(src))} + routeInfo.Src = ipn + routeInfo.PrefSrc = src + if m.Flags&0x2 != 0 /* RTF_GATEWAY */ { + routeInfo.Src.Mask = net.CIDRMask(0, 8*len(routeInfo.Src.IP)) + } + } else { + return nil, fmt.Errorf("Unexpected RIB src: %v", err) + } + routeInfo.OutputIface = uint32(m.Index) + + switch m.Addrs[0].(type) { + case *route.Inet4Addr: + rtr.v4 = append(rtr.v4, routeInfo) + case *route.Inet6Addr: + rtr.v6 = append(rtr.v6, routeInfo) + } + } + sort.Sort(rtr.v4) + sort.Sort(rtr.v6) + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + for i, iface := range ifaces { + if i != iface.Index-1 { + return nil, fmt.Errorf("out of order iface %d = %v", i, iface) + } + rtr.ifaces = append(rtr.ifaces, iface) + var addrs ipAddrs + ifaceAddrs, err := iface.Addrs() + if err != nil { + return nil, err + } + for _, addr := range ifaceAddrs { + if inet, ok := addr.(*net.IPNet); ok { + // Go has a nasty habit of giving you IPv4s as ::ffff:1.2.3.4 instead of 1.2.3.4. + // We want to use mapped v4 addresses as v4 preferred addresses, never as v6 + // preferred addresses. + if v4 := inet.IP.To4(); v4 != nil { + if addrs.v4 == nil { + addrs.v4 = v4 + } + } else if addrs.v6 == nil { + addrs.v6 = inet.IP + } + } + } + rtr.addrs = append(rtr.addrs, addrs) + } + return rtr, nil +} diff --git a/netroute_linux.go b/netroute_linux.go new file mode 100644 index 0000000..e75b752 --- /dev/null +++ b/netroute_linux.go @@ -0,0 +1,13 @@ +// +build linux + +package netroute + +import ( + "net" + + "github.com/google/gopacket/routing" +) + +func New() (routing.Router, error) { + return routing.New() +} diff --git a/netroute_test.go b/netroute_test.go new file mode 100644 index 0000000..c2831d7 --- /dev/null +++ b/netroute_test.go @@ -0,0 +1,14 @@ +package netroute + +import ( + "net" + "testing" +) + +func TestRoute(t *testing.T) { + _, gw, src, err := Route(net.IPv4(127, 0, 0, 1)) + if err != nil { + t.Fatal(err) + } + t.Logf("Default route is via %v from %v", gw, src) +} diff --git a/netroute_windows.go b/netroute_windows.go new file mode 100644 index 0000000..1574c71 --- /dev/null +++ b/netroute_windows.go @@ -0,0 +1,146 @@ +// +build windows + +package netroute + +// Reference: +// https://docs.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getbestroute2 +import ( + "encoding/binary" + "fmt" + "net" + "syscall" + "unsafe" + + "github.com/google/gopacket/routing" + sockaddrnet "github.com/libp2p/go-sockaddr/net" + "golang.org/x/sys/windows" +) + +var ( + modiphlpapi = syscall.NewLazyDLL("iphlpapi.dll") + + procGetBestRoute2 = modiphlpapi.NewProc("GetBestRoute2") +) + +type NetLUID uint64 + +type AddressPrefix struct { + windows.Sockaddr + PrefixLength byte +} + +type RouteProtocol uint32 // MIB_IPFORWARD_PROTO + +// https://docs.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2 +type mib_row2 struct { + luid NetLUID + index uint32 + destinationPrefix AddressPrefix + nextHop windows.Sockaddr + prefixLength byte + lifetime uint32 + preferredLIfetime uint32 + metric uint32 + protocol RouteProtocol + loopback byte + autoconfigured byte + publich byte + immortal byte + age uint32 + origin byte +} + +func callBestRoute(source, dest net.IP) (*mib_row2, net.IP, error) { + sourceAddr := sockaddrnet.IPAndZoneToSockaddr(source, "") + destAddr := sockaddrnet.IPAndZoneToSockaddr(dest, "") + bestRoute := make([]byte, 64) + var bestSource windows.RawSockaddrAny + + err := getBestRoute2(nil, 0, source, dest, 0, bestRoute, bestSource) + if err != nil { + return nil, nil, err + } + + // interpret best route and best source. + route, err := parseRoute(bestRoute) + if err != nil { + return nil, nil, err + } + bestSrc, _ := sockaddrnet.SockaddrToIPAndZone(bestSource.Sockaddr()) + + return &route, bestSrc, nil +} + +func parseRoute(mib []byte) (*mib_row2, error) { + var route mib_row2 + var err error + + route.luid = binary.LittleEndian.Uint64(mib[0:]) + route.index = binary.LittleEndian.Uint32(mib[8:]) + pfix, idx, err := readDestPrefix(mib, 12) + if err != nil { + return nil, err + } + route.destinationPrefix = pfix + route.nextHop, idx, err = readSockAddr(mib, idx) + if err != nil { + return nil, err + } + + return route, err +} + +func readDestPrefix(buffer []byte, idx int) (*AddressPrefix, int, error) { + sock, idx, err := readSockAddr(buffer, idx) + if err != nil { + return nil, 0, err + } + pfixLen := buffer[idx] + return &AddressPrefix{sock, pfixLen}, idx + 1, nil +} + +func readSockAddr(buffer []byte, idx int) (*windows.Sockaddr, int, error) { + family := binary.LittleEndian.Uint16(buffer[idx:]) + if family == AF_INET { + //14 bytes? + } else if family == AF_INET6 { + //24 bytes? + } else { + return nil, 0, fmt.Errorf("Unknown windows addr family %d", family) + } +} + +func getBestRoute2(interfaceLuid *NetLUID, interfaceIndex uint32, sourceAddress, destinationAddress []byte, addressSortOptions uint32, bestRoute []byte, bestSourceAddress []byte) (errcode error) { + r0, _, _ := syscall.Syscall9(procGetBestRoute2.Addr(), 7, + uintptr(unsafe.Pointer(interfaceLuid)), + uintptr(interfaceIndex), + uintptr(unsafe.Pointer(&sourceAddress[0])), + uintptr(unsafe.Pointer(&destinationAddress[0])), + uintptr(addressSortOptions), + uintptr(unsafe.Pointer(&bestRoute[0])), + uintptr(unsafe.Pointer(&bestSourceAddress[0])), + 0, 0) + if r0 != 0 { + errcode = syscall.Errno(r0) + } + return +} + +type winRouter struct{} + +func (r *winRouter) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { + return RouteWithSource(nil, nil, dst) +} + +func (r *winRouter) RouteWithSrc(input net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) { + route, pref, err := callBestRoute(src, dst) + if err != nil { + return nil, nil, err + } + return route, pref, nil +} + +func New() (routing.Router, error) { + rtr := &winRouter{} + return rtr, nil +}