前些日子在IDC实验docker的时候,为了避免与公司网络冲突,将bridge设置为127.x网段的IP,原以为这样就OK,后来发现在访问container内部的服务的时候无法访问。开始以为iptables的问题,搞了半天,后来,才发现系统对127.x.x.x的包根本不会经过bridge。这两天补习了一下linux的路由实现,才彻底明白其中缘由。
其实,关于环回接口,TCP/IP详解中已经描述得很清楚,只是自己没有去仔细阅读而已。
Linu支持环回接口( Loopback Interface),以允许运行在同一台主机上的客户程序和服务器程序通TCP/IP进行通信。 A 类网络127就是为环回接口预留的 。根据惯例,大多数系统把IP地址127.0.0.1分配给这个接口,并命名为localhost。一个传给环回接口的IP数据报不能在任何网络上出现。实际上,访问127.x.x.x的所有IP都是访问环回接口(lo)。
按理来说,一旦传输层检测到目的端地址是环回地址时,应该可以省略部分传输层和所有网络层的逻辑操作。但是大多数的产品还是照样完成传输层和网络层的所有过程,只是当 I P 数据报离开网络层时把它返回给自己。Linux的内核实现就是这样。
几个关键点:
(1)传给环回地址(一般是127.0.0.1 )的任何数据均作为IP输入。
(2)传给广播地址或多播地址的数据报复制一份传给环回接口,然后送到以太网上。这是因为广播传送和多播传送的定包含主机本身。
(3)任何传给该主机I P地址的数据均送到环回接口 。
从上面的描述可以明白,访问127.0.0.1和本机IP(比如192.168.1.10)都是通过lo来完成的。
考虑如下路由表:
尽管我们为172.16.213.0/24和129.0.0.0/8指定了出口设备(eth0/docker0),但实际上,数据仍然是通过lo来完成的。
内核默认有两个路由表(不考虑策略路由):
struct fib_table *ip_fib_local_table;
struct fib_table *ip_fib_main_table;
前者用于本地路由,后都可以由管理员配置。
从上面的可以看到,172.16.213.129,127.0.0.0/8都被认为是本机IP。
linux在进行路由查找时,先查找local,再查找main:
static inline int fib_lookup(const struct flowi *flp, struct fib_result *res) { if (ip_fib_local_table->tb_lookup(ip_fib_local_table, flp, res) && ip_fib_main_table->tb_lookup(ip_fib_main_table, flp, res)) return -ENETUNREACH; return 0; } |
实际上,如果内核认为目标地址是本机IP,就会将包的出口设备设置为loopback_dev(不管路由表将出口设备设置成什么)。
static int ip_route_output_slow(struct rtable **rp, const struct flowi *oldflp) { ... 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; dev_hold(dev_out); fl.oif = dev_out->ifindex; if (res.fi) fib_info_put(res.fi); res.fi = NULL; flags |= RTCF_LOCAL; goto make_route; } |
整个数据流过程:
我们顺着udp_sendmsg往下走: 在这个函数的主要作用是填充UDP头(源端口,目的端口等),接着调用了 ip_route_output,作用是查找出去的路由,而后: ... ip_build_xmit(sk, (sk->no_check == UDP_CSUM_NOXMIT ? udp_getfrag_nosum : udp_getfrag), &ufh, ulen, &ipc, rt, msg->msg_flags); ... ip_build_xmit函数的很大比例是生成sk_buff,并为数据包加入IP头。 后面有这么一句: ... NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,output_maybe_reroute); ... 简单的说,在没有防火墙代码干预的情况下,你可以将此处理解为直接调用output_maybe_reroute, (具体可参看绿盟月刊14期中的《内核防火墙netfilter入门 》) 而output_maybe_reroute中只有一句: return skb->dst->output(skb); 依旧照上面的方法(不过这个确实不太好找),发现其实这个指针是在ip_route_output中指定的, (提示:ip_route_output_slow中:rth->u.dst.output=ip_output;),ip_route_output的作用 便是查找路由,并将结果记录到skb->dst中。 于是,我们开始看ip_output函数了,而它马上又走向了ip_finish_output~~。 每个网络设备,如网卡,在内核中由一个net_device表示,在ip_finish_output中找到其用到的设备 (也是在ip_route_output中初始化的),这个参数在会传给netfilter在NF_IP_POST_ROUTING点登记 的函数,结束后调用ip_finish_output2,而这个函数中又会调用: ... hh->hh_output(skb); ... 闲话少叙,实际调用了dev_queue_xmit,到此我们完成了TCP/IP层的工作,开始数据链路层的处理。 在做了一些判断之后,实际的调用是这句: ... dev->hard_start_xmit(skb, dev); ... 这个函数是在网卡的驱动程序中定义的,每个不同的网卡有不同的处理,我的网卡是比较通用的3c509 (其驱动程序是3c509.c),在网卡处理化的时候(el3_probe),有: ... dev->hard_start_xmit = &el3_start_xmit; ... 再往下便是IO操作,将数据包真正的发到网络上去,至此发送过程结束。 中间我说的有些草率,完全没顾的上中间的如出错,阻塞,分片等特殊处理,只是将理想的过程描述出来。 这篇短文的目的也只是帮助大家建立个大致的印象,其实每个地方的都有非常复杂的处理(尤其是TCP部分)。 2.3 接受数据 当有数据到达网卡的时候,会产生一个硬件中断,然后调用网卡驱动程序中的函数来处理,对我的3c509网卡来说, 其处理函数为:el3_interrupt。(相应的IRQ号是在系统启动,网卡初始化时通过request_irq函数决定的。) 这个中断处理程序首先要做的当然就是进行一些IO操作将数据读入(读IO用inw函数),当数据帧成功接受后, 执行el3_rx(dev)进一步处理。 在el3_rx中,收到的数据报会被封装成struct sk_buff,并脱离驱动程序,转到通用的处理函数netif_rx (dev.c)中。为了CPU的效率,上层的处理函数的将采用软中断的方式激活,netif_rx的一个重要工作就 是将传入的sk_buff放到等候队列中,并置软中断标志位,然后便可放心返回,等待下一次网络数据包的到来: ... __skb_queue_tail(&queue->input_pkt_queue,skb); __cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ); ... 这个地方在2.2内核中一直被称为"底半"处理--bottom half,其内部实现基本类似,目的是快速的从中断中返回。 过了一段时间后,一次CPU调度会由于某些原因会发生(如某进程的时间片用完)。在进程调度函数即schedule() 中,会检查有没有软中断发生,若有则运行相应的处理函数: ... if (softirq_active(this_cpu) & softirq_mask(this_cpu)) goto handle_softirq; handle_softirq_back: ... ... handle_softirq: do_softirq(); goto handle_softirq_back; ... 在系统初始化的时候,具体说是在net_dev_init中,此软中断的处理函数被定为net_rx_action: ... open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL); ... 当下一次进程调度被执行的时候,系统会检查是否发生NET_TX_SOFTIRQ软中断,若有则调用net_rx_action。 net_tx_action函数既是2.2版本中的net_bh函数,在内核中有两个全局变量用来登记网络层的, 一个是链表ptype_all,另外一个是数组ptype_base[16],他们记载了所有内核能够处理 的第三层(按照OSI7层模型)协议。每个网络层的接收处理由一个 struct packet_type表示,而这个结构将通dev_add_pack函数将他们登记到ptype_all或 ptype_base中。只有packet_type中的type项为ETH_P_ALL时,才会登记到ptype_all链表 中,否则如ip_packet_type,会在数组ptype_base[16]找到相应的位置。两者不同点是 如果是以ETH_P_ALL类型登记,那么处理函数会受到所有类型的包,否则只能处理自己登记 的类型的。 skb->protocol是在el3_rx中赋值的,其实就是以太帧头信息中提取出的上层协议名,对 于我们的例子来说,这个值是ETH_P_IP,所以在net_tx_action中,会选择IP层的接收处理 函数,而从ip_packet_type 不难看出,这个函数便是ip_recv()。 pt_prev->func(实际指向ip_recv)前面有一个atomic_inc(&skb->users)操作(在2.2 内核中这个地方是一句skb_clone,原理类似),目的是增加这个sk_buff的引用数。网络层 的接收函数在处理完或因为某些原因要丢弃此sk_buff时(如防火墙)会调用kfree_skb, 而kfree_skb中首先会检查是否还有其他地方需要此函数,如果没有地方再用,才真正释放 此内存(__kfree_skb),否则只是计数器减一。 现在我们便来看看ip_recv(net/ipv4/ip_input.c)。这个函数的操作是非常清晰的: 首先检查这个包的合法性(版本号,长度,校验和等是否正确),如果合法则进行接下来 的处理。在2.4内核中,为了灵活处理防火墙代码,将原来的一个ip_recv分成了两部分, 即将将原来的的ip_recv的后半段独立出一个ip_rcv_finish函数。在ip_rcv_finish中, 一部分是带有IP选项(如源路由等)的IP包,例外就是通过ip_route_input查找路由, 并将结果记录到skb->dst中。此时接收到的包有两种,发往本地进程(需要传往上层协议) 或转发(用作网关时),此时需要的处理函数也不相同,如果传往本地,则调用ip_local_deliver (/net/ipv4/ip_input.c),否则调用ip_forward(/net/ipv4/ip_forward.c).skb->dst->input 这个函数指针会将数据报领上正确的道路。 对我们的例子而言,此时应该是调用ip_local_deliver的时候了。 发来的包很有可能是碎片包,这样的话则首先应该把它们组装好再传给上层协议,这当然也是 ip_local_deliver函数所做的第一份工作,如果组装成功(返回的sk_buff不为空),则继续处 理(详细的组装算法可参见绿盟月刊13期中的《IP分片重组的分析和常见碎片攻击》)。 但此时代码又被netfilter一分为二了,象前面一样,我们直接到后半段,即ip_local_deliver_finish (/net/ipv4/ip_input.c)中去。 传输层(如TCP,UDP,RAW)的处理被登记到了inet_protos中(通过inet_add_protocol)。 ip_local_deliver_finish会根据 IP头信息中的上层协议信息(即iph->protocol),调用相应的处理函数。为了简便,我们 采用了udp,此时的ipprot->handler实际便是udp_rcv了。 前面已经提到,在应用程序中建立的每个socket在内核中有一个struct socket/struct sock 对应。udp_rcv会通过udp_v4_lookup首先找到在内核中的sock,然后将其作参数调用 udp_queue_rcv_skb(/net/ipv4/udp.c)。马上,sock_queue_rcv_skb函数被调用, 此函数将sk_buff放入等待队列,然后通知上层数据到达: ... kb_set_owner_r(skb, sk); skb_queue_tail(&sk->receive_queue, skb); if (!sk->dead) sk->data_ready(sk,skb->len); return 0; ... sk->data_ready的定义在sock结构初始化的时候(sock_init_data): ... sk->data_ready=sock_def_readable; ... 现在我们便要从上往下看起了: 进程B要接收数据报,在程序里调用: ... read(sockfd,buff,sizeof(buff)); ... 此系统调用在内核中的函数是sys_read(fs/read_write.c)以下的处理类似write的操作, 不再详述.udp_recvmsg函数会调用skb_recv_datagram,如果数据还没有到达,且socket 设为阻塞模式时,进程会挂起(signal_pending(current)),直到data_ready通知进程 资源得到满足后继续处理(wake_up_interruptible(sk->sleep);)。