Bind端口限制

root用户运行的程序能bind的端口是有限制的。在macOS Sierra上,1~1023这些端口,非root用户在bind的时候,会得到Permission denied的报错。

背景

有同事在群里面问,说macOS环境下想在Intellij IDEA里启动DNS server来模拟一些场景,但是绑定53端口时会报没有权限的错误。最后通过使用root用户来启动Intellij IDEA绕过了这个问题。

引申问题

上面这个case可以引申到另外几个问题:

  • 受限制的端口在什么范围
  • 这个范围是由什么参数决定的
  • 有什么方法能优雅地修改这个范围

背景中提到的现象,在Linux上也是同样如此,后面对于提到的3个引申问题的解释都是基于linux kernel 3.16.39版本。

端口范围

macOS SierraLinux上,都可以明确地看到1~1023这些端口是无法绑定的。绑定端口0是比较特殊的,会返回一个随机的ephemeral port,可以调用getsockname获取成功绑定的port。

决定范围的参数

查看kernel源码,文件net/ipv4/af_inet.c中有函数inet_bind的定义,里面有一段是:

1
2
3
4
5
477 snum = ntohs(addr->sin_port);
478 err = -EACCES;
479 if (snum && snum < PROT_SOCK &&
480 !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
481 goto out;

可以看到PROT_SOCK就是决定非root用户默认情况可以绑定的端口。

文件include/net/sock.h中有关于常量PROT_SOCK的声明:

1
2
1360 /* Sockets 0-1023 can't be bound to unless you are superuser */
1361 #define PROT_SOCK 1024

修改范围的优雅方法

可以使用setcap添加cap_net_bind_service到对应的可执行文件,使非root用户能够bind对应的端口:

1
2
3
➜ ~ sudo setcap 'cap_net_bind_service=+ep' test_bind
➜ ~ sudo getcap test_bind
test_bind = cap_net_bind_service+ep

命令中ep的含义可以在这篇文档中找到解释:

e: Effective
This means the capability is “activated”.

p: Permitted
This means the capability can be used/is allowed.

其它

上述引申问题的解析中用到了实验代码test_bind.c,内容如下:

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
#include <errno.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <string.h>
#include <zconf.h>
int main(int argc, char** argv) {
int start = 0, end = 20, portno;
for (portno = start; portno < end; portno++) {
int sockfd;
struct sockaddr_in serv_addr;
/* First call to socket() function */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
printf("ERROR opening socket");
close(sockfd);
continue;
}
/* Initialize socket structure */
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
/* Now bind the host address using bind() call.*/
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
printf("bind port %d err: %s ...\n", portno, strerror(errno));
} else {
printf("bind port %d ok ...\n", portno);
if (0 == portno) {
struct sockaddr_in real_addr;
socklen_t addr_len = sizeof(real_addr);
int ret = getsockname(sockfd, (struct sockaddr *) &real_addr, &addr_len);
if (ret) {
printf("getsockname err: %s ...\n", strerror(errno));
} else {
printf("real port: %d\n", ntohs(real_addr.sin_port));
}
}
}
close(sockfd);
}
return 0;
}

参考

[1] http://stackoverflow.com/questions/413807/is-there-a-way-for-non-root-processes-to-bind-to-privileged-ports-on-linux
[2] https://access.redhat.com/solutions/1453783
[3] https://linux.die.net/man/7/capabilities
[4] https://www.insecure.ws/linux/getcap_setcap.html
[5] https://www.tutorialspoint.com/unix_sockets/socket_server_example.htm