綁定特殊 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