Skip to content

Commit 53db2cc

Browse files
Query the RouteSocket for routes on Unix systems
Mimic what `route get` does, instead of dumping the routing table and trying to determine the most specific route from the dump.
1 parent 906f19b commit 53db2cc

File tree

5 files changed

+304
-102
lines changed

5 files changed

+304
-102
lines changed

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ require (
99
golang.org/x/net v0.37.0
1010
golang.org/x/sys v0.31.0
1111
)
12+
13+
require (
14+
github.com/davecgh/go-spew v1.1.1 // indirect
15+
github.com/pmezard/go-difflib v1.0.0 // indirect
16+
github.com/stretchr/testify v1.10.0 // indirect
17+
gopkg.in/yaml.v3 v3.0.1 // indirect
18+
)

go.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
24
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
8+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
39
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
410
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
511
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -20,3 +26,6 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
2026
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
2127
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
2228
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
29+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
30+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
31+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

netroute_bsd.go

Lines changed: 178 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -13,137 +13,220 @@
1313
package netroute
1414

1515
import (
16+
"errors"
1617
"fmt"
18+
"math/rand/v2"
1719
"net"
18-
"sort"
20+
"os"
1921
"syscall"
2022

2123
"github.com/google/gopacket/routing"
2224
"golang.org/x/net/route"
25+
"golang.org/x/sys/unix"
2326
)
2427

25-
func toIPAddr(a route.Addr) (net.IP, error) {
28+
const (
29+
RTF_IFSCOPE = 0x1000000
30+
)
31+
32+
type bsdRouter struct{}
33+
34+
func toIPAddr(a route.Addr) net.IP {
2635
switch t := a.(type) {
2736
case *route.Inet4Addr:
28-
return t.IP[:], nil
37+
return t.IP[:]
2938
case *route.Inet6Addr:
30-
return t.IP[:], nil
39+
return t.IP[:]
3140
default:
32-
return net.IP{}, fmt.Errorf("unknown family: %v", t)
41+
return nil
3342
}
3443
}
3544

36-
// selected BSD Route flags.
37-
const (
38-
RTF_UP = 0x1
39-
RTF_GATEWAY = 0x2
40-
RTF_HOST = 0x4
41-
RTF_REJECT = 0x8
42-
RTF_DYNAMIC = 0x10
43-
RTF_MODIFIED = 0x20
44-
RTF_STATIC = 0x800
45-
RTF_BLACKHOLE = 0x1000
46-
RTF_LOCAL = 0x200000
47-
RTF_BROADCAST = 0x400000
48-
RTF_MULTICAST = 0x800000
49-
)
45+
// toRouteAddr converts the net.IP to route.Addr .
46+
// If ip is not an IPv4 address, toRouteAddr returns nil.
47+
func toRouteAddr(ip net.IP) route.Addr {
48+
if len(ip) == 0 {
49+
return nil
50+
}
5051

51-
func New() (routing.Router, error) {
52-
rtr := &router{}
53-
rtr.ifaces = make(map[int]net.Interface)
54-
rtr.addrs = make(map[int]ipAddrs)
55-
tab, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0)
56-
if err != nil {
57-
return nil, err
52+
if len(ip) != net.IPv4len && len(ip) != net.IPv6len {
53+
return nil
5854
}
59-
msgs, err := route.ParseRIB(route.RIBTypeRoute, tab)
60-
if err != nil {
61-
return nil, err
55+
if p4 := ip.To4(); len(p4) == net.IPv4len {
56+
return &route.Inet4Addr{IP: [4]byte(p4)}
6257
}
63-
var ipn *net.IPNet
64-
for _, msg := range msgs {
65-
m := msg.(*route.RouteMessage)
66-
// We ignore the error (m.Err) here. It's not clear what this error actually means,
67-
// and it makes us miss routes that _should_ be included.
68-
routeInfo := new(rtInfo)
69-
70-
if m.Version < 3 || m.Version > 5 {
71-
return nil, fmt.Errorf("unexpected RIB message version: %d", m.Version)
72-
}
73-
if m.Type != 4 /* RTM_GET */ {
74-
return nil, fmt.Errorf("unexpected RIB message type: %d", m.Type)
75-
}
58+
return &route.Inet6Addr{IP: [16]byte(ip)}
59+
}
7660

77-
if m.Flags&RTF_UP == 0 ||
78-
m.Flags&(RTF_REJECT|RTF_BLACKHOLE) != 0 {
61+
// IPToIfIndex takes an IP and returns index of the interface with the given IP assigned if any
62+
func IPToIfIndex(ip net.IP) (int, error) {
63+
ifaces, err := net.Interfaces()
64+
if err != nil {
65+
return -1, fmt.Errorf("failed to get interfaces: %s", err)
66+
}
67+
for _, iface := range ifaces {
68+
addrs, err := iface.Addrs()
69+
if err != nil {
7970
continue
8071
}
81-
82-
dst, err := toIPAddr(m.Addrs[0])
83-
if err == nil {
84-
mask, _ := toIPAddr(m.Addrs[2])
85-
if mask == nil {
86-
mask = net.IP(net.CIDRMask(0, 8*len(dst)))
72+
for _, addr := range addrs {
73+
inet, ok := addr.(*net.IPNet)
74+
if !ok {
75+
continue
8776
}
88-
ipn = &net.IPNet{IP: dst, Mask: net.IPMask(mask)}
89-
if m.Flags&RTF_HOST != 0 {
90-
ipn.Mask = net.CIDRMask(8*len(ipn.IP), 8*len(ipn.IP))
77+
if inet.IP.Equal(ip) {
78+
return iface.Index, nil
9179
}
92-
routeInfo.Dst = ipn
93-
} else {
94-
return nil, fmt.Errorf("unexpected RIB destination: %v", err)
9580
}
81+
}
82+
return -1, fmt.Errorf("no interface found for IP: %s", ip)
83+
}
9684

97-
if m.Flags&RTF_GATEWAY != 0 {
98-
if gw, err := toIPAddr(m.Addrs[1]); err == nil {
99-
routeInfo.Gateway = gw
100-
}
85+
// MACToIfIndex takes a MAC address and returns index of interface with matching address
86+
func MACToIfIndex(hwAddr net.HardwareAddr) (int, error) {
87+
ifaces, err := net.Interfaces()
88+
if err != nil {
89+
return -1, fmt.Errorf("failed to get interfaces: %s", err)
90+
}
91+
for _, iface := range ifaces {
92+
if hwAddr.String() == iface.HardwareAddr.String() {
93+
return iface.Index, nil
10194
}
102-
if src, err := toIPAddr(m.Addrs[5]); err == nil {
103-
ipn = &net.IPNet{IP: src, Mask: net.CIDRMask(8*len(src), 8*len(src))}
104-
routeInfo.Src = ipn
105-
routeInfo.PrefSrc = src
106-
if m.Flags&0x2 != 0 /* RTF_GATEWAY */ {
107-
routeInfo.Src.Mask = net.CIDRMask(0, 8*len(routeInfo.Src.IP))
108-
}
95+
}
96+
return -1, fmt.Errorf("no interface found for MAC: %s", hwAddr.String())
97+
}
98+
99+
func getIfIndex(MACAddr net.HardwareAddr, ip net.IP) (int, error) {
100+
ipIndex := -1
101+
macIndex := -1
102+
var err error
103+
if ip == nil && MACAddr == nil {
104+
return -1, nil
105+
}
106+
107+
if ip != nil {
108+
ipIndex, err = IPToIfIndex(ip)
109+
if err != nil {
110+
return -1, fmt.Errorf("failed to find interface with IP: %s", ip.String())
109111
}
110-
routeInfo.OutputIface = uint32(m.Index)
112+
}
111113

112-
switch m.Addrs[0].(type) {
113-
case *route.Inet4Addr:
114-
rtr.v4 = append(rtr.v4, routeInfo)
115-
case *route.Inet6Addr:
116-
rtr.v6 = append(rtr.v6, routeInfo)
114+
if MACAddr != nil {
115+
macIndex, err = MACToIfIndex(MACAddr)
116+
if err != nil {
117+
return -1, fmt.Errorf("failed to find interface with MAC: %s", MACAddr.String())
117118
}
118119
}
119-
sort.Sort(rtr.v4)
120-
sort.Sort(rtr.v6)
121-
ifaces, err := net.Interfaces()
120+
121+
switch {
122+
case (ipIndex >= 0 && macIndex >= 0) && (macIndex != ipIndex):
123+
return -1, fmt.Errorf("given MAC address and source IP do resolve to same Interface")
124+
case (ipIndex >= 0 && macIndex >= 0) && (macIndex == ipIndex):
125+
return ipIndex, nil
126+
case ipIndex >= 0:
127+
return ipIndex, nil
128+
case macIndex >= 0:
129+
return macIndex, nil
130+
default:
131+
return -1, fmt.Errorf("no index found for given ip and/or mac")
132+
}
133+
}
134+
135+
136+
func (r *bsdRouter) Route(dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) {
137+
return r.RouteWithSrc(nil, nil, dst)
138+
}
139+
140+
func (r *bsdRouter) RouteWithSrc(MACAddr net.HardwareAddr, src, dst net.IP) (iface *net.Interface, gateway, preferredSrc net.IP, err error) {
141+
dstAddr := toRouteAddr(dst)
142+
if dstAddr == nil {
143+
return nil, nil, nil, fmt.Errorf("failed to parse dst: %#v", dst)
144+
}
145+
146+
pid := os.Getpid()
147+
seq := rand.Int()
148+
msg := &route.RouteMessage{
149+
Version: syscall.RTM_VERSION,
150+
Type: unix.RTM_GET,
151+
ID: uintptr(pid),
152+
Seq: seq,
153+
Addrs: []route.Addr{
154+
dstAddr,
155+
nil,
156+
nil,
157+
nil,
158+
&route.LinkAddr{},
159+
},
160+
}
161+
ifIndex, err := getIfIndex(MACAddr, src)
162+
if err != nil {
163+
return nil, nil, nil, fmt.Errorf("failed to determine ifIndex: %s", err)
164+
}
165+
if ifIndex >= 0 {
166+
msg.Flags = RTF_IFSCOPE
167+
msg.Index = ifIndex
168+
}
169+
170+
var reply *route.RouteMessage
171+
if reply, err = GetRouteMsgAnswer(msg); err != nil {
172+
return
173+
}
174+
if iface, err = net.InterfaceByIndex(reply.Index); err != nil {
175+
return
176+
}
177+
preferredSrc = toIPAddr(reply.Addrs[5])
178+
if dst.String() == preferredSrc.String() {
179+
return
180+
}
181+
gateway = toIPAddr(reply.Addrs[1])
182+
return
183+
}
184+
185+
// GetRouteMsgAnswer takes an RTM_GET RouteMessage and returns a answer (RouteMessage)
186+
func GetRouteMsgAnswer(m *route.RouteMessage) (*route.RouteMessage, error) {
187+
if m.Type != syscall.RTM_GET {
188+
return nil, errors.New("message type is not RTM_GET")
189+
}
190+
so, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
122191
if err != nil {
123192
return nil, err
124193
}
125-
for _, iface := range ifaces {
126-
rtr.ifaces[iface.Index] = iface
127-
var addrs ipAddrs
128-
ifaceAddrs, err := iface.Addrs()
194+
defer unix.Close(so)
195+
196+
wb, err := m.Marshal()
197+
if err != nil {
198+
return nil, err
199+
}
200+
if _, err := unix.Write(so, wb); err != nil {
201+
return nil, err
202+
}
203+
204+
var rb [2 << 10]byte
205+
for {
206+
n, err := unix.Read(so, rb[:])
129207
if err != nil {
130-
return nil, err
208+
return nil, fmt.Errorf("failed to read from routing socket: %s", err)
131209
}
132-
for _, addr := range ifaceAddrs {
133-
if inet, ok := addr.(*net.IPNet); ok {
134-
// Go has a nasty habit of giving you IPv4s as ::ffff:1.2.3.4 instead of 1.2.3.4.
135-
// We want to use mapped v4 addresses as v4 preferred addresses, never as v6
136-
// preferred addresses.
137-
if v4 := inet.IP.To4(); v4 != nil {
138-
if addrs.v4 == nil {
139-
addrs.v4 = v4
140-
}
141-
} else if addrs.v6 == nil {
142-
addrs.v6 = inet.IP
143-
}
144-
}
210+
// Parse the response messages
211+
rms, err := route.ParseRIB(route.RIBTypeRoute, rb[:n])
212+
if err != nil {
213+
return nil, fmt.Errorf("failed to parsed messages: %s", err)
145214
}
146-
rtr.addrs[iface.Index] = addrs
215+
if len(rms) == 0 {
216+
break
217+
}
218+
rm, ok := rms[0].(*route.RouteMessage)
219+
// confirm it is a reply to our query
220+
if !ok || (m.ID != rm.ID && m.Seq != rm.Seq && rm.Type != unix.RTM_GET) {
221+
continue
222+
}
223+
return rm, nil
147224
}
225+
return nil, errors.New("failed to read from routing socket")
226+
}
227+
228+
// New return a stateless routing router
229+
func New() (routing.Router, error) {
230+
rtr := &bsdRouter{}
148231
return rtr, nil
149232
}

0 commit comments

Comments
 (0)