绑定特殊 IP 之 0.0.0.0 的内部工作原理
大家好,我是飞哥!
前段时间有位读者提了个问题,:“服务器端监听 0.0.0.0 的内部是咋样的?”
大家可能也在 nginx、redis 等 server 的配置文件中见过 bind 的时候不用真实的 IP,而使用 0.0.0.0 的情况。
我觉得这个问题提的很不错,弄懂这个实现过程很有利于大家理解 Linux 服务器在多网卡情况下的监听过程。所以专门来一篇文章解答一下。
这个 0.0.0.0 和 127.0.0.1 都是特殊 IP。为了方便本文展开叙述,咱们先列一段绑定 0.0.0.0 的 c 语言 server 代码(只为了展示,不可运行)。
void main(){
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sinport = ...;
//绑定 ip 和端口
bind(fd, addr, ...);
//监听
listen(fd, ...);
}
其中 INADDR_ANY 是定义在 include/uapi/linux/in.h
文件下的,就是 0 IP 地址。
#define INADDR_ANY ((unsigned long int) 0x00000000)
一、bind 过程
我们来看一下 bind 的相关内部过程,它的核心是 inet_bind, 其源码位于 net/ipv4/af_inet.c
中。我们只看和今天问题相关的部分。
//file: net/ipv4/af_inet.c
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct sock *sk = sock->sk;
struct inet_sock *inet = inet_sk(sk);
...
//bind时 将 inet_rcv_saddr 和 inet_saddr 都设置为地址
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
//bind 时设置要使用的端口
inet->inet_sport = htons(inet->inet_num);
...
}
这个函数有两个重点参数,分别是 sock 和 uaddr。其中 sock 是我们刚创建出来的 socket 对象,uaddr 的值就是我们在自己的代码里传入的 addr 值。函数接下来的 inet 是获取了 socket 内核对象中的一部分。
在 inet_bind 的函数体中,将要绑定的 IP 地址 addr->sin_addr.s_addr( 0 ) 设置到了 socket 的 inet->inet_rcv_saddr 成员中,将要绑定的端口设置到了 inet->inet_sport 成员上。
接下来服务器在 listen 的时候会把当前 socket 添加到一个 listen 状态的 hash 表中,了解就行了。接下来咱们看当用户握手包到达的时候的处理过程。
二、响应握手请求
在收到来自客户端数据包的时候(包括握手请求),会进入到 tcp_v4_rcv 这个核心函数中。在这里会读取数据包的 tcp 头和 ip 头。其中在 tcp 头中有 ip 和端口的四元组。
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
th = tcp_hdr(skb);
iph = ip_hdr(skb);
//在这里查找正在监听的 socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
......
}
在 __inet_lookup_skb 这个函数内部会寻找服务器上处理该数据包的 socket。先查看是否有已经建立的连接,如果没有就寻找合适的 listen 状态的 socket,以进行握手。我们直接查看查找 listen socket 的 __inet_lookup_listener。
//file: net/ipv4/inet_hashtables.c
struct sock *__inet_lookup_listener(struct net *net,
struct inet_hashinfo *hashinfo,
const __be32 saddr, __be16 sport,
const __be32 daddr, const unsigned short hnum,
const int dif)
{
//根据端口计算 hash 值
unsigned int hash = inet_lhashfn(net, hnum);
//所有的 listen 都是存这个 hash 中
//根据 hash,并把所有可能的 listen 的 socket 链表找出来
struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
begin:
result = NULL;
hiscore = 0;
sk_nulls_for_each_rcu(sk, node, &ilb->head) {
score = compute_score(sk, net, hnum, daddr, dif);
if (score > hiscore) {
result = sk;
}
...
}
...
return result;
}
在 __inet_lookup_listener 中遍历同 hash 值的所有在监听的 socket。挨个计算匹配分数,并把匹配分最高的挑选出来,就是要握手的 socket 对象了。
我们重点来看下 compute_score,我们今天问题的答案就藏在它里面。在看源码之前先回忆一下,上面我们在 bind 地址为 INADDR_ANY 的时候,内核会把 listen socket 的 inet_rcv_saddr 设置为 0。来看源码:
//file: net/ipv4/inet_hashtables.c
static inline int compute_score(struct sock *sk, struct net *net,
const unsigned short hnum, const __be32 daddr,
const int dif)
{
//默认分数是负数
int score = -1;
struct inet_sock *inet = inet_sk(sk);
//只有网络命名空间和端口等都匹配才真正计算匹配分
if (net_eq(sock_net(sk), net) && inet->inet_num == hnum &&
!ipv6_only_sock(sk)) {
//inet socket 优先级高
score = sk->sk_family == PF_INET ? 2 : 1;
//注意!!!
//
__be32 rcv_saddr = inet->inet_rcv_saddr;
if (rcv_saddr) {
if (rcv_saddr != daddr)
return -1;
score += 4;
}
...
}
return score;
}
在计算匹配分的时候会判断 listen 状态的 socket 中 bind 时记录的 inet_rcv_saddr。如果它不为 0(bind 时指定了 IP),则数据包中的目的地址必须和它匹配才行。而如果为 0(bind 时设置 IP 是 INADDR_ANY, 亦即 0.0.0.0),则不会进行 IP 地址的比对就能计算出正的匹配分。
四、结论
可以用一句话来总结 0.0.0.0。如果一个服务是绑定到 0.0.0.0 ,那么外部机器访问该机器上所有 IP 都可以访问该服务。如果服务绑定到的是特定的 ip,则只有访问该 ip 才能访问到服务。
实现的原理也很简单,如果 bind 时绑定的是 0.0.0.0(INADDR_ANY),则内核在查找 listen 状态的 socket 的时候不进行目的地址匹配。反之,则必须要网络包中的目的地址和该 socket 上的 IP 匹配才能访问!