网络协议栈(3)server与connect的交互
一、侦听和连接
现在暂时不考虑网络拥塞问题,假设我们生活在新闻联播里,网络和谐,网速超快。现在一个好客的server在listen之后通过accept准备接受四面八方的朋友,此时就有一个客户端系统通过connect系统调用来连接这个服务器上的侦听套接口,我们暂时分析一下这个服务器的大致流程。当然,服务器在明,客户端在暗处,正所谓明枪易躲暗箭难防,这个服务器也是很多人希望攻击的对象,最为常见的就是DOS(Denial of Service)攻击,这种攻击从技术上说没有含量,但是通常是最为有效的,正所谓“乱箭射死英雄”,就是这个道理了。当了解了这个大致的过程之后,就可以回过头来看看这些攻击是怎么实现的。
二、服务器的listen系统调用
sys_listen--->>>inet_listen--->>>inet_csk_listen_start
这个调用链中一些关键的操作
1、inet_csk_listen_start:
其中进行了最为重要的一个操作,就是设置这个套接口为侦听状态,相当于说小店今天算是正式挂牌开张了。如果没有这个标志,所以向这个端口发送的套接口都不会被接受,因为那是私闯民宅啊。对应代码
/* There is race window here: we announce ourselves listening,
* but this transition is still not validated by get_port().
* It is OK, because this socket enters to hash table only
* after validation is complete.
*/
sk->sk_state = TCP_LISTEN;
2、inet_csk_listen_start--->>reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries)
该函数根据listen第二个参数backlog的值,在其中分配了一个listen_sock数据结构,并且根据系统调用传入的backlog值为这个结构分配了nr_table_entries(这个值并不一定是系统调用传入的原始值,从代码中看经过了很多的周折和取舍)个数的request_sock指针(这里只是动态分配了指针,但是没有分配实体)。
然后这个侦听的套接口icsk->icsk_accept_queue.listen_opt指针指向这个listen_sock结构的起始地址,而listen_sock则是用来保存大量的request_sock指针。当然,由于指针结构全部都是知道的,所以得到这些实体也不是难事。
注意的是:listen不会因为发送和接受而阻塞,它只是设置自己的状态,并申明自己可以等待的资源数,也就是更多的来说只是一个圈地的过程。
三、accept系统调用
这个函数是可能进行阻塞的,也可能定时阻塞,也可能无限阻塞。如果说这个accept系统调用的flags中设置了非阻塞模式,那么如果没有远端连接请求到达,那么此时就直接返回了;如果希望等待一定的时间,可以通过setsockopt中的SO_RCVTIMEO来修改该值;在大多数的默认情况下,这将是一个无限等待的过程。
1、同步等待及accept系统调用返回值的确定
sys_accept--->>inet_accept--->>>inet_csk_accept
其中核心的代码为
/* Find already established connection */
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
error = inet_csk_wait_for_connect(sk, timeo); 这个函数将会在sk->sk_sleep队列上进行休眠,具体的唤醒时机之后分析。
if (error)
goto out_err;
}
newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);从这里可以看到,此时是从这个icsk->icsk_accept_queue中提取一个远端的连接,加上上面我们看到的wait_for函数,可以看出这个套接口的创建并不是由这个接受函数来实现的,而是在网络报文到达之后由TCP协议栈创建的。由于协议栈不可能无限多的为这个侦听套接口执行来者不拒的接受,所以就要套接口提供一个接受的上限,而这个上限就是之前listen系统调用的第二个参数提供的内容。
2、accept系统调用int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)第二三个参数的确定
sys_accept-->>newsock->ops->getname(newsock, (struct sockaddr *)address,&len, 2)
这里调用的是新创建的套接口的方法而不是原始套接口的getname方法,传入的参数也是新创建的套接口,不过我可以先透个剧,新创建的套接口的getname函数指针就是inet_getname函数,这个函数的实现比较简单,也就是把新套接口中的目标地址和目标端口赋值给sockaddr_in结构,同时把协议簇赋值给sin->sin_family 。从inet_getname代码中看到的
sin->sin_port = inet->dport;
sin->sin_addr.s_addr = inet->daddr;
这里的dport表示服务器端的这个套接口发送的目的,所以是新接入的客户端的IP地址和端口号。
四、server对客户端connect系统调用的响应
大家注意了,这就是最为经典的“三次握手”了。大致的思想就是:客户端和服务器两侧都要发送一个SYN,并且要收到这个SYN的ACK。这个SYN中一个关键的内容就是它包含了一个开始的序列号,这个序列号的选取也有大学问,例如syncookie就是利用这个序列号来防止synflood攻击的,这是后话。由于这个过程太经典了,随便google了一下,发现了这个图片很nice,就用了一下,图片来自http://www3.gdin.edu.cn/jpkc/dzxnw/jsjkj/chapter3/35.htm <Figure 3.5-10: TCP three-way handshake: segment exchange>
从这个图片可以看到,除了syn ack标志位之外,就是两端友好的交换了一下序列号。
1、服务器接收到SYN报文
此时假设套接口已经通过listen侦听在了套接口上,此时侦听套接口处于TCP_LISTEN状态,然后又一个同步连接报文翩翩而至。这里我觉得画图比较麻烦,但是退而求其次的贴一下调用链,大家将就着当半个图片看就好了:
(gdb) bt
#0 reqsk_queue_hash_req (timeout=750, req=0xc1270660, hash=12,
queue=0xcfafdb98) at include/net/request_sock.h:252
#1 inet_csk_reqsk_queue_hash_add (timeout=750, req=0xc1270660, hash=12,
queue=0xcfafdb98) at net/ipv4/inet_connection_sock.c:395
#2 0xc0755db6 in tcp_v4_conn_request (sk=0xcfafd9c0, skb=0xcfaf15c0)
at net/ipv4/tcp_ipv4.c:1393
#3 0xc07f76f9 in tcp_v6_conn_request (sk=0xcfafd9c0, skb=0xcfaf15c0)
at net/ipv6/tcp_ipv6.c:1243
#4 0xc074550d in tcp_rcv_state_process (sk=0xcfafd9c0, skb=0xcfaf15c0,
th=0xcfa55034, len=40) at net/ipv4/tcp_input.c:4422
#5 0xc0757022 in tcp_v4_do_rcv (sk=0xcfafd9c0, skb=0xcfaf15c0)
at net/ipv4/tcp_ipv4.c:1584
#6 0xc0757fc0 in tcp_v4_rcv (skb=0xcfaf15c0) at net/ipv4/tcp_ipv4.c:1683
#7 0xc071678b in ip_local_deliver_finish (skb=0xcfaf15c0)
at net/ipv4/ip_input.c:236
#8 ip_local_deliver (skb=0xcfaf15c0) at net/ipv4/ip_input.c:275
#9 0xc07175db in dst_input (skb=0xcfaf15c0) at include/net/dst.h:242
#10 ip_rcv_finish (skb=0xcfaf15c0) at net/ipv4/ip_input.c:363
#11 ip_rcv (skb=0xcfaf15c0) at net/ipv4/ip_input.c:434
#12 0xc06eab0e in netif_receive_skb (skb=0xcfaf15c0) at net/core/dev.c:1840
#13 0xc06eac56 in process_backlog (backlog_dev=0xc1205200, budget=0xc09f3df4)
at net/core/dev.c:1874
这里值得注意的是:
①、tcp_v4_conn_request函数中执行了
req = reqsk_alloc(&tcp_request_sock_ops);
函数,这个函数的重要意义就在于它分配了一个重要的实体,就是一个request_sock结构实例,回想一下sys_listen中创建的ics结构,它的icsk->icsk_accept_queue.listen_opt志向的是一个listen_sock结构,而结构中包含了大连各的request_sock指针,此时分配的结构就会填充到这个结构中的指针成员中,从而供accept函数查询,更为重要的是它还要供供服务器接收到客户端ACK回应的时候查询。
这里有一个事实需要注意:就是当客户端connect发送syn之后,服务器只是创建了一个request_sock结构,但是并没有创建真正的网络栈结构sock结构,这个结构的创建将会被推迟。具体什么时候分配这个结构在之后说明。
②、分配的request_sock结构进入队列tcp_v4_conn_request-->>inet_csk_reqsk_queue_hash_add--->>>reqsk_queue_hash_req(&icsk->icsk_accept_queue, h, req, timeout)
static inline void reqsk_queue_hash_req(struct request_sock_queue *queue,
u32 hash, struct request_sock *req,
unsigned long timeout)
{
struct listen_sock *lopt = queue->listen_opt;
…………
req->dl_next = lopt->syn_table[hash];
…………
lopt->syn_table[hash] = req; 这里可以看到,相同的一个套接口上的请求套接口通过request_sock结构中的dl_next指针连接在一起。
……
}
③服务器端构造序列号
这个其实没什么好说的,只是图中有显示,也是三次握手中一个重要参数,所以可以顺便带过。
tcp_v4_conn_request-->>>tcp_v4_init_sequence
④向客户端回应SYN/ACK报文
tcp_v4_conn_request--->>tcp_v4_send_synack
这里服务器向客户端发送连接请求的回应报文,从上面的图中可以看到,这个报文的SYN和ACK均置位。这样服务器就开始等待客户端对自己的SYN进行回应,如果服务器收到对方的ACK,那这是就算成了,两方可以开始真正连接了。由于此时真正的sock结构尚未创建,所以此时仍然使用套接口爸爸的状态,也就是还是处于TCP_LISTEN状态。
2、服务器受到客户端回应的ACK报文
真正sock创建时的调用连
#0 inet_csk_clone (sk=0x0, req=0xc09f3028, priority=3231657964)
at net/ipv4/inet_connection_sock.c:495
#1 0xc075bfcd in tcp_create_openreq_child (sk=0xcfafc9e0, req=0xcf9e4680,
skb=0xcfae65c0) at net/ipv4/tcp_minisocks.c:379
#2 0xc0755ea6 in tcp_v4_syn_recv_sock (sk=0xcfafc9e0, skb=0xcfae65c0,
req=0xcf9e4680, dst=0xc133eb60) at net/ipv4/tcp_ipv4.c:1426
#3 0xc07f8492 in tcp_v6_syn_recv_sock (sk=0xcfafc9e0, skb=0xcfae65c0,
req=0xcf9e4680, dst=0x0) at net/ipv6/tcp_ipv6.c:1335
#4 0xc075cb7d in tcp_check_req (sk=0xcfafc9e0, skb=0xcfae65c0,
req=0xcf9e4680, prev=0xcfa0f65c) at net/ipv4/tcp_minisocks.c:652
#5 0xc075681e in tcp_v4_hnd_req (sk=0xcfafc9e0, skb=0xcfae65c0)
at net/ipv4/tcp_ipv4.c:1492
#6 0xc0756fc6 in tcp_v4_do_rcv (sk=0xcfafc9e0, skb=0xcfae65c0)
at net/ipv4/tcp_ipv4.c:1570
#7 0xc0757fc0 in tcp_v4_rcv (skb=0xcfae65c0) at net/ipv4/tcp_ipv4.c:1683
#8 0xc071678b in ip_local_deliver_finish (skb=0xcfae65c0)
at net/ipv4/ip_input.c:236
#9 ip_local_deliver (skb=0xcfae65c0) at net/ipv4/ip_input.c:275
①此次连接通讯(worker)sock的创建
在 inet_csk_clone函数中,创建了为此次连接而准备的套接口,这个也就是sys_accept将会返回的套接口,这个套接口复制了套接口爸爸的大部分内容。在这个inet_csk_clone函数中,其中设置了这个新创建的套接口为TCP_SYNC_RECV状态,这个是不同于套接口爸爸的TCP_LISTEN状态的。
newsk->sk_state = TCP_SYN_RECV;
这个函数中同时还记录了对方的端口号
inet_sk(newsk)->dport = inet_rsk(req)->rmt_port;
②、从TCP_SYN_RECV到TCP_ESTABLISED的转变
tcp_v4_do_rcv-->>tcp_child_process--->>>tcp_rcv_state_process
/* step 5: check the ACK field */
if (th->ack) {
int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);
switch(sk->sk_state) {
case TCP_SYN_RECV: 由于之前已经设置了新创建的套接口为TCP_SYN_RECV状态,所以此时经过这条路径,此时连个人终于对上暗号,接上头了。
if (acceptable) {
tp->copied_seq = tp->rcv_nxt;
smp_mb();
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);此时的这个sk是新创建的sock,所以它执行sock_def_wakeup时判断if (sk->sk_sleep && waitqueue_active(sk->sk_sleep))不会满足,所以不做任何操作就返回了。
③将新创建的sock添加到accept_queue中
tcp_check_req--->>>inet_csk_reqsk_queue_add(sk, req, child)--->>>reqsk_queue_add(&inet_csk(sk)->icsk_accept_queue, req, sk, child)
req->sk = child;
④对sys_accept的唤醒
tcp_child_process
if (state == TCP_SYN_RECV && child->sk_state != state)
parent->sk_data_ready(parent, 0);
注意,这里判断的是套接口爸爸,也就是sys_accept的第一个参数对应的sock,而不是新创建的sock,所以执行了sock_def_readable函数,从而将sys_accept系统调用唤醒。
五、sys_accept被唤醒之后
此时sys_accept函数在下面等待处被唤醒inet_csk_accept-->>inet_csk_wait_for_connect
if (reqsk_queue_empty(&icsk->icsk_accept_queue))
timeo = schedule_timeout(timeo);
然后对于新的套接口的获取路径为,
inet_csk_accept--->>>reqsk_queue_get_child(&icsk->icsk_accept_queue, sk)
struct request_sock *req = reqsk_queue_remove(queue);
struct sock *child = req->sk;
此时获得了一个刚才安装好的sock结构,并把这个结构返回给用户。
六、注意的一个细节
即使在执行sys_accept之后,套接口爸爸依然是出于TCP_LISTEN状态,这里所说的套接口状态变化都是在新套接口中执行的,这一点尤为重要,因为它关系着
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
}
流程的走向,而这个流程正式儿子套接口创建的关键路径。
六、例子
我们可以看到一个系统中可以在同一个端口上有很多套接口,这也就是说:当server创建一个socket的时候,服务器端的端口号和服务器的地址和原始的侦听地址相同,这些连接将会根据客户端的IP和端口进行区分。回头看看这个模式,和伪终端的实现有些相似,也是每次通过一个系统掉用来生成多个不同的实例,只是伪终端是通过对ptmx设备文件的打开实现,而此处由网络协议栈实现。
[tsecer@Harry ~]$ netstat -anp | sort | grep -e "\<23\>"
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:23 127.0.0.1:42974 ESTABLISHED -
tcp 0 0 127.0.0.1:23 127.0.0.1:42976 ESTABLISHED -
tcp 0 0 127.0.0.1:23 127.0.0.1:60355 ESTABLISHED -
tcp 0 0 127.0.0.1:42974 127.0.0.1:23 ESTABLISHED -
tcp 0 0 127.0.0.1:42976 127.0.0.1:23 ESTABLISHED 14768/telnet
tcp 0 0 127.0.0.1:60355 127.0.0.1:23 ESTABLISHED 14860/telnet
tcp 0 0 192.168.203.155:23 192.168.203.155:48224 ESTABLISHED -
tcp 0 0 192.168.203.155:48224 192.168.203.155:23 ESTABLISHED 14681/telnet
tcp 0 0 :::23 :::* LISTEN -
七、遗留问题
如果客户端在发送了SYNC并接收到服务器的SYN+ACK之后不再给予服务器任何回应,此时服务器将会有什么行为。
syn-flood攻击的原理以及服务器的syn-cookie原理。