为什么ip层收到的报文可能已经设置了路由

一、收到报文对于网络地址的判断
对于刚收到的网络数据,经过了NF_IP_PRE_ROUTING过滤之后,开始到达了ip_rcv_finish函数,在该函数的开始做了一个看起来比较诡异的操作,就是判断了这个数据包中的路由dst是否已经设置过了,如果没有设置过则进行路由;这也就是反过来说,一些收到的报文是可能已经设置过了了路由。那么为什么会有这样的报文,它们从哪里来,又是在哪里设置了这些路由项?
static inline int ip_rcv_finish(struct sk_buff *skb)
{
struct iphdr *iph = skb->nh.iph;
 
/*
 * Initialise the virtual path cache for the packet. It describes
 * how the packet travels inside Linux networking.
 */
if (skb->dst == NULL) {//在什么情况下这个指针非空
int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,
 skb->dev);
if (unlikely(err)) {
if (err == -EHOSTUNREACH)
IP_INC_STATS_BH(IPSTATS_MIB_INADDRERRORS);
goto drop;
}
}
……
}        
二、一个典型的场景
其实这个最为典型的场景发生在一个机器上的不同进程通过tcp通讯数据,也就是说当一个报文的发送目的地是本机的时候,此时这个报文就不再是从机器外部过来的数据,而是从本机内部的数据,所以它的目的地址在output的时候已经被设置过了,当然,如果你ping一个本地的loopback网卡设备,同样也是下面说的流程。假设说发送的目的地是在本机,此时经过的路由为
ip_route_output_flow-->>__ip_route_output_key-->>ip_route_output_slow-->>
if (res.type == RTN_LOCAL) {
if (!fl.fl4_src)
fl.fl4_src = fl.fl4_dst;
if (dev_out)
dev_put(dev_out);
dev_out = &loopback_dev;
如果网络发送的目的地址经过路由表判断之后为一个本地的地址,则此时“发送”时使用的网卡就是虚拟的loopback网卡,
ip_route_output_slow-->>ip_mkroute_output-->>ip_mkroute_output_def-->>__mkroute_output
rth->u.dst.output=ip_output;
 
RT_CACHE_STAT_INC(out_slow_tot);
 
if (flags & RTCF_LOCAL) {
rth->u.dst.input = ip_local_deliver;
rth->rt_spec_dst = fl->fl4_dst;
}ip_output-->>ip_finish_output-->>ip_finish_output2-->>dev_queue_xmit-->>dev_hard_start_xmit-->>loopback_xmit
static int loopback_xmit(struct sk_buff *skb, struct net_device *dev)
{
……
/* it's OK to use __get_cpu_var() because BHs are off */
lb_stats = &__get_cpu_var(pcpu_lstats);
lb_stats->bytes += skb->len;
lb_stats->packets++;
 
netif_rx(skb);再次掉头进入本机的网络协议栈
 
return 0;
}
也就是这里的确使用了一个“具体的”网卡loopback将数据“发送”出去,但是这个设备非常特殊,它的发送接口是将所有被要求发送的报文进行了一个180°大转弯,调转netif_rx之后一头再次扎进了系统的协议栈中,这里的报文结构并没有作任何修改,所以它的dst指针依然有效,它的输入接口依然保留着在__mkroute_output函数中设置的方法,也就是ip_local_deliver,所以在ip_rcv_finish-->>dst_input
/* Input packet from network to transport.  */
static inline int dst_input(struct sk_buff *skb)
{
int err;
 
for (;;) {
err = skb->dst->input(skb);
 
if (likely(err == 0))
return err;
/* Oh, Jamal... Seems, I will not forgive you this mess. :-) */
if (unlikely(err != NET_XMIT_BYPASS))
return err;
}
}
此时的skb->dst->input方法就是__mkroute_output中设置的ip_local_deliver。
三、再看一个“反面”的例子
除了loopback这样的设备之外,系统中还有一些其他的设备类型也是需要进行这么一番周折的,例如我们在lvs可能使用的ipip设备。它的数据包在系统协议栈中的流转也比较坎坷,因为它包含了两个iphdr(想起了《国产零零七》中的“痰盂子母弹”),这意味着这个数据包也有机会两次进入系统协议栈,此时它是否也存在这样的问题呢?对于ipip协议数据的处理主要在函数ipip_rcv中完成:
if ((tunnel = ipip_tunnel_lookup(iph->saddr, iph->daddr)) != NULL) {
……
dst_release(skb->dst);
skb->dst = NULL;
                ……
           netif_rx(skb);
 
return 0;
}
由于这里的例子并不适应,所以对于需要再次进入系统网络协议栈的报文,这里ipip进行了主动的有意识的重新归零,也就是为了避这个报文再次进入协议栈时有残留的路由信息,而这个信息又是一个错误的路由信息,所以这个地方要主动的将这个路由信息清除。
四、路由转发对于traceroute实现的支持
在看这个代码的时候,顺便看到了对于报文转发的处理。其实这个代码处理非常简单,就是对于过了的数据包进行路由计算并转发。这个数据包进入内核协议栈之后将会被设置为ip_forward,这个函数的功能其实非常简单,因为__mkroute_input已经为它选好了output使用的dev和gateway信息,所以它只是执行了ip_forward函数就可以返回了。在这个简短的函数中,除了执行我们最为关心的netfilter的钩子调用之外,它还有一个重要的功能就是递减这个被转发的报文的生存期,也就是ttl(Time To Live)字段。当这个值减少为1之后,这个报文将会被丢弃,并且给谁发送源发送一个ICMP_TIME_EXCEEDED类型的icmp报文。据说在windows下的troucert,linux下的traceroute工具都是利用了这个特性来进行路由检测,方法就是以1为ttl初始值来发送探测数据包,然后侦听回报中的ICMP_TIME_EXCEEDED报文,通过这样来逐渐尝试到达目的地址经过的各个站点。
int ip_forward(struct sk_buff *skb)
{
……
/*
 * According to the RFC, we must first decrease the TTL field. If
 * that reaches zero, we must reply an ICMP control message telling
 * that the packet's lifetime expired.
 */
if (skb->nh.iph->ttl <= 1)
goto too_many_hops;
……
/* Decrease ttl after skb cow done */
ip_decrease_ttl(iph);
……
too_many_hops:
/* Tell the sender its packet died... */
IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
        
在icmp_send的回包中,使用的源地址是检测到ttl归零的主机地址(而不是被转发报文的最终目的地址)。这个转换在icmp_send中可以看到:
void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info)
{
……
/*
 * Construct source address and options.
 */
 
saddr = iph->daddr;
if (!(rt->rt_flags & RTCF_LOCAL)) {
if (sysctl_icmp_errors_use_inbound_ifaddr)
saddr = inet_select_addr(skb_in->dev, 0, RT_SCOPE_LINK);
else
saddr = 0;
}                                                
……
struct flowi fl = {
.nl_u = {
.ip4_u = {
.daddr = icmp_param.replyopts.srr ?
icmp_param.replyopts.faddr :
iph->saddr,
.saddr = saddr,//如果源地址为0,则由路由层自主选择出口网卡的IP地址
.tos = RT_TOS(tos)
}
},
.proto = IPPROTO_ICMP,
.uli_u = {
.icmpt = {
.type = type,
.code = code
}
}
};
在最终的发送接口中,其填充的源地址为ip_push_pending_frames
 ……
  iph->ttl = ttl;
iph->protocol = sk->sk_protocol;
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
ip_send_check(iph);
……
五、系统默认TTL的值
static void rt_set_nexthop(struct rtable *rt, struct fib_result *res, u32 itag)
if (rt->u.dst.metrics[RTAX_HOPLIMIT-1] == 0)
rt->u.dst.metrics[RTAX_HOPLIMIT-1] = sysctl_ip_default_ttl;
                
tsecer@harry: cat /proc/sys/net/ipv4/ip_default_ttl 
64
通过tcpdump抓一个ping本地地址的数据包,可以看到ttl为默认的64
08:43:38.626117 IP (tos 0x0, ttl 64, id 31217, offset 0, flags [DF], proto UDP (17), length 118)        

posted on 2019-03-07 09:51  tsecer  阅读(417)  评论(0编辑  收藏  举报

导航