不用 root 权限实现 ping 的三种方式

这篇文章只针对 linux 系统,其他操作系统我没有仔细调查过就不管了。

很多人在刚开始学习网络编程时都会写一个 ping 程序当作练习,然后发现发送 ICMP 包时需要 SOCK_RAW,以及使用 SOCK_RAW 的程序需要 root 权限才能执行。接下来就会进一步产生疑问:为什么我写的 ping 就需要 root,系统里自带的那个(通常指 iputils 包提供的 ping 程序)就不用呢?

问题的答案其实很容易找到,但我发现很多回答都比较旧了,回答了以前常用的两种方式setuidcapabilities,经过尝试发现在我电脑上的表现跟他们说的完全不一样,感到非常迷惑。又找了找才发现现在新一些的发行版通常会采用另一种方式。

这个过程感觉挺有意思的,于是就想把这几种方式的原理都总结一下,水一篇博客。如果嫌麻烦可以直接看回答了我的疑问的这个 stackoverflow 回答

为什么需要 root 权限

首先,为什么用 SOCK_RAW 时规定需要 root 权限呢?

我的理解是 SOCK_RAW 意味着可以绕过系统的协议栈来直接操作IP层到应用层之间的各层,也就是可以监听到其他应用的包,还可以构造虚假的包进行欺骗,因此为了安全需要额外的权限加以限制。

三种常用方法

在这个问题上,不同的 linux 发行版可能会采用不同的方法,虽然都能达到同样的目的,但在原理和安全性上是有区别的。

setuid

setuid 是文件权限里一个特殊的权限,作用是允许执行程序的用户以程序所有者的权限去执行。

给 ping 程序加上这个权限,由于文件所有者是 root,即使普通用户执行 ping,也能够以 root 用户的权限去创建 SOCK_RAW,这就解决了问题。

如果执行 ls -l /bin/ping 看到类似 -rwsr-xr-x 1 root root 这样的输出(注意那个s),说明你的发行版是用这种方法实现的,如 Ubuntu 18.04。

实际上 setuid 存在着安全隐患,比如我们在运行 ping 的时候其实只需要一部分网络上的权限,而 setuid 直接把 root 用户的全部权限给了使用者。

capabilities

自版本 2.2 开始,linux 内核引入了叫 capabilities 的机制,把原来属于超级用户的所有的权限细分成不同的单元,每个单元可以独立开启和关闭。通过 capabilities 可以只授权给程序特定的权限,缓解了 setuid 存在的安全隐患。

与 ping 相关的权限名为 CAP_NET_RAW,即允许程序使用 原始套接字 SOCK_RAW

如果执行 getcap /bin/ping 看到类似 /bin/ping = cap_net_raw+ep 的输出,说明你的发行版是用这种方法实现的。

ping_group_range

ping_group_range 是一个内核参数,since Linux 2.6.39,可以看成专门为了 ping 设置的,相比上一种方法权限更加细化了。

sysctl 文档说:

ping_group_range - 2 INTEGERS
Restrict ICMP_PROTO datagram sockets to users in the group range. The default is “1 0”, meaning, that nobody (not even root) may create ping sockets. Setting it to “100 100” would grant permissions to the single group. “0 4294967295” would enable it for the world, “100 4294967295” would enable it for the users, but not daemons.

意思是说设置后这个用户组范围内的用户就可以用 SOCK_DGRAM 来处理 ICMP 包了,不再需要 SOCK_RAW,因此也就不需要什么 root 权限了。

如果执行 sysctl net.ipv4.ping_group_range 看到类似 net.ipv4.ping_group_range = 0 2147483647的输出,说明你的发行版动过这个内核选项,用这种方式实现,如我现在用的 Manjaro。

其实 Ubuntu 20.04 也设置了这个内核选项,但是getcap /bin/ping 却同时也有输出,这是让我一开始感到迷惑的原因,因为 Manjaro 的这条命令输出是空。关于这个问题的解答是这个要看发行版在 build iputils 的时候是怎么设置的。既然能用第三种方法,那么肯定还是不去用前两种更加安全。

iputils 的实现

说了这么多,其实可以发现要实现 ping without root 需要应用和操作系统相互配合,那么 iputils 里的 ping 又是怎么做的呢?

简单搜索 commit 记录后发现 2015 年的一个 commit 之后开始支持第三种方式,优先尝试用SOCK_DGRAM创建 socket,如果创建失败再根据内核是否支持 capabilities来决定是否能申请CAP_NET_RAW权限。如果不支持capabilities的话就只能回退到第一种方法。

可见 ping 程序是这三种方法都能兼容的,优先采用更安全的,实际运行用的哪种方式要看各个发行版怎么设置。

See Also

总结

这个问题算是一个比较古老的问题,最新的解决方法用的内核版本也是很久之前的 2.6 了。虽然不是什么很重要的问题,但想要搞清楚原理我还是查了好一阵子。主要原因是我在使用 linux 时还是比较浮于表面,没有专门研究过更加底层的一些设计,这对于计算机行业的人来说还是有帮助的。此外,通过搜索引擎迅速了解一个话题的有关信息并保证信息的正确性,对于任何一个现代人来说都是很需要的技能,这块我还差很远。