綁定特殊 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 匹配才能訪問!

作者:allen

來源:開發內功修煉

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UmiIOQqmFNtYPBzAxa-ykg