Linux xử lý `ping` qua NAT như thế nào?
(devnonsense.com)- NAT IPv4 thường phân biệt đích nhận phản hồi bằng cổng TCP/UDP, nhưng ICMP echo của
pingkhông có cổng, nên điểm mấu chốt là Linux dùng giá trị nào làm khóa ánh xạ - Thí nghiệm tái hiện cấu hình NAT từ 192.168.99.0/24 sang 10.0.100.0/24 bằng cách tạo
client1,client2,natbox,servervới network namespace vàiptablesMASQUERADE - Khi đối chiếu RFC 792 với gói tin bắt được, có thể thấy Identifier và Sequence Number của ICMP echo được dùng để ghép cặp request/reply; trên đường đi ICMP
SOCK_DGRAMcủa Linux, local port của socket sẽ đi vào trường ID - Nếu hai client cùng dùng ICMP ID
999, netfilter sẽ đổi ICMP ID của một bên sang giá trị ngẫu nhiên để tránh xung đột, rồi trong phản hồi sẽ đổi lại về IP và ID gốc của client - NAT của Linux, ngay cả với ICMP không có cổng, vẫn lưu trạng thái hướng đi/hướng về trong conntrack tuple và dùng ICMP ID như khóa có thể thao tác để ánh xạ phản hồi về đúng host nội bộ
Môi trường thí nghiệm và cấu hình NAT
- Để mô phỏng nhiều thiết bị trên một máy Linux, bài viết dùng network namespace
- Tạo riêng từng namespace cho hai client,
natboxđóng vai trò router NAT, và server; đồng thời tách mạng riêng với mạng phía serverclient1:192.168.99.1/24client2:192.168.99.2/24- Giao diện trong của
natbox:192.168.99.3/24 - Giao diện ngoài của
natbox:10.0.100.1/24 server:10.0.100.2/24
- Chạy
ip,iptables,tcpdump... với quyền root trên Fedora 38 Server VM và Linux kernel 6.2.9 - Hai client được nối vào bridge
br0, cònnatboxđược nối lần lượt tới bridge và veth pair phía server - Default route của client được đặt là
192.168.99.3để lưu lượng đi tới server phải đi quanatbox - Trên
natbox, bật chuyển tiếp gói bằngnet.ipv4.ip_forward=1và thêm quy tắc MASQUERADE vào chainPOSTROUTINGcủa bảngnattrongiptablesip netns exec natbox iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
NAT ICMP qua quan sát gói tin
- Dùng
tcpdump -n icmptrong namespaceclient1vàserverđể bắt gói ICMP - Ở phía client thấy echo request
192.168.99.1 > 10.0.100.2và echo reply10.0.100.2 > 192.168.99.1 - Ở phía server, IP nguồn của cùng request đã đổi thành
10.0.100.1, xác nhận NAT đã viết lại địa chỉ nguồn thành IP ngoài củanatbox - Các request ICMP từ những client khác nhau có trường id khác nhau
- Trong ví dụ,
client1có ID31428 client2có ID33391
- Trong ví dụ,
- Quan sát này cho thấy
natboxcó thể dùng trường ID khi chuyển phản hồi ICMP trở lại client nội bộ
RFC 792 và ICMP ID của ping
- ICMP là giao thức cũ được định nghĩa trong RFC 792, công bố từ năm 1981
- Bản tin ICMP echo và echo reply có Type, Code, Checksum, Identifier, Sequence Number, Data
- Type dùng để phân biệt echo request và echo reply
- Type của echo request là
8 - Trong phần trích RFC, echo reply được ghi là
1 - Code là
0
- Type của echo request là
- RFC 792 giải thích rằng Identifier và Sequence Number có thể được dùng để ghép cặp echo request với reply
- Identifier có thể được dùng để nhận diện phiên giống như cổng trong TCP/UDP, còn Sequence Number có thể tăng dần theo mỗi echo request
- RFC không quy định cụ thể cách chọn ID trong thực tế, nên cần xem mã nguồn của
pinglà triển khai cụ thể
Cách iputils ping quyết định ID
- Lệnh
pingthuộc gói iputils - Chú thích gần
ping4_send_probenói rằng khi tạo ICMP echo request, trường ID là một số ngẫu nhiên còn Sequence Number là số nguyên tăng dần - Bên trong
pingcó trườngidentcủastruct ping_rts- Giá trị mặc định là
-1 - Có thể ghi đè bằng tùy chọn CLI
-evới giá trị từ0đếnIDENTIFIER_MAXlà0xFFFF
- Giá trị mặc định là
- Nếu
rts->ident == -1,pingsẽ bind socket với kiểuSOCK_DGRAMvà giao thứcIPPROTO_ICMP - Theo mô tả về socket
IPPROTO_ICMPtrên Linux, header ICMP sẽ được kiểm tra và dọn dẹp khisend(), còn id sẽ được đặt theo local port của socket - Nếu
pingkhông chỉ định source port, Linux kernel sẽ chọn ngẫu nhiên một cổng còn trống, và cổng đó được dùng làm ID của gói ICMP
Khi cùng một ICMP ID bị xung đột
- Trên hai client, dùng
ping -e 999để gửi ping tới server với cùng ICMP ID 999 - Kết quả bắt gói ở server cho thấy request của một client vẫn giữ ID
999, còn request của client kia bị đổi thành ID30218 - Thiết bị NAT đổi ID của một bên để tránh xung đột tổ hợp cùng IP ngoài và cùng ICMP ID
- Để tìm nơi xử lý xung đột, bài viết kiểm tra mã trong thư mục
net/netfiltercủa Linux nơi sử dụng trường ICMPid
Vai trò của netfilter, conntrack và NAT
- Hệ thống con trong kernel thực thi các quy tắc
iptableslà netfilter - Vì quy tắc MASQUERADE thực hiện NAT, nên phần triển khai NAT cho ICMP cũng nằm trong netfilter
nf_nat_setup_infotrongnf_nat_core.cgọiget_unique_tuple, rồi tiếp tục tớinf_nat_l4proto_unique_tuplenf_nat_l4proto_unique_tuplecó nhánhIPPROTO_ICMPvà tham chiếutuple->src.u.icmp.idnf_nat_manip_pkttrongnf_nat_proto.cđi quanf_nat_ipv4_manip_pktvàl4proto_manip_pkt, rồi khi là ICMP sẽ gọiicmp_manip_pkticmp_manip_pktghi ICMP ID thực tế vào gói bằnghdr->un.echo.id = tuple->src.u.icmp.id
Cách conntrack tuple biểu diễn ICMP
- Trong netfilter, connection không chỉ có nghĩa là kết nối TCP mà còn là trạng thái dùng để ghép gói đi và gói về cho cả các giao thức không kết nối như UDP hay ICMP
nf_conncótuplehash[IP_CT_DIR_MAX]IP_CT_DIR_ORIGINAL: hướng gói đi raIP_CT_DIR_REPLY: hướng phản hồi đi vào
- Mỗi
nf_conntrack_tuple_hashchứanf_conntrack_tupledùng để nhận diện connection - Tuple được chia thành
srccó thể thao tác vàdstbất biếnsrcchứa địa chỉ IP và trường riêng theo giao thức- Với ICMP, trường riêng theo giao thức là
__be16 id dstchứa địa chỉ IP không thay đổi cùng ICMPtype,code
- NAT lưu trong connection cách nó đã biến đổi gói đi, rồi đảo ngược thay đổi đó trên gói phản hồi
Đường đi mã chọn ICMP ID
- Khi
natboxnhận ICMP echo,nf_nat_setup_infotạo connection mới và quyết định có cần đổi IP nguồn cùng ICMP ID hay không - Sau đó, với mỗi gói ICMP,
nf_nat_manip_pktsẽ đặt IP nguồn hoặc đích và ICMP ID dựa trên giá trị đã lưu trong connection get_unique_tuplelà đường đi chính để chọn một NAT tuple còn dùng đượcfind_best_ips_protoviết lại địa chỉ IP nguồnnf_nat_used_tuplekiểm tra tuple đã được dùng hay chưa; nếu chưa thì trả lại nguyên tuple hiện tại- Vì vậy, nếu ICMP ID của hai client khác nhau thì ID vẫn được giữ nguyên sau NAT
- Nếu tuple đã được dùng,
nf_nat_l4proto_unique_tuplesẽ được gọi để thực hiện NAT theo từng giao thức
- Với ICMP,
tuple->src.u.icmp.idđược chọn làm khóa NAT đích find_free_idtạo ID ngẫu nhiên bằngget_random_u16(), điều chỉnh về phạm vi ICMP ID hợp lệ rồi kiểm tra xem đã dùng chưa- Phạm vi ID mặc định là toàn bộ dải ID; trong quy tắc
iptablesMASQUERADE có thể chỉ định phạm vi như100-200bằng--to-ports - Nếu không tìm được tuple chưa dùng, ID trùng vẫn sẽ ở lại trong connection, và sau đó
__nf_conntrack_confirmphát hiện trùng lặp rồi drop gói
Xác minh hành vi kernel bằng bpftrace
- Để kiểm tra cách hiểu về netfilter, bài viết dùng
bpftrace - Các hàm kernel được theo dõi là
nf_nat_setup_infovànf_nat_manip_pkt kprobetheo dõi thời điểm hàm được gọi, cònkretprobetheo dõi thời điểm hàm trả về- Trong
kretprobekhông thể truy cập trực tiếp tham số hàm, nên cần lưu tham số vào BPF map lúc vào hàm rồi đọc lại khi trả về struct sk_bufflà cấu trúc Linux kernel dùng để biểu diễn gói tinbswapđược dùng để đổi network byte order dạng big endian sang little endianntopchuyển địa chỉ IP thành chuỗi- Nhờ BPF Type Format (BTF) trên các kernel Linux mới, chương trình BPF có thể tham chiếu các cấu trúc kernel như
sk_buff,nf_connmà không cần include header - Chương trình
bpftracenày được thử trên Linux kernel 6.2.9; ở các phiên bản kernel khác, khả năng hoạt động có thể khác
Kết quả theo dõi và kết luận
- Khi hai client gửi ping với cùng ICMP ID
999,nf_nat_setup_infođược gọi một lần cho mỗi client - Client đầu tiên
192.168.99.1giữ nguyên ICMP ID999ở cả original tuple lẫn reply tuple - Client thứ hai
192.168.99.2bị viết lại ICMP ID của reply tuple thành32809 nf_nat_manip_pktđổi IP nguồn thành10.0.100.1vớiNF_NAT_MANIP_SRCtrên echo request, và trên phản hồi thì đổi IP đích về IP client gốc vớiNF_NAT_MANIP_DST- ICMP ID trong gói phản hồi cũng được khôi phục từ giá trị đã NAT về lại giá trị gốc mà client đã gửi
- Có thể kiểm tra timeout conntrack ICMP mặc định tại
/proc/sys/net/netfilter/nf_conntrack_icmp_timeout, và giá trị mặc định quan sát được là 30 giây - Nếu client không gửi gói nào trong hơn 30 giây,
nf_nat_setup_infosẽ được gọi lại ở lần ping tiếp theo - Hành vi NAT của Linux với
pingcũng được tài liệu hóa trong Netfilter Hacking HOWTO, và điểm cốt lõi là conntrack tuple cùng việc viết lại ICMP ID
1 bình luận
Các ý kiến trên Hacker News
Khi máy chủ khởi động, nó bắt đầu gửi các gói ICMP echo request cố định đến địa chỉ cố định 3.3.3.3, và kỳ vọng rằng các gói này sẽ không quay trở lại
3.3.3.3 không phải là một host có thể truy cập được, cũng không phải là mục tiêu để spoof. Thay vào đó, khi client muốn kết nối, vì nó biết IP của server nên nó gửi cho server một gói ICMP Time Exceeded. Bên trong gói ICMP đó có gói cố định “gốc” mà server vẫn gửi đến 3.3.3.3, và gói được hard-code này đóng vai trò là định danh của pwnat
Có thể hiểu là client giả vờ là một hop trên Internet và báo rằng “ICMP echo request” ban đầu của server không thể được chuyển tiếp. NAT thấy gói bên trong ICMP Time Exceeded khớp với gói mà server đã gửi nên chuyển tiếp nó đến server phía sau NAT; lúc này toàn bộ IP header của client cũng được bao gồm, nhờ đó server biết được địa chỉ IP của client
Hoạt động cốt lõi của công cụ này sau đó là tạo một UDP tunnel giữa client và server
Tuy nhiên, theo tôi lướt qua thì có vẻ nó giả định rằng NAT không viết lại cổng nguồn UDP, nên có lẽ không hoạt động trên mọi router. STUN, dùng trong WebRTC và các trường hợp tương tự, triển khai các kỹ thuật tinh vi hơn, nhưng nếu vẫn không được thì phải dùng TURN làm relay
Rất có khả năng cùng vấn đề này cũng áp dụng cho mẹo ping 3.3.3.3. Nếu NAT viết lại định danh ping như trong bài viết, mẹo này sẽ hỏng
Khi nhận được phản hồi, router dùng giá trị ID duy nhất đó để chuyển phản hồi đến đúng thiết bị trong mạng cục bộ
Chỉ cần một máy tính và Wireshark/tcpdump là có thể kiểm chứng
Bản thân bài viết thì hay, và có thể đem lại khoảnh khắc “vỡ ra” cho những ai hoàn toàn chưa hiểu về networking. Tuy vậy, về bản chất nó giống cách dựng một phòng lab mạng tử tế rồi đào vào mã nguồn hơn là tự suy luận trực tiếp
Những ví dụ có thể tự tay làm theo kiểu này thật sự rất đáng quý, và tôi định sẽ tự làm theo
Bài viết gần như duy nhất khác về chủ đề này mà tôi thấy dễ hiểu là bài này của Tailscale. Có nhiều “ví dụ diễn giải chi tiết”, nên thấy rõ toàn bộ các phần khớp với nhau ra sao
https://tailscale.com/blog/how-nat-traversal-works/
Tình cờ là cuối tuần này tôi cũng vật lộn với Netfilter để bật transparent proxy trên router OpenWRT
Các tài liệu cơ bản nên tham khảo khi xem Netfilter là https://wiki.nftables.org/wiki-nftables/index.php/Main_Page và https://www.netfilter.org/projects/nftables/manpage.html
Nhưng ICMP echo request có ID, và trên thực tế nó đóng vai trò giống như số cổng nguồn
Để NAT ICMP echo đúng cách, cũng như remap cổng nguồn của UDP, ID cũng cần được remap hai chiều
Vì nếu các máy phía sau NAT đồng thời nhận ping từ hai host, và hai host tình cờ dùng cùng số yêu cầu, thì sẽ trở nên mơ hồ
Một khả năng khác là không viết lại định danh mà duy trì danh sách các máy từ xa gắn với từng ID. Nếu ID bị trùng, danh sách sẽ có hơn một địa chỉ IP từ xa; khi máy phía sau NAT trả lời, NAT có thể chọn một máy trong danh sách, gửi phản hồi đến máy đó rồi xóa mục tương ứng
Nếu là IPv6, tôi đã phải đổi mọi node trong mạng và cập nhật cả DNS nội bộ
Về lý thuyết tôi có thể có một /48 mang theo được, nhưng ISP mới phải quảng bá nó giúp tôi, và dù ISP hiện tại có làm thì đó cũng không phải chuyện phổ biến
Một tuần trước khi đường dây điện thoại bị đứt, tôi lấy 5G MiFi ra và chuyển kết nối WAN sang đó; chỉ cần bật masquerade đơn giản trên interface đó là được. Tín hiệu yếu nên không tốt lắm, nhưng vẫn hoạt động
Vấn đề là dù dùng IPv6, tôi vẫn phải vận hành dual-stack hoặc dùng lớp trừu tượng NAT bẩn thỉu. Chỉ làm tăng việc mà không đem lại lợi ích cho tôi
Phía công việc cũng tương tự. Các xe dùng các subnet nội bộ 172.16/12 kết nối và định tuyến lẫn nhau, đồng thời nối ra bên ngoài qua nhiều kết nối VPN khác nhau. Chúng thường đậu dưới tầng hầm nên gần như không có tín hiệu, vì vậy kiến trúc là hy vọng ít nhất một trong nhiều cách sẽ hoạt động
Khi chuyển sang IPv6 thì lại phải di chuyển các khối /48. Hơn nữa, những xe này nhận Internet tại nhiều sân vận động khác nhau, mà nhiều nơi trong số đó thậm chí còn khó tắt MITM/443 hoặc gỡ chặn UDP. Trong môi trường đến nơi lúc 10 giờ sáng thứ Bảy và phải chạy được sau 2 giờ thì cách đó không khả thi
Không rõ lợi ích kinh doanh của việc chuyển sang dual-stack là gì, khi nó làm khối lượng công việc và rủi ro tăng gấp đôi
https://en.wikipedia.org/wiki/Lindy_effect
https://stackoverflow.com/questions/31857419/how-to-send-a-m...
Đáng tiếc là ping được hệ điều hành xử lý, nên các ứng dụng ở IP ngang hàng không thể đọc thông điệp
Tôi nghĩ đã đến lúc cung cấp hook ở user space cho một số dịch vụ kiểu này để cho phép P2P thật sự khi cả hai bên đều ở sau NAT. Ít nhất cũng là một luồng sự kiện chỉ đọc chẳng hạn. Giờ đây, mọi rào cản ngăn điều đó đều có vẻ nhân tạo
Tuy vậy, tôi đã từng thấy các chiến lược rò rỉ dữ liệu dùng ping hoặc các cách liên lạc khác. Ngày nay, đa số cấu hình mặc định của tường lửa âm thầm bỏ toàn bộ ICMP, bao gồm ping, nên có lẽ gần như không thể dùng cho P2P
idthực chất trông tương đương với(sport, dport), nhưng chỉ 16 bit nên không gian nhỏ hơn nhiều so với 32 bitNhưng tôi nghĩ vấn đề cốt lõi của NAT hole punching là muốn tạo kết nối thì cả hai đầu đều phải có hoạt động. Vì vậy luôn cần một máy chủ điều phối để báo cho T biết rằng nút S muốn nói chuyện với nó
Dù vậy cũng có thêm điều để suy nghĩ. Tôi tự hỏi liệu có cách nào dùng các thông điệp định tuyến ICMP, chẳng hạn unreachable hoặc TTL expired, hay không. Khi traceroute tới một IP nào đó, bạn nhận lại gói tin từ các IP tùy ý khác, và việc này nhìn chung đi qua NAT
Có thể tưởng tượng host T muốn nhận kết nối đến chọn ngẫu nhiên một địa chỉ IP “giả”, công bố
(IP router, IP giả)làm định danh của mình, rồi định kỳ gửi gói tin tới IP giả đó. Host S muốn nói chuyện với T có thể gửi ICMP TTL-expired liên quan đến địa chỉ giả đó tới router của T, và router có thể thấy nó rồi chuyển tiếp cho TTất nhiên điều này phụ thuộc vào việc địa chỉ IP bên trong các trường ICMP có bị lọc ingress như địa chỉ trong IP header hay không
Sửa: đã có một bình luận cấp cao trỏ tới phần triển khai của ý tưởng này
[1] - https://github.com/yarrick/pingfs
Nếu là GitHub thì có thể gắn với tổ hợp commit hash cụ thể, tên tệp và số dòng, nhưng khi codebase thay đổi lớn thì nó không còn hữu ích mấy. Trên các trình xem web cho git ít dùng hơn như git.blender.org thì việc này không hoạt động tốt
https://elixir.bootlin.com/linux/latest/source