查看原文
其他

绑定特殊 IP 之 0.0.0.0 的内部工作原理

张彦飞allen 开发内功修炼 2022-06-14

大家好,我是飞哥!

前段时间有位读者提了个问题,:“服务器端监听 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 匹配才能访问!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存