一个奇葩的网络问题,把技术砖家搞蒙了
大家好,最近遇到一個奇葩的網(wǎng)絡(luò)問題,分享給大家,看完一定會覺得很奇葩。
問題現(xiàn)象
客戶反饋有一個server端S, 兩個client端C1, C2, S的iptables規(guī)則對C1, C2都是放通的,但是C2無法連接上S,客戶很著急,催我們盡快解決。
這里解釋一下,iptables規(guī)則是防火墻規(guī)則,是linux系統(tǒng)實現(xiàn)防火墻一個應(yīng)用層配置工具,底層依賴是Linux內(nèi)核網(wǎng)絡(luò)子系統(tǒng)的netfiler框架。這里簡單介紹一下問題的背景知識(后面網(wǎng)絡(luò)硬核系列文章會詳細介紹)。
背景知識
netfilter是Linux內(nèi)核內(nèi)部的一個框架,該框架允許內(nèi)核模塊在Linux網(wǎng)絡(luò)堆棧的不同位置注冊回調(diào)函數(shù)。然后對于遍歷Linux網(wǎng)絡(luò)堆棧內(nèi)各個掛鉤的每個數(shù)據(jù)包,將調(diào)用已注冊的回調(diào)函數(shù)。
netfilter / iptables能做什么?
建立基于無狀態(tài)和有狀態(tài)數(shù)據(jù)包過濾的Internet防火墻
部署高可用性的無狀態(tài)和有狀態(tài)防火墻集群
如果您沒有足夠的公共IP地址,請使用NAT和偽裝來共享Internet訪問
使用NAT來實現(xiàn)透明代理
協(xié)助tc和iproute2系統(tǒng)用于構(gòu)建復(fù)雜的QoS和策略路由器
進行進一步的數(shù)據(jù)包處理(處理),例如更改IP標頭的TOS / DSCP / ECN位
在內(nèi)核協(xié)議棧位置大概是這樣:
netfilter 注冊HOOK函數(shù)位置:
5個HOOK點:
PREROUTING:數(shù)據(jù)包進入路由表之前
INPUT:通過路由表后目的地為本機
FORWARD:通過路由表后,目的地不為本機
OUTPUT:由本機產(chǎn)生,向外發(fā)送
POSTROUTIONG:發(fā)送到網(wǎng)卡接口之前。
每個HOOK點都會執(zhí)行一些函數(shù),大致分為下面幾個表:
NAT表: 用于實現(xiàn)nat功能,端口映射,地址映射等
mangle表: 用來修改報文,例如更改IP標頭的TOS / DSCP / ECN位
filter表:用來過濾報文
raw表:用來提前標記報文不走一些流程(比如不需要建會話)
如果不熟悉不要緊,只需知道在協(xié)議棧收發(fā)關(guān)鍵入口(進入,轉(zhuǎn)發(fā),發(fā)出)注冊一些 hook函數(shù),然后報文會經(jīng)過hook里面函數(shù)處理,而這些hook的動作(函數(shù))是需要一個iptables的工具下發(fā)配置生成的。
iptables是您下發(fā)規(guī)則的用戶態(tài)工具。IP表中的每個規(guī)則都由多個分類器(iptables匹配項)和一個連接的動作(iptables目標)組成。命令格式可參考下圖,詳細可以參考man手冊:
這里記住兩個動作就行了,ACCEPT表示通過,DROP表示丟掉;
內(nèi)核里面看到的規(guī)則大概是這個樣子:
主要特點
無狀態(tài)數(shù)據(jù)包過濾(IPv4和IPv6)
有狀態(tài)的數(shù)據(jù)包過濾(IPv4和IPv6)
各種網(wǎng)絡(luò)地址和端口轉(zhuǎn)換,例如NAT / NAPT(IPv4和IPv6)
靈活可擴展的基礎(chǔ)架構(gòu)
第三方擴展的API
所以大多數(shù)公司會基于netfilter框架實現(xiàn)軟件防火墻(安全組)和軟件虛擬交換機(vswitch,類似ovs)。
好,背景簡單介紹完了,如果不是很理解,后面會詳細介紹的,來看一下這個客戶實際問題。
分析過程
1.先了解網(wǎng)絡(luò)拓撲
客戶網(wǎng)絡(luò)拓撲是相當?shù)暮唵?#xff0c;就是同網(wǎng)段下client機器訪問服務(wù)器,一臺可以訪問,一臺不可以。
2. 查看配置是否丟失
由于是同網(wǎng)段,相當于局域網(wǎng)通信,C1都可以訪問S,那在S上路由肯定是ok的。對于服務(wù)端 10.0.0.3 而言,兩臺客戶端機器到本機的iptable規(guī)則都是一樣的。但10.0.2.8到10.0.0.3 的20202端口的syn包被丟了,而10.0.2.7 到 10.0.0.3 的 20202 端口的訪問是正常的。
配置都在,需要進一步分析。
3. 抓包分析
client端發(fā)起telnet命令連接server, server按client ip抓包,?發(fā)現(xiàn)只有TCP SYN請求過來,并無回包,確實是本機OS內(nèi)丟包導(dǎo)致無法正常連接:
通過netstat可以查看服務(wù)器TCP統(tǒng)計:?連接數(shù)不高,沒有TCP隊列丟包統(tǒng)計,結(jié)合10.0.2.7 client可以訪問,進一步確認,放通安全組后,10.0.2.8 到 10.0.0.3 的 icmp 包也沒有回,這個時候和TCP協(xié)議處理沒有必然關(guān)系了。
4.?追蹤內(nèi)核丟包調(diào)用棧
報文如果被內(nèi)核丟棄后,正常情況都會調(diào)用kfree_skb函數(shù),內(nèi)核通過kfree_skb釋放skb,kfree_skb函數(shù)中已經(jīng)埋下了trace點,并且通過__builtin_return_address(0)記錄下了調(diào)用kfree_skb的函數(shù)地址并傳給location參數(shù),可以通過systemtap腳本,追蹤kfree_skb這個trace point, 找到匹配ip的丟包并輸出對應(yīng)的堆棧:
/*** kfree_skb - free an sk_buff* @skb: buffer to free** Drop a reference to the buffer and free it if the usage count has* hit zero.*/ void kfree_skb(struct sk_buff *skb) {if (unlikely(!skb))return;if (likely(atomic_read(&skb->users) == 1))smp_rmb();else if (likely(!atomic_dec_and_test(&skb->users)))return;trace_kfree_skb(skb, __builtin_return_address(0));__kfree_skb(skb); } EXPORT_SYMBOL(kfree_skb);/** Tracepoint for free an sk_buff:*/ TRACE_EVENT(kfree_skb,TP_PROTO(struct sk_buff *skb, void *location),TP_ARGS(skb, location),TP_STRUCT__entry(__field( void *, skbaddr )__field( void *, location )__field( unsigned short, protocol )),TP_fast_assign(__entry->skbaddr = skb;__entry->location = location;__entry->protocol = ntohs(skb->protocol);),TP_printk("skbaddr=%p protocol=%u location=%p",__entry->skbaddr, __entry->protocol, __entry->location) );systemtap腳本:
# cat mydropwatch.stp #!/usr/bin/stap --all-modules %{ #include <linux/kernel.h> #include <linux/net.h> #include <linux/textsearch.h> #include <net/checksum.h> #include <linux/dma-mapping.h> #include <linux/netdev_features.h> #include <linux/skbuff.h> #include <uapi/linux/ip.h> #include <uapi/linux/udp.h> #include <uapi/linux/tcp.h> %} ############################################################ # dropwatch2.stp # An example script to mimic the behavior of the dropwatch utility # Reports every 5 seconds with timestamp # Usage: stap -g --all-modules mydropwatch.stp //-g for guru mode ############################################################ function get_packet_info:string(skb:long) %{int ret=-1;unsigned int src_port = 0;unsigned int dest_port = 0;struct udphdr *udp_header;struct tcphdr *tcp_header;struct sk_buff *skb= (struct sk_buff *)STAP_ARG_skb;struct iphdr *ip_header;unsigned int src_ip=0,dest_ip=0;if(!skb){goto EXIT_F;}ip_header = (struct iphdr *)skb_network_header(skb);if(!ip_header){goto EXIT_F;}src_ip = (unsigned int)ip_header->saddr;dest_ip = (unsigned int)ip_header->daddr; #if 0if (ip_header->protocol==17) {udp_header = (struct udphdr *)skb_transport_header(skb);src_port = (unsigned int)ntohs(udp_header->source);} else if (ip_header->protocol == 6) {tcp_header = (struct tcphdr *)skb_transport_header(skb);src_port = (unsigned int)ntohs(tcp_header->source);dest_port = (unsigned int)ntohs(tcp_header->dest);}printk(KERN_INFO "OUT packet info: src ip: %u, src port: %u; dest ip: %u, dest port: %u; proto: %u\n", src_ip, src_port, dest_ip, dest_port, ip_header->protocol); #endif// printk(KERN_DEBUG "IP addres = %pI4 DEST = %pI4\n", &src_ip, &dest_ip); EXIT_F:snprintf(STAP_RETVALUE, MAXSTRINGLEN, "%d.%d.%d.%d",(unsigned int)((unsigned char *)&src_ip)[0],(unsigned int)((unsigned char *)&src_ip)[1],(unsigned int)((unsigned char *)&src_ip)[2],(unsigned int)((unsigned char *)&src_ip)[3]); %} # Array to hold the list of drop points we find global locations # Note when we turn the monitor on and off probe begin { printf("Monitoring for dropped packets\n") } probe end { printf("Stopping dropped packet monitor\n") } # increment a drop counter for every location we drop at probe kernel.trace("kfree_skb") {SrcIp=get_packet_info($skb)//if(SrcIp=="10.0.2.8")//src ip of packet is "10.0.2.8"// printf("srcip:%s\r\n",SrcIp)if(SrcIp==@1){locations[$location] <<< 1 //systemtap Statistical aggregateif(symname($location)=="nf_hook_slow")//dump the backtrace if kfree_skb called by nf_hook_slow functionprint_backtrace();} } # Every 5 seconds report our drop locations probe timer.sec(5) { // printf("\n=== %s ===\n", ctime(gettimeofday_s()))foreach (l in locations-) {printf("\n=== %s ===\n", ctime(gettimeofday_s()))printf("%d packets dropped at %x (%s)\n",@count(locations[l]), l, symname(l))}delete locations } ### mydropwatch.stp ends ###執(zhí)行方法:
stap -g -v --all-modules mydropwatch.stp 10.0.2.8
執(zhí)行命令后,對應(yīng)輸出:
從調(diào)用棧可知是在ip_local_deliver函數(shù)時因為iptables規(guī)則導(dǎo)致的丟包,結(jié)合內(nèi)核代碼:
nf_hook_slow+0xf3 對于代碼188行(先找到對應(yīng)內(nèi)核版本的源代碼,用crash工具打開對應(yīng)內(nèi)核版本的vmlinux,反匯編函數(shù)查看指令偏移,定位到源代碼行號):
可知是在INPUT鏈丟的包, 但是檢查了mangle, nat, filter等表的INPUT鏈,并無其他drop規(guī)則,默認policy均為ACCEPT, 所以這里有點奇怪。
為了確定是否真的匹配上了ACCEPT規(guī)則,我在規(guī)則前后添加了打印log規(guī)則,報文匹配后會打印log出來:
顯示確認一下:
通過觸發(fā)請求,匹配上了第一個打印log的規(guī)則,10.0.2.8白名單規(guī)則命中計數(shù)有新增,而第二log規(guī)則沒有匹配上,說明報文命中了白名單規(guī)則,報文可以通過防火墻:
但為什么服務(wù)端沒有回syn+ack呢,是不是某種場景觸發(fā)了netfiler框架的bug,假裝accept,實際上把報文悄悄地給丟了?? 結(jié)合報文和系統(tǒng)環(huán)境,看了幾遍netfiler代碼后,沒有看出啥問題,清空iptables規(guī)則,停了firewalld,依然丟了,還是丟在 nf_hook_slow,也進一步證明不是普通iptables規(guī)則導(dǎo)致的。
如果要繼續(xù)排查,是不是得遍歷下內(nèi)核數(shù)據(jù)結(jié)構(gòu),看看是不是list壞掉了,由于時間比較緊迫,遍歷內(nèi)核數(shù)據(jù)結(jié)構(gòu)有點麻煩,需要找到netfiler hook表起始地址,再結(jié)合數(shù)據(jù)結(jié)構(gòu)算出偏移,一步一步遍歷內(nèi)存,由于客戶機器可以重啟(業(yè)務(wù)已經(jīng)不正常了),客戶相信,重啟可以比我們更快解決問題,還是重啟大法好,但不幸的是,機器重啟完后沒有恢復(fù),問題依然存在,這個就有意思了。
5. 排查其他netfilter hook(鉤子)函數(shù)
那么,有沒有可能是有其他內(nèi)核模塊在INPUT鏈中注冊了hook函數(shù),drop了對應(yīng)的包呢?我們使用crash調(diào)試內(nèi)核來找一下:
背景知識
crash是redhat的工程師開發(fā)的,主要用來離線分析linux內(nèi)核轉(zhuǎn)存文件,它整合了gdb工具,功能非常強大。可以查看堆棧,dmesg日志,內(nèi)核數(shù)據(jù)結(jié)構(gòu),反匯編等等。crash支持多種工具生成的轉(zhuǎn)存文件格式,如kdump,LKCD,netdump和diskdump,而且還可以分析虛擬機Xen和Kvm上生成的內(nèi)核轉(zhuǎn)存文件。同時crash還可以調(diào)試運行時系統(tǒng),直接運行crash即可,ubuntu下內(nèi)核映象存放在/proc/kcore。
5.1. 從nf_hooks找到ipv4 INPUT鏈的hook列表
內(nèi)核hook的組織:
那么對于NFPROTO_IPV4協(xié)議的NF_INET_LOCAL_IN鏈,我們可以找到其鏈表地址:
可以看到有一個非標模塊resguard_linux引入了非標hook函數(shù)net_hook_in_v4,反匯編net_hook_in_v4函數(shù),可知其有一些白名單/黑名單之類的檢查:
由此基本可以確定是這個模塊的安全功能利用netfilter hook攔截了訪問。
他大爺?shù)?#xff0c;還真的注冊了nf?hook。
但要客戶認賬,還得證明這個包就是這個hook函數(shù)丟的,其實已經(jīng)差不多了。
5.2. 如何證明是net_hook_in_v4返回NF_DROP(0x0)導(dǎo)致的丟包?
可以結(jié)合systemtap得到skb地址,然后通過perf probe抓取net_hook_in_v4和nf_iterate的返回值:
腳本:
運行結(jié)果:
由此可以看出bb2100結(jié)尾的這個skb是因為nf_hook_in_v4導(dǎo)致的丟包。
網(wǎng)上搜索可知該第三方安全模塊疑似云鎖這類軟件提供,他可以通過ip黑白名單控制對網(wǎng)站的訪問。
問題算是終于結(jié)束了,想著之前催得那么急,真想在線battle一下,然而客戶是大爺,不敢想,不敢想。
可以學(xué)到什么
1. 大致了解Linux防火墻實現(xiàn)框架:netfilter+iptables;
2. 通過systemtap腳本追蹤內(nèi)核協(xié)議棧丟包堆棧信息;
3. 怎么通過堆棧函數(shù)和指令偏移找到源代碼信息;
4. 一些丟包排查思路;
5. 了解一些工具使用,比如systemtap,crash,iptables,tcpdump等等;
最后,希望本文可以對你有所幫助,謝謝觀賞,有任何問題可以在評論區(qū)留言,如果覺得文章不錯,麻煩一鍵三連支持
網(wǎng)絡(luò)硬核文章推薦
計算機網(wǎng)絡(luò)硬核指南|網(wǎng)絡(luò)設(shè)計核心思想
云網(wǎng)絡(luò)丟包故障定位全景指南
TCP協(xié)議疑難雜癥全景解析|硬核
可以關(guān)注【極客重生】并星標,防止錯過后面精彩文章。
專注分享硬核知識和技術(shù)人一起涅槃重生
總結(jié)
以上是生活随笔為你收集整理的一个奇葩的网络问题,把技术砖家搞蒙了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TCP协议疑难杂症全景解析|硬核
- 下一篇: Redis 多线程网络模型全面揭秘|网络