IP 层收发报文简要剖析1-ip报文的输入
ip层数据包处理场景如下:
网络层处理数据包文时需要和路由表以及邻居系统打交道。输入数据时,提供输入接口给链路层调用,并调用传输层的输入接口将数据输入到传输层。
在输出数据时,提供输出接口给传输层,并调用链路层的输出接口将数据输出到链路层。在输入输出数据时,需要查找路由表 通过netfiler处理等操作。
一、ip数据报文输入 ip_rcv &ip_rcv_finish &ip_rcv_finish2
1、在inet_init中注册了类型为ETH_P_IP协议的数据包的回调ip_rcv
2、ip_rcv处理完成后交给 NETFILTER(PRE-ROUTING)过滤,再上递给 ip_rcv_finish(),
3、ip_rcv_finish根据 skb 包中的路由信息,决定这个数据包是转发还是上交给本机,由此产生两条路径,一为 ip_local_deliver(),另一个为ip_forward()
4、ip_local_deliver它首先检查这个包是否是一个分 片 包 , 如 果 是 , 它 要 调 动 ip_defrag() 将 分 片 重 装 , 然 后 再 次 将 包 将 给 NETFILTER(LOCAL_IN)过滤后,
再由 ip_local_deliver_finish()将数据上传到 L4 层,这样就完成了 IP 层的处理;它负责将数据上传
5、ip_forward(),它负责将数据转发,经由 NETFILTER(FORWARD)过滤后将给 ip_forward_finish(),然后调用 dst_output()将数据包发送出去。
/* * Main IP Receive routine. */ int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { const struct iphdr *iph; struct net *net; u32 len; /* When the interface is in promisc. mode, drop all the crap * that it receives, do not try to analyse it. */ /*数据包不是发给我们的,这里所说的“不属于”这个主机, 是指在这个包目标主机的MAC地址不是本机,而不是L3层的ip地址*/ if (skb->pkt_type == PACKET_OTHERHOST) goto drop; net = dev_net(dev); __IP_UPD_PO_STATS(net, IPSTATS_MIB_IN, skb->len); skb = skb_share_check(skb, GFP_ATOMIC); if (!skb) { __IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS); goto out; } if (!pskb_may_pull(skb, sizeof(struct iphdr))) goto inhdr_error; iph = ip_hdr(skb); /* * RFC1122: 3.2.1.2 MUST silently discard any IP frame that fails the checksum. * * Is the datagram acceptable? * * 1. Length at least the size of an ip header * 2. Version of 4 * 3. Checksums correctly. [Speed optimisation for later, skip loopback checksums] * 4. Doesn't have a bogus length */ if (iph->ihl < 5 || iph->version != 4) goto inhdr_error; BUILD_BUG_ON(IPSTATS_MIB_ECT1PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_1); BUILD_BUG_ON(IPSTATS_MIB_ECT0PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_0); BUILD_BUG_ON(IPSTATS_MIB_CEPKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_CE); __IP_ADD_STATS(net, IPSTATS_MIB_NOECTPKTS + (iph->tos & INET_ECN_MASK), max_t(unsigned short, 1, skb_shinfo(skb)->gso_segs)); /* 对数据报的头长度进行检查 iph->ihl*4是20,是首部最长的长度, 此语句是说如果头部长度不能pull,则error */ if (!pskb_may_pull(skb, iph->ihl*4)) goto inhdr_error; iph = ip_hdr(skb); /* 校验和错误 */ if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl))) goto csum_error; /* 取总长度 */ len = ntohs(iph->tot_len); if (skb->len < len) { __IP_INC_STATS(net, IPSTATS_MIB_INTRUNCATEDPKTS); goto drop; } else if (len < (iph->ihl*4)) goto inhdr_error; /* Our transport medium may have padded the buffer out. Now we know it * is IP we can trim to the true length of the frame. * Note this now means skb->len holds ntohs(iph->tot_len). */ /* 设置总长度为ip包的长度//根据ip包总长度, 重新计算skb的长度,去掉末尾的无用信息 */ if (pskb_trim_rcsum(skb, len)) { __IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS); goto drop; } /*获取传输层的头部*/ skb->transport_header = skb->network_header + iph->ihl*4; /* Remove any debris in the socket control block */ //这里面后面会存ip填充信息 memset(IPCB(skb), 0, sizeof(struct inet_skb_parm)); IPCB(skb)->iif = skb->skb_iif; /* 保存输入设备信息 */ /* Must drop socket now because of tproxy. */ skb_orphan(skb); /* * 最后通过netfilter模块处理后,调用ip_rcv_finish() * 完成IP数据包的输入。 */ return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb, dev, NULL, ip_rcv_finish);//hook 在nf_register_hooks注册 csum_error: __IP_INC_STATS(net, IPSTATS_MIB_CSUMERRORS); inhdr_error: __IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS); drop: kfree_skb(skb); out: return NET_RX_DROP; }
ip_rcv_finish()在ip_rcv()中当IP数据包经过netfilter模块 处理后被调用。完成的主要功能是,如 还没有为该数据包查找输入路由缓存,则 调用ip_route_input()为其查找输入路由缓存。
接着处理IP数据包首部中的选项,最后 根据输入路由缓存输入到本地或转发。
/* * ip_rcv_finish()在ip_rcv()中当IP数据包经过netfilter模块 * 处理后被调用。完成的主要功能是,如果 * 还没有为该数据包查找输入路由缓存,则 * 调用ip_route_input()为其查找输入路由缓存。 * 接着处理IP数据包首部中的选项,最后 * 根据输入路由缓存输入到本地或转发发。 */ static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb) { const struct iphdr *iph = ip_hdr(skb); struct rtable *rt; /* if ingress device is enslaved to an L3 master device pass the * skb to its handler for processing */ skb = l3mdev_ip_rcv(skb); if (!skb) return NET_RX_SUCCESS; /* 启用了early_demux skb路由缓存为空 skb的sock为空 不是分片包 当内核接收到一个TCP数据包来说,首先需要查找skb对应的路由, 然后查找skb对应的socket。David Miller 发现这样做是一种浪费, 对于属于同一个socket(只考虑ESTABLISHED情况)的路由是相同的, 那么如果能将skb的路由缓存到socket(skb->sk)中,就可以只查找查找一次skb所属的socket, 就可以顺便把路由找到了,于是David Miller提交了一个patch ipv4: Early TCP socket demux 添加的这个patch是有局限的,因为这个处理对于转发的数据包, 增加了一个在查找路由之前查找socket的逻辑,可能导致转发效率的降低。 Alexander Duyck提出增加一个ip_early_demux参数来控制是否启动这个特性 */ if (net->ipv4.sysctl_ip_early_demux && !skb_dst(skb) && !skb->sk && !ip_is_fragment(iph)) { const struct net_protocol *ipprot; int protocol = iph->protocol; /* 找到上层协议 */ /* 获取协议对应的prot */ ipprot = rcu_dereference(inet_protos[protocol]); /* 找到early_demux函数,如tcp_v4_early_demux */ if (ipprot && ipprot->early_demux) { /* 调用该函数,将路由信息缓存到skb->refdst对于tcp 就是关联skb和socket */ ipprot->early_demux(skb); /* must reload iph, skb->head might have changed */ /* 重新取ip头 */ iph = ip_hdr(skb); } } /* * Initialise the virtual path cache for the packet. It describes * how the packet travels inside Linux networking. */ /* * 如果还没有为该数据包查找输入路由缓存, * 则调用ip_route_input_noref()为其查找输入路由缓存。 * 若查找失败,则将该数据包丢弃。 */ if (!skb_valid_dst(skb)) { int err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, skb->dev); if (unlikely(err)) { if (err == -EXDEV) __NET_INC_STATS(net, LINUX_MIB_IPRPFILTER); goto drop; } } #ifdef CONFIG_IP_ROUTE_CLASSID if (unlikely(skb_dst(skb)->tclassid)) { struct ip_rt_acct *st = this_cpu_ptr(ip_rt_acct); u32 idx = skb_dst(skb)->tclassid; st[idx&0xFF].o_packets++; st[idx&0xFF].o_bytes += skb->len; st[(idx>>16)&0xFF].i_packets++; st[(idx>>16)&0xFF].i_bytes += skb->len; } #endif /* * 根据长度判断IP首部中是否存在选项,如果有, * 则调用ip_rcv_options()处理IP选项。 */ if (iph->ihl > 5 && ip_rcv_options(skb)) goto drop; /* * 最后根据输入路由缓存决定输入到本地或 * 转发,最终前者调用ip_local_deliver(),后者调用 * ip_forward()。 * 对于输入到本地或转发的组播报文,在经过netfilter处理 * 之后会调用ip_rcv_finish()正式进入输入的处理。先调用 * ip_route_input()进行输入路由的查询,如果发现目的地址 * 为组播地址,就会按照组播地址的规则查找路由,查找 * 到组播的输入路由后,组播报文接收处理函数为ip_mr_input()。 */ rt = skb_rtable(skb); if (rt->rt_type == RTN_MULTICAST) { __IP_UPD_PO_STATS(net, IPSTATS_MIB_INMCAST, skb->len); } else if (rt->rt_type == RTN_BROADCAST) { __IP_UPD_PO_STATS(net, IPSTATS_MIB_INBCAST, skb->len); } else if (skb->pkt_type == PACKET_BROADCAST || skb->pkt_type == PACKET_MULTICAST) { struct in_device *in_dev = __in_dev_get_rcu(skb->dev); /* RFC 1122 3.3.6: * * When a host sends a datagram to a link-layer broadcast * address, the IP destination address MUST be a legal IP * broadcast or IP multicast address. * * A host SHOULD silently discard a datagram that is received * via a link-layer broadcast (see Section 2.4) but does not * specify an IP multicast or broadcast destination address. * * This doesn't explicitly say L2 *broadcast*, but broadcast is * in a way a form of multicast and the most common use case for * this is 802.11 protecting against cross-station spoofing (the * so-called "hole-196" attack) so do it for both. */ if (in_dev && IN_DEV_ORCONF(in_dev, DROP_UNICAST_IN_L2_MULTICAST)) goto drop; } /* 调用路由项的input函数,可能为ip_local_deliver或者ip_forward */ return dst_input(skb); drop: kfree_skb(skb); return NET_RX_DROP; }
tcp_v4_early_demux:将关联sock中的sk_rx_dst 缓存到skb复用

void tcp_v4_early_demux(struct sk_buff *skb) { const struct iphdr *iph; const struct tcphdr *th; struct sock *sk; if (skb->pkt_type != PACKET_HOST) return; if (!pskb_may_pull(skb, skb_transport_offset(skb) + sizeof(struct tcphdr))) return; iph = ip_hdr(skb); th = tcp_hdr(skb); if (th->doff < sizeof(struct tcphdr) / 4) return; sk = __inet_lookup_established(dev_net(skb->dev), &tcp_hashinfo, iph->saddr, th->source, iph->daddr, ntohs(th->dest), skb->skb_iif); if (sk) { skb->sk = sk; skb->destructor = sock_edemux; if (sk_fullsock(sk)) { struct dst_entry *dst = READ_ONCE(sk->sk_rx_dst); if (dst) dst = dst_check(dst, 0); if (dst && inet_sk(sk)->rx_dst_ifindex == skb->skb_iif) skb_dst_set_noref(skb, dst); } } }
ip_rcv_finish 函数最终会调用ip_route_input函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
1、如果是发到本机的话,调用ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用ip_local_deliver函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。
2、如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用dst_input函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用 dev_queue_xmit,进入链路层处理流程。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!