最近在做一个工具,估计很快就能和大家见面了。
这是我第一次用 Go 写 CLI 工具,功能意外的还蛮多蛮复杂,其中涉及到一些 Linux 运维的工作,包括对网络接口进行操作,对 IPtables 的维护等。
不可避免的,这个过程中需要调用 Syscall,Go 的 Syscall 不像 C 那样直接,虽然 Go 提供了 golang.org/x/sys/unix 包,其中打包了一些常用的 Syscall,但就我们的案例而言需要去调用裸 Syscall,因此研究了一下 Go 调用 Syscall 的正确打开方式。
案例分析
在这个需求中,我需要创建一个 Tun 接口,尽管有一些第三方库包装了 Tun/Tap 接口,但就当前的早期版本而言,我需要让程序创建一个持久化的 Tun 接口并退出,只能自己实现 Syscall 包装。
首先,先来看对于 C 语言实现的 Tun 接口创建过程:
1#include <sys/types.h>
2#include <linux/if.h>
3#include <linux/if_tun.h>
4#include <fcntl.h>
5#include <errno.h>
6#define TUNDEV "/dev/net/tun"
7
8static int tap_add_ioctl(struct ifreq *ifr, uid_t uid, gid_t gid)
9{
10 int fd;
11 int ret = -1;
12
13#ifdef IFF_TUN_EXCL
14 ifr->ifr_flags |= IFF_TUN_EXCL;
15#endif
16
17 fd = open(TUNDEV, O_RDWR);
18 if (fd < 0) {
19 perror("open");
20 return -1;
21 }
22 if (ioctl(fd, TUNSETIFF, ifr)) {
23 perror("ioctl(TUNSETIFF)");
24 goto out;
25 }
26 if (uid != -1 && ioctl(fd, TUNSETOWNER, uid)) {
27 perror("ioctl(TUNSETOWNER)");
28 goto out;
29 }
30 if (gid != -1 && ioctl(fd, TUNSETGROUP, gid)) {
31 perror("ioctl(TUNSETGROUP)");
32 goto out;
33 }
34 if (ioctl(fd, TUNSETPERSIST, 1)) {
35 perror("ioctl(TUNSETPERSIST)");
36 goto out;
37 }
38 ret = 0;
39out:
40 close(fd);
41 return ret;
42}
这一段代码截取自 iproute2/ip/iptuntap.c,他做了如下几件事情:
- 打开
/dev/net/tun
设备 - 对 Tun 设备调用 ioctl() Syscall 设置参数,这个部分可以参考内核主线文档中的 TUN/TAP 设备,而对于 ifreq 这个结构体,则可以查看 netdevice(7) 文档
- 对创建的设备设置参数,如设备所有者,设备组,设备持久化等
- 关闭文件
删除一个持久化的设备只需要将持久化位重新置为 0 即可。
Go 调用 Syscall
Go 标准库中有 syscall 库,但文档有这样一句话:
The syscall package is locked down. Callers should use the corresponding package in the golang.org/x/sys repository instead. That is also where updates required by new systems or versions should be applied. See https://golang.org/s/go1.4-syscall for more information.
即,syscall 标准库不被鼓励使用。
这样一来,按照他的说法,可以使用新的 golang.org/x/sys 库,这个库包装了不同系统的 Syscall,我们需要使用的是 Unix 的版本。
这个库包装了许多 Syscall 可以不需要使用 unix.Syscall()
直接调用,虽然没有提供 Tun/Tap 接口相关的包装,但我们可以最大化利用他已有的方法。
这样一来我们得到了如下的代码:
1package main
2
3import (
4 "fmt"
5 "os"
6 "syscall"
7 "unsafe"
8
9 "golang.org/x/sys/unix"
10)
11
12func NewTunDevice(n string) error {
13 f, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
14 if err != nil {
15 return err
16 }
17 defer f.Close()
18
19 // Create a new TUN device
20 ifr, err := unix.NewIfreq(n)
21 if err != nil {
22 return err
23 }
24
25 // ifr.flags = IFF_TUN
26 ifr.SetUint16(unix.IFF_TUN)
27
28 _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(ifr)))
29 if errno != 0 {
30 return fmt.Errorf(errno.Error())
31 }
32 // Set Tun Interface Persistent
33 _, _, errno = unix.Syscall(unix.SYS_IOCTL, f.Fd(), uintptr(unix.TUNSETPERSIST), uintptr(1))
34 if errno != 0 {
35 return fmt.Errorf(errno.Error())
36 }
37 return nil
38}
这段代码的功能是显而易见的。
ifreq 结构体的第二个元素是一个 union,ifr_flags 元素是一个 Short 的变量,因此调用 ifr.SetUint16() 方法设置 ifr_flags 标志位。
对于这个 Syscall 调用,ioctl 没有有意义的返回值,因此直接忽略,只取 errno 即可。
unix.Syscall() 的 errno 返回一个 syscall.Errno 类型的值,他实际上是一个 uintptr,但我们可以直接用它和 0 比较,此处和 C 风格错误处理一致。
这个重新定义的 syscall.Errno 提供了 Error() 方法,他返回一个字符串,可用于 fmt.Errorf() 用来返回 Go 风格的 error。
对 Tun/Tap 的 Syscall 需要 CAP_NET_ADMIN 权限,因此需要增加权限或以 root 身份运行程序。
参考
https://pkg.go.dev/golang.org/x/[email protected]/unix
https://www.kernel.org/doc/html/v5.8/networking/tuntap.html
https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/iptuntap.c
https://github.com/getlantern/tuntap/blob/1cde8300bb3754e63386e8c79b544b9e237a79d8/tun_linux.go#L9
评论