0%

Golang 调用 Linux Netlink 管理路由表

通常我们使用route命令或者ip命令来管理linux路由表(routing table), 但是如何使用linux原生的接口来进行操作呢?

netlink 是一种与内核交互的方式,首先创建一个netlink类型的socket,之后向这个socket发送指定格式的数据,就可以从该socket拿到内核的返回。
linux的route就是通过这种方式来进行管理的。
关于netlink的详细信息建议直接man 7 netlink,网上的信息通常都不够直观。下面节选部分文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
NETLINK(7) 

NAME
netlink - communication between kernel and user space (AF_NETLINK)

SYNOPSIS
#include <asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>

netlink_socket = socket(AF_NETLINK, socket_type, netlink_family);

DESCRIPTION
Netlink is used to transfer information between the kernel and user-space processes. It consists of a standard sockets-based interface for user space processes and an internal kernel API for
kernel modules. The internal kernel interface is not documented in this manual page. There is also an obsolete netlink interface via netlink character devices; this interface is not docu‐
mented here and is provided only for backward compatibility.

Netlink is a datagram-oriented service. Both SOCK_RAW and SOCK_DGRAM are valid values for socket_type. However, the netlink protocol does not distinguish between datagram and raw sockets.

netlink_family selects the kernel module or netlink group to communicate with. The currently assigned netlink families are:

NETLINK_ROUTE
Receives routing and link updates and may be used to modify the routing tables (both IPv4 and IPv6), IP addresses, link parameters, neighbor setups, queueing disciplines, traffic classes
and packet classifiers (see rtnetlink(7)).

NETLINK_W1 (Linux 2.6.13 to 2.16.17)
Messages from 1-wire subsystem.

NETLINK_USERSOCK
Reserved for user-mode socket protocols.

NETLINK_FIREWALL (up to and including Linux 3.4)
Transport IPv4 packets from netfilter to user space. Used by ip_queue kernel module. After a long period of being declared obsolete (in favor of the more advanced nfnetlink_queue fea‐
ture), NETLINK_FIREWALL was removed in Linux 3.5.

查询接口

goalng的syscall库中包装了部分函数帮助我们通过netlink完成查询工作。方便起见我们使用最为通用的syscall.NetlinkRIB函数,该函数会一次将所有table查询出来,无法指定,但是不需要我们手动设置查询结构体,同时帮我们完成了数据接收工作,使用起来较为方便。
syscall.NetlinkRIB的返回为[]byte,我们需要手动将该字节串转为结构体。在syscall中提供了syscall.ParseNetlinkMessage函数可以帮我们完成这一步操作。

1
2
3
4
5
6
7
8
tab, err := syscall.NetlinkRIB(syscall.RTM_GETROUTE, syscall.AF_INET)
if err != nil {
panic(err)
}
msgs, err := syscall.ParseNetlinkMessage(tab)
if err != nil {
panic(err)
}

msgs类型为[]NetlinkMessage每一个table中的路由都会被存储为一个NetlinkMessage结构体,接下来我们遍历msgs来取出所有table的路由。

1
2
3
4
5
6
7
8
9
10
for _, m := range msgs {
switch m.Header.Type {
case syscall.NLMSG_DONE:
fmt.Println("recv done")
goto done
case syscall.RTM_NEWROUTE:
// 解析数据
}
}
done:

对于每一个NetlinkMessage结构,我们需要先判断其类型,如果为NLMSG_DONE则表示数据结束,有人可能觉得数组遍历完了就好,为什么非要把最后一个元素设置为NLMSG_DONE,这其实也是socket传输结束的标志,NetlinkRIBParseNetlinkMessage只是将他原样解析了而已。

第二个需要注意的点是,我们使用syscall.RTM_GETROUTE标志get的到的路由表,其头类型为syscall.RTM_NEWROUTE而不是syscall.RTM_GETROUTE,其原因我还没有仔细查过。

路由 表属性 解析

当进入case syscall.RTM_NEWROUTE:分支后,我们就可以拿到一个路由表的具体数据了。首先需要使用unsafe指针将NetlinkMessage的data解析为RtMsg结构体。之后我们可以从rtmsg中解析出路由表的各项属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
rtmsg := (*syscall.RtMsg)(unsafe.Pointer(&m.Data[0]))

fmt.Print("Scope ")
switch rtmsg.Scope {
case syscall.RT_SCOPE_UNIVERSE:
fmt.Print("RT_SCOPE_UNIVERSE ")
case syscall.RT_SCOPE_SITE:
fmt.Print("RT_SCOPE_SITE ")
case syscall.RT_SCOPE_LINK:
fmt.Print("RT_SCOPE_LINK ")
case syscall.RT_SCOPE_HOST:
fmt.Print("RT_SCOPE_HOST ")
case syscall.RT_SCOPE_NOWHERE:
fmt.Print("RT_SCOPE_NOWHERE ")
}

fmt.Print(" Protocol ")
switch rtmsg.Protocol {
case syscall.RTPROT_UNSPEC:
fmt.Print("RTPROT_UNSPEC")
case syscall.RTPROT_REDIRECT:
fmt.Print("RTPROT_REDIRECT")
case syscall.RTPROT_KERNEL:
fmt.Print("RTPROT_KERNEL")
case syscall.RTPROT_BOOT:
fmt.Print("RTPROT_BOOT")
case syscall.RTPROT_STATIC:
fmt.Print("RTPROT_STATIC")
}

fmt.Print(" Type ")
switch rtmsg.Type {
case syscall.RTN_UNSPEC:
fmt.Print("RTN_UNSPEC")
case syscall.RTN_UNICAST:
fmt.Print("RTN_UNICAST")
case syscall.RTN_LOCAL:
fmt.Print("RTN_LOCAL")
case syscall.RTN_BROADCAST:
fmt.Print("RTN_BROADCAST")
case syscall.RTN_ANYCAST:
fmt.Print("RTN_ANYCAST")
case syscall.RTN_MULTICAST:
fmt.Print("RTN_MULTICAST")
case syscall.RTN_BLACKHOLE:
fmt.Print("RTN_BLACKHOLE")
case syscall.RTN_UNREACHABLE:
fmt.Print("RTN_UNREACHABLE")
case syscall.RTN_PROHIBIT:
fmt.Print("RTN_PROHIBIT")
case syscall.RTN_THROW:
fmt.Print("RTN_THROW")
case syscall.RTN_NAT:
fmt.Print("RTN_NAT")
case syscall.RTN_XRESOLVE:
fmt.Print("RTN_XRESOLVE")
}
fmt.Print(" Family ")
switch rtmsg.Family {
case syscall.AF_INET:
fmt.Print("AF_INET")
case syscall.AF_INET6:
fmt.Print("AF_INET6")
}

路由 解析

使用syscall.ParseNetlinkRouteAttr函数可以将NetlinkMessage结构体中的每一项路由属性解析出来。每一个attr既代表路由表中的一项路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
attrs, err := syscall.ParseNetlinkRouteAttr(&m)
if err != nil {
panic(err)
}
for _, attr := range attrs {
switch attr.Attr.Type {
case syscall.RTA_DST:
ip := net.IPv4(attr.Value[0], attr.Value[1], attr.Value[2], attr.Value[3])
fmt.Print("RTA_DST ", ip.String(), " ")
case syscall.RTA_SRC:
ip := net.IPv4(attr.Value[0], attr.Value[1], attr.Value[2], attr.Value[3])
fmt.Print("RTA_SRC ", ip.String(), " ")
case syscall.RTA_GATEWAY:
ip := net.IPv4(attr.Value[0], attr.Value[1], attr.Value[2], attr.Value[3])
fmt.Print("RTA_GATEWAY ", ip.String(), " ")
case syscall.RTA_PRIORITY:
priority := *(*int32)(unsafe.Pointer(&attr.Value[0]))
fmt.Print("RTA_PRIORITY ", priority, " ")
case syscall.RTA_TABLE:
table := *(*int32)(unsafe.Pointer(&attr.Value[0]))
fmt.Print("RTA_TABLE ", strconv.Itoa(table), " ")
case syscall.RTA_METRICS:
metrics := *(*int32)(unsafe.Pointer(&attr.Value[0]))
fmt.Print("RTA_METRICS ", metrics, " ")
case syscall.RTA_IIF:
iif := *(*int32)(unsafe.Pointer(&attr.Value[0]))
fmt.Print("RTA_IIF ", iif, " ")
case syscall.RTA_OIF:
oif := *(*int32)(unsafe.Pointer(&attr.Value[0]))
fmt.Print("RTA_OIF ", oif, " ")
}
}
fmt.Println("")
fmt.Println("============")
}

参考资料

  1. LINUX,NETLINK 和 GO – 第 1 部分:NETLINK
  2. Linux, Netlink, and Go - Part 1: netlink
  3. [https://beej-zhcn.netdpi.net/](Beej’s Guide to Network Programming)
  4. RFC3549