為什麼我的 Nodejs 的http 服務接收到的IP地址前面會有::ffff:?

potatso發表於2024-07-10

Hello World

今天介紹一個比較繞口的技術。

故事的首先要從測試同學提的一個 BUG 開始

img

為什麼一個ipv4 地址前面會有::ffff:呢?是不是你的程式寫錯了呢。那我們來深究一下這個是什麼東西。

這種地址叫 ipv4 mapped ipv6。為什麼會有這麼奇怪的東西呢,與 ipv6 部署有關。

我們知道,ipb6 的地址空間範圍非常大。ipv6 改動不光涉及到網路層,例如配置 ipv6 的路由協議,還有應用層的調整,例如程式從 ipv4 監聽改成 ipv6 監聽。但是這個改動的成本巨大,又要涉及到程式碼的調整,又要涉及到地址處理的問題,例如是否監聽多協議等等等,於是有人發明了 ipv6 dual stack mode 技術。通俗來講就是

  • 程式只需要監聽 ipv6,如果不做特殊設定(IPV6_ONLY) 那麼socket 同時附帶同時也監聽 ipv4。並且 ipv4 接受到的請求,自動轉換為 ipv6 的格式交給應用層處理

下面我們寫一個python 的小程式來看一下

import socket
import threading

def create_socket():
    # 建立一個可以同時支援 IPv4 和 IPv6 的套接字
    sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
    
    # 設定地址重用選項,允許快速重啟伺服器
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 設定 IPv6 only 選項為 False,允許 IPv4 客戶端連線
    if hasattr(socket, 'IPV6_V6ONLY'):
        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False)
    
    return sock

def start_server():
    addr = ("", 8080)  # 監聽所有網路介面的 8080 埠
    
    # 嘗試建立支援雙棧 IPv6 的套接字
    if socket.has_dualstack_ipv6():
        sock = socket.create_server(addr, family = socket.AF_INET6, dualstack_ipv6 = True)
    else:
        # 如果不支援雙棧 IPv6,則建立普通的 IPv6 套接字
        sock = socket.create_server(addr)
    
    sock.listen(5)
    print("Server listening on 0.0.0.0:8080 (IPv4 and IPv6)")
    
    while True:
        conn, addr = sock.accept()
        threading.Thread(target = handle_client, args = (conn, addr)).start()

def handle_client(conn, addr):
    print(f"New connection from {addr[0]}:{addr[1]}")
    
    try:
        while True:
            data = conn.recv(1024)
            if not data:
                break
            print(f"Received: {data.decode()}")
            conn.sendall(data)  # 回顯收到的資料
    except Exception as e:
        print(f"Error handling client: {e}")
    finally:
        conn.close()
        print(f"Connection closed with {addr[0]}:{addr[1]}")

if __name__ == "__main__":
    start_server()

我們透過 nc 使用 ipv4 協議連結 python 的服務端,可以發現正常觸發了ffff::

img

img

並且這種監聽會被核心socket 標記為 tcp 46。當然,這裡還有一個重要區別,我們以 linux 為例,AP_ENABLE_V4_MAPPED 。這個引數決定了雙棧模式下,收到的 ipv4 地址是否翻譯為 ipv6 地址去展示。

但是這種技術不知為何鮮為人知,導致測試的同學和絕大多數研發都不知道,以為是 BUG。那我們怎麼解決呢?

  1. 關閉ipv6 雙棧

  2. 只監聽 ipv6

  3. 在 Linux 中,預設情況下,AP_ENABLE_V4_MAPPED 是 1,那麼 httpd 就會直接監聽 ipv6, 因為此時 ipv6 的 socket 能夠處理 ipv4 的請求;另外,bind() 系統呼叫會對使用者空間的程序透明處理 ipv6 沒有開啟的情況,此時會監聽到 ipv4。

    而如果我們在編譯 httpd 的時候使用 --disable-v4-mapped 引數禁止 ipv4 mapped,那麼預設情況下, httpd 會分別監聽在 ipv4 和 ipv6,而非只監聽 ipv6

回到我們的業務程式碼,我們的業務是 Nodejs 開發。在 Nodejs 的 Issues 中,很多人都提到了 ipv6 dual stack 很容易誤導人,強烈要求社群關閉該功能。

img

Nodejs 的 socket 監聽再沒有指定協議的情況下,會使用 Ipv6 dual stack 監聽。並且與作業系統有關,Issues 中有人提到,Mac 會預設監聽 Tcp46。也就是 ipv4 的請求和 ipv6 的請求不會互相摻雜,但是 ubuntu 卻變成了 tcp6.

img

所以,大約在 21 年左右,Nodejs 終於支援了單獨設定 Ipv6Only 選項,如圖

img

https://github.com/nodejs/help/issues/4067#:~:text=https://nodejs.org/api/net.html#serverlisten

測試由於年少無知,亂提 BUG

相關文章