本文为作者原创,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
作者:misteryoung
博客:http://blog.chinaunix.net/uid/20706239.html
============================================================
1 前言
本文旨在介绍Linux作为3层设备(路由器)对收到的报文是如何处理的。
说明:本文的代码对应的内核版本为:Linux-2.6.32.11
2 分析
我们都知道,路由器根据报文的目的IP(不考虑策略路由的情况)的路由结果来决定报文的走向:
1)上送本地;
2)转发出去;
3)路由失败,报文被丢弃(回应ICMP差错报文);
下面针对这几种情况一次分析。
2.1 本地处理
对于上送本地的报文,3层(路由)处理完以后,需要交给4层处理,根据协议的不同,报文会被TCP/UDP/ICMP等模块处理。
2.1.1 TCP报文
对于TCP报文来说,内核首先会查找socket,若查找失败,会回复TCP reset报文;否则,若是协议报文(例如SYN或者FIN包),内核进行TCP状态机的处理,若是数据报文,则会将报文交给用户进程处理(放入收包队列,并唤醒相关进程)。
-
tcp_v4_rcv函数代码片段 --查找socket
-
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
-
if (!sk)
-
goto no_tcp_socket;
-
-
process:
-
if (sk->sk_state == TCP_TIME_WAIT)
-
goto do_time_wait
-
tcp_v4_rcv函数代码片段 --无相应的socket,则回复TCP reset
-
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
-
bad_packet:
-
TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
-
} else {
-
tcp_v4_send_reset(NULL, skb);
-
}
2.1.2 UDP报文
对于UDP报文来说,内核依然会查找socket,若查找失败,则回复ICMP的端口不可达报文;否则,或者将报文交给用户进程处理(放入收包队列,并唤醒相关进程),或者内核继续处理。
-
udp_rcv -> __udp4_lib_rcv函数代码片段 -- 查找socket
-
if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
-
return __udp4_lib_mcast_deliver(net, skb, uh,
-
saddr, daddr, udptable);
-
-
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
-
-
if (sk != NULL) {
-
int ret = udp_queue_rcv_skb(sk, skb);
-
sock_put(sk);
-
-
/* a return value > 0 means to resubmit the input, but
-
* it wants the return to be -protocol, or 0
-
*/
-
if (ret > 0)
-
return -ret;
-
return 0;
-
}
-
udp_rcv -> __udp4_lib_rcv函数代码片段 -- 无相应的socket,则回复端口不可达差错报文
-
UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
-
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
-
-
/*
-
* Hmm. We got an UDP packet to a port to which we
-
* don't wanna listen. Ignore it.
-
*/
-
kfree_skb(skb);
-
return 0;
下面说一下,哪些UDP报文到了4层,查找完socket后还需要内核进一步处理。
下面的代码展示了,udp_queue_rcv_skb函数确实会进行特殊处理,那么哪些报文需要特殊处理呢?
-
udp_rcv -> __udp4_lib_rcv -> udp_queue_rcv_skb函数代码片段
-
if (up->encap_type) {
-
/*
-
* This is an encapsulation socket so pass the skb to
-
* the socket's udp_encap_rcv() hook. Otherwise, just
-
* fall through and pass this up the UDP socket.
-
* up->encap_rcv() returns the following value:
-
* =0 if skb was successfully passed to the encap
-
* handler or was discarded by it.
-
* >0 if skb should be passed on to UDP.
-
* <0 if skb should be resubmitted as proto -N
-
*/
-
-
/* if we're overly short, let UDP handle it */
-
if (skb->len > sizeof(struct udphdr) &&
-
up->encap_rcv != NULL) {
-
int ret;
-
-
ret = (*up->encap_rcv)(sk, skb);
-
if (ret <= 0) {
-
UDP_INC_STATS_BH(sock_net(sk),
-
UDP_MIB_INDATAGRAMS,
-
is_udplite);
-
return -ret;
-
}
-
}
-
-
/* FALLTHROUGH -- it's a UDP Packet */
-
}
1)L2TP报文
对应的处理函数为pppol2tp_udp_encap_recv,该函数处理UDP报文内部的L2TP及PPP协议,然后根据PPP内部的数据决定是通过/dev/ppp将报文上送用户态,还是解封装后将报文重新入队列(调用netif_rx函数)。
2)IPSec
对应的处理函数为xfrm4_udp_encap_rcv,该函数在IPSec穿NAT时被注册。该函数将UDP剥掉以后,将内层数据交由ESP模块进一步处理。
2.1.3 ICMP报文
不同于TCP/UDP,ICMP报文不会查找socket或者不会首先查找socket。而是根据不同的type,内核调用不同的函数做不同的处理。
简单来说,ICMP共分为两类:
1、PING Request和Reply
1)对于Request报文,内核直接回复;而对于Reply会直接丢弃。
注:对高版本内核(3.0及以上)来说,Request报文,内核依然直接回复,而对于Reply,内核会查找socket,若成功,则将报文交给用户进程处理。因为高版本内核支持ping socket的创建,其流程大致如下是:
socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)
bind 指定报文的ID
send/recv 收发包
而对于低版本内核,只能通过socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)实现,并通过raw socket收包流程,将报文上送用户进程。当然,这与ICMP协议无关,无论4层是哪种协议(包括TCP/UDP),均可以将报文(保留IP头)通过raw socket机制上送用户进程。
2、ICMP差错报文
对差错报文,内核会解析内层协议,并根据内层协议调用相应的差错处理函数,后者根据原始报文(内层报文)查找socket,若查找失败,则不做任何处理,若成功则进一步处理。
-
icmp_rcv -> icmp_unreach函数代码片段
-
rcu_read_lock();
-
ipprot = rcu_dereference(inet_protos[hash]);
-
if (ipprot && ipprot->err_handler)
-
ipprot->err_handler(skb, info);
-
rcu_read_unlock();
TCP、UDP对应的差错处理函数(err_handler)分别为:tcp_v4_err,udp_err。这里不做介绍,感兴趣的同学可自行研究。而ICMP对应的err_handler指向空。
2.1.4 其他报文
内核注册基于IPv4(3层)的4层处理函数的函数是inet_add_protocol,搜索inet_add_protocol,你会发现内核注册了大量的处理函数,除了常见的TCP、UDP、ICMP,还有IGMP、GRE、AH、ESP等等。这里对这些报文的处理就不做介绍了,各位感兴趣的同学可以自行研究代码。
2.2 转发处理
这里要提一下,路由查找是如何影响报文的处理流程:
若是本地报文,则skb_dst(skb)->input被赋值为ip_local_deliver;
若是转发报文,则skb_dst(skb)->input被赋值为ip_forward;
若是路由失败,则skb_dst(skb)->input被赋值为ip_error;
在ip_rcv_finish最后调用dst_input(skb),因此上面的函数会被调用。
-
static inline int dst_input(struct sk_buff *skb)
-
{
-
return skb_dst(skb)->input(skb);
-
}
2.2.1 转发流程
这里简单列出来函数的调用关系,感兴趣的同学,可以根据该线索分析代码。
ip_rcv -> ip_rcv_finish -> ip_forward -> ip_forward_finish -> ip_output -> ip_finish_output -> ip_finish_output2 -> 交给ARP模块处理
2.2.2 ARP模块
ARP模块收到上层传递过来的报文(skb)后,会将报文缓存在neigh->arp_queue(skb_dst(skb)->neighbour指向neigh)中,并调用arp_solicit(neigh->ops->solicit指向该函数)发送ARP请求,并等待答复。收到回应报文后,会将学习到的MAC替换缓存skb的目的MAC,并发送出去。这样一个报文就被成功发送了出去。
2.3 路由失败处理
2.2章节中提到,skb_dst(skb)->input被赋值为ip_error,因此ip_error被调用,让我们看看该函数的具体实现:
-
static int ip_error(struct sk_buff *skb)
-
{
-
struct rtable *rt = skb_rtable(skb);
-
unsigned long now;
-
int code;
-
-
switch (rt->u.dst.error) {
-
case EINVAL:
-
default:
-
goto out;
-
case EHOSTUNREACH:
-
code = ICMP_HOST_UNREACH;
-
break;
-
case ENETUNREACH:
-
code = ICMP_NET_UNREACH;
-
IP_INC_STATS_BH(dev_net(rt->u.dst.dev),
-
IPSTATS_MIB_INNOROUTES);
-
break;
-
case EACCES:
-
code = ICMP_PKT_FILTERED;
-
break;
-
}
-
-
now = jiffies;
-
rt->u.dst.rate_tokens += now - rt->u.dst.rate_last;
-
if (rt->u.dst.rate_tokens > ip_rt_error_burst)
-
rt->u.dst.rate_tokens = ip_rt_error_burst;
-
rt->u.dst.rate_last = now;
-
if (rt->u.dst.rate_tokens >= ip_rt_error_cost) {
-
rt->u.dst.rate_tokens -= ip_rt_error_cost;
-
icmp_send(skb, ICMP_DEST_UNREACH, code, 0);
-
}
-
-
out: kfree_skb(skb);
-
return 0;
-
}
可以清楚的看到,ip_error回复了一个ICMP 网络不可达的差错报文。
3 总结
本文分析了,Linux作为路由器在收到报文后,可能存在的3种处理流程,分别是:
1)上送本地;
2)转发出去;
3)路由失败,报文被丢弃(回应ICMP差错报文)
这3种处理流程是由路由结果中的skb_dst(skb)->input所决定的。这里再强调一下skb_dst(skb)->input对应3个函数:
1)ip_local_deliver;
2)ip_forward;
3)ip_error;
阅读(7288) | 评论(0) | 转发(0) |