一、报文跨层传递
所有的网络协议栈都告诉我们:TCP/IP协议栈是分层的,低一层的协议无需也不能感觉到上层的协议,这个观念在我的脑海中根深蒂固,并且由衷的赞叹这种设计的思想,但是在经过一些简单的思考就会发现,这种分层并不是绝对的,正如这世间的一切。一个直观的问题是,一样米养百样人,同样的网卡上,可以跑IP/ARP协议,也可以有ICMP/IGMP/TCP/UDP协议,低层协议栈的数据向上传递,如何根据这些不同的类型派发到下一层的不同逻辑也是一个问题;对应地,当上层通过网卡发送数据时,下层的协议栈同样可能会需要知道上层的协议。
对于这种问题的处理,通常有两种方法,一种就是在低层字段解析出上层的类型;另一种就是真的透明,在下一层的固定字段,通常最简单的就是第一个字段,但是第一个字段通常还要有更加必不可少的报文长度信息,并且在实际应用中也没有使用这个格式。更直观的说,以以太网一个数据帧来说,它最为关键的三个信息时 Destination MAC Address、Source MAC Address、 Ether Type;其中的 Ether Type决定了上层的协议类型,可能是IP、ARP,在内核的if_ether.h文件中枚举了大量我没有见到更没有用到过的数据帧类型。在IP协议中,Protocol字段表示了传输层的协议;也就是说,一个低层的协议中其实是包含了直接上层协议栈中的具体类型,按照Google protobuffer的格式,其实就相当于一个union结构的selector放在了自己依赖层而不是放在自己的结构中,这样从实现上来看,个人感觉的确是没有做到层与层之间的绝对透明。但是从程序或者说协议栈的实现来看,这个地方真真是极好的。现在考虑一个网卡收到一个报文,在这个报文自下向上传递的过程中,如果提前知道下一层的具体协议,可以非常方便报文向上层协议栈的派发。这里也使我们得到一个重要的经验:如果一个字段是该层所有模块都必须的,那么可以下放到下一层。
由于网卡是最为常见的介质,使用这个作为例子未免会缺少不同实现之间比较的意义,所以使用另一个基于tty设备的通讯介质通讯类型作为例子,其实我们常见的modem也就是tty的一种。
二、从介质层接收到报文
对于slip的处理位于linux-2.6.32.60drivers
etslip.c文件:slip_receive_buf--->>>slip_unesc--->>sl_bump。在这个函数中,SLIP将从tty设备上接收的数据上传给网络层处理,但是问题是在这个设备上并没有指明上层协议类型,因为作为SLIP设备它只支持一种IP类型,ARP在SLIP中没有意义,所以在该函数中直接写死了网络层协议类型,也就是IP协议:
skb->protocol = htons(ETH_P_IP);
作为对比,我们看下功能更为强大的,也是我们最常用的网卡设备中对于这个字段的处理。自然而然地,这个字段在以太网frame中自带类型,驱动层只需要做一个简单的处理(区分出 类型还是长度)之后透传给上层,而这个透传对于协议栈来说就是派发(dispatch)。
对于通用网卡上接收的以太帧来说,它的处理位于linux-2.6.32.60
etetherneteth.c:eth_header(),其中对于上层协议栈类型的解析只有一点特殊:
if (type != ETH_P_802_3)
eth->h_proto = htons(type);
else
eth->h_proto = htons(len);
三、向网络层的dispatch
对于介质层发送的报文,内核通常叫做packet,由于介质层已经指明了网络层的类型,所以这个packet的派发就比较简单了,大家把自己对应的selector注册到一起,然后内核就可以根据介质层packet中的ether type决定由网络层的哪一种类型来处理,这些协议的注册就是一个一个的packet_type对象,我们常见的packet类型包含有af_inet.c:ip_packet_type;arp.c:arp_packet_type两种类型。
当网络设备接收到报文之后,解析出一个基本的sk_buff结构,这个结构关键的地方就是要设置结构中的protocol字段,在netif_receive_skb函数中会遍历这个设备上所注册的所有的packet类型:
type = skb->protocol;
list_for_each_entry_rcu(ptype,
&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
if (ptype->type == type &&
(ptype->dev == null_or_orig || ptype->dev == skb->dev ||
ptype->dev == orig_dev)) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
四、向传输层的dispatch
网络层协议的注册通过net_protocol对象来注册,在af_inet.c文件中定义了我们最为常见的igmp_protocol、tcp_protocol、udp_protocol、icmp_protocol。这些协议注册之后,在IP层向上dispatch的时候就可以直接使用这个作为路由规则(或者认为是protocol selector,我更prefer这个叫法)。linux-2.6.32.60
etipv4ip_input.c:ip_local_deliver_finish(struct sk_buff *skb)
int protocol = ip_hdr(skb)->protocol;
int hash, raw;
const struct net_protocol *ipprot;
……
hash = protocol & (MAX_INET_PROTOS - 1);
ipprot = rcu_dereference(inet_protos[hash]);
……
ret = ipprot->handler(skb);
到了控制层之后,不再有再上一层的protocol selector了,http、ftp都是自己来区分和定义协议。
五、为什么会又看到了这些
网络这一块感觉很久没有再看了,再次看到这里,说来话长,简单来说原因就是想知道一个问题的答案:假设A和B两个主机建立了一个TCP连接,然后A close连接,在A经过一些FIN_WAIT1、FIN_WAIT2、TIME_WAIT延迟状态之后,这个主机A上和B之间的TCP连接终将断开,这个断开意味着这个连接状态从整个系统A中消失,这个也没什么不合理,但是问题套接口的另一端B如果坚持不关闭这个socket,在B上将始终看到一个CLOSE_WAIT状态的established状态的socket连接,这个连接在默认情况下将会一直存在(我说的默认情况包括了没有设置socket的keepalive选项)。再进一步,假设说A不断的连接B的同一个端口,由于A主机上的本地端口会有一个端口范围,随着时间的推移,A机器上的本地端口会有一个轮回,也就是说B机器上现在还存在的、处于CLOSE_WAIT状态中的established TCP socket连接将怎么看待这个新来的连接?
这个地方说起来有些拗口,所以通过一个代码来演示下吧(代码从网路上一个代码改写,主要是在客户端增加了设置TCP_LINGER2时间,从而让客户端快速释放本地port):
tsecer@harry: cat server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
int main(int argc, char *argv[])
{
int listenfd = 0, connfd = 0;
struct sockaddr_in serv_addr;
char sendBuff[1025];
time_t ticks;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, '0', sizeof(serv_addr));
memset(sendBuff, '0', sizeof(sendBuff));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(5555);
bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(listenfd, 10);
while(1)
{
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
}
}
tsecer@harry: cat client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
int main(int argc, char *argv[])
{
int sockfd = 0, n = 0;
char recvBuff[1024];
struct sockaddr_in serv_addr;
if(argc != 2)
{
printf("
Usage: %s <ip of server>
",argv[0]);
return 1;
}
memset(recvBuff, '0',sizeof(recvBuff));
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("
Error : Could not create socket
");
return 1;
}
int iset = 1;设置linger2时间为1,从而便于快速释放本地port
iset = setsockopt(sockfd, SOL_TCP, TCP_LINGER2, &iset,sizeof(iset));
printf("iset %d
", iset);
memset(&serv_addr, '0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(5555);
if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)
{
printf("
inet_pton error occured
");
return 1;
}
if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("
Error : Connect Failed
");
return 1;
}
return 0;
}
tsecer@harry: gcc server.c -o server
tsecer@harry: gcc client.c -o client
tsecer@harry: ./server &
[1] 19063
tsecer@harry: echo "32768 32770" > /proc/sys/net/ipv4/ip_local_port_range 为了便于测试,限制本地连接端口最多使用两个,也即是最多使用了两个本地端口之后本地端口开始回绕。
tsecer@harry: for (( n = 0; n < 3; n++)) do strace ./client 127.0.0.1 ; done
此处代码省略,当执行了多次之后,我们看下侦听进程的状态:
secer@harry: lsof -Pnp 19063
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
server 19063 root cwd DIR 8,4 4096 5685857 /data/harry/harrywork/closewaitprobe
server 19063 root rtd DIR 8,1 4096 2 /
server 19063 root txt REG 8,4 10005 5685861 /data/harry/harrywork/closewaitprobe/server
server 19063 root mem REG 8,1 132822 24291 /lib64/ld-2.4.so
server 19063 root mem REG 8,1 1570761 24350 /lib64/libc-2.4.so
server 19063 root mem REG 0,0 0 [stack] (stat: No such file or directory)
server 19063 root 0u CHR 136,12 14 /dev/pts/12
server 19063 root 1u CHR 136,12 14 /dev/pts/12
server 19063 root 2u CHR 136,12 14 /dev/pts/12
server 19063 root 3u IPv4 158668992 TCP *:5555 (LISTEN)
server 19063 root 4u sock 0,4 158668993 can't identify protocol
server 19063 root 5u sock 0,4 158671295 can't identify protocol
server 19063 root 6u sock 0,4 158671303 can't identify protocol
server 19063 root 7u sock 0,4 158671472 can't identify protocol
server 19063 root 8u sock 0,4 158671608 can't identify protocol
server 19063 root 9u sock 0,4 158672286 can't identify protocol
server 19063 root 10u sock 0,4 158674733 can't identify protocol
server 19063 root 11u sock 0,4 158674800 can't identify protocol
server 19063 root 12u sock 0,4 158674864 can't identify protocol
server 19063 root 13u sock 0,4 158706082 can't identify protocol
server 19063 root 14u sock 0,4 158706230 can't identify protocol
server 19063 root 15u sock 0,4 158713114 can't identify protocol
server 19063 root 16u sock 0,4 158713422 can't identify protocol
server 19063 root 17u sock 0,4 158716774 can't identify protocol
server 19063 root 18u sock 0,4 158721739 can't identify protocol
server 19063 root 19u IPv4 158725544 TCP 127.0.0.1:5555->127.0.0.1:32769 (CLOSE_WAIT)
server 19063 root 20u IPv4 158728291 TCP 127.0.0.1:5555->127.0.0.1:32768 (CLOSE_WAIT)
tsecer@harry:
看到了大量的不在listen也不在established状态的socket,这个比较奇怪,所以通过TCP抓包来看下系统交互:
tsecer@harry: tcpdump -ni any host 127.0.0.1 and tcp port 5555
tcpdump: WARNING: Promiscuous mode not supported on the "any" device
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 96 bytes
21:56:01.944826 IP 127.0.0.1.32768 > 127.0.0.1.5555: S 474258987:474258987(0) win 32767 <mss 16396,sackOK,timestamp 1148830800 0,nop,wscale 7>
21:56:01.944931 IP 127.0.0.1.5555 > 127.0.0.1.32768: . ack 4210751241 win 256<nop,nop,timestamp 1148830800 1148699191>
21:56:01.944989 IP 127.0.0.1.32768 > 127.0.0.1.5555: R 4210751241:4210751241(0) win 0
21:56:04.942581 IP 127.0.0.1.32768 > 127.0.0.1.5555: S 474258987:474258987(0) win 32767 <mss 16396,sackOK,timestamp 1148831550 0,nop,wscale 7>
21:56:04.942606 IP 127.0.0.1.5555 > 127.0.0.1.32768: S 479567568:479567568(0) ack 474258988 win 32767 <mss 16396,sackOK,timestamp 1148831550 1148831550,nop,wscale 7>
21:56:04.942616 IP 127.0.0.1.32768 > 127.0.0.1.5555: . ack 1 win 256 <nop,nop,timestamp 1148831550 1148831550>
21:56:04.943397 IP 127.0.0.1.32768 > 127.0.0.1.5555: F 1:1(0) ack 1 win 256 <nop,nop,timestamp 1148831550 1148831550>
21:56:04.946575 IP 127.0.0.1.5555 > 127.0.0.1.32768: . ack 2 win 256 <nop,nop,timestamp 1148831551 1148831550>
8 packets captured
24 packets received by filter
0 packets dropped by kernel
tsecer@harry: cat /proc/sys/net/ipv4/tcp_retries1
3
这里从tcpdump抓包可以看到,系统中一个客户端的连接比较曲折,第一次发送的syn包并没有收到对应的FIN包,而是一个序列号和自己SYN序列号相差极大的一个SEQ,然后客户端发送RST报文,导致对方重置,established socket释放。接下来3秒钟之后发送第二个SYN包,这个发送是由于写定时器超时导致的重传包,这个超时时间就是前面从 /proc/sys/net/ipv4/tcp_retries1中看到的值。
内核中客户端发送RST的判断逻辑为(服务器中代码由于比较简单,所以省略):
系统内代码linux-2.6.32.60
etipv4 cp_input.c:
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
……
if (th->ack) {
/* rfc793:
* "If the state is SYN-SENT then
* first check the ACK bit
* If the ACK bit is set
* If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
* a reset (unless the RST bit is set, if so drop
* the segment and return)"
*
* We do not send data with SYN, so that RFC-correct
* test reduces to:
*/
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_nxt)
goto reset_and_undo;
……
reset_and_undo:
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
return 1;
}
从tcp_rcv_synsent_state_process函数返回之后,发送RST包的代码:
tcp_rcv_synsent_state_process-->>tcp_rcv_state_process--->>tcp_v4_do_rcv
reset:
tcp_v4_send_reset(rsk, skb);
discard:
kfree_skb(skb);
/* Be careful here. If this function gets more complicated and
* gcc suffers from register pressure on the x86, sk (in %ebx)
* might be destroyed here. This current version compiles correctly,
* but you have been warned.
*/
return 0;