版本(version):指定所用 IP 协议的版本,该字段有效值为 4 或 6;
IP首部长度(IHL):定义首部的长度,由于选项数量可变;
服务区分:用于更复杂协议选项;
长度:指定分组的总长度,首部加数据的长度;
分片标识:标识一个分片的IP分组的各个部分;
TTL:指定从发送者到接收者的传输路径上中间站点的最大数目;
Proctocol 标识:IP 分组承载的高层协议(传输层);
检验和:根据首部和数据的内容计算;
IP首部所有的数值都以网络字节序存储(大端序)。
网络访问层仍受到传输介质的性质以及相关适配器的设备驱动程序的影响很大。网络层与网络适配器的硬件性质几乎是完全分离。
为什么说是几乎?该层不仅负责发送和接收数据,还负责在彼此不直接连接的系统之间转发和路由分组。
查找最佳路由并选择适当的网络设备来发送分组,也涉及对底层地址族的处理(如特定硬件MAC地址)。
ip_rcv
函数是网络层的入口点,分组向上穿过内核的过程如下:
接收分组及分组转发
1、接收分组
在分组转发到 ip_rcv
之后,必须检查接收到的信息确保它是正确的。主要检查计算的校验和与首部中存储的校验和是否一致。其他的检查包括分组是否达到了 IP 首部的最小长度,分组的协议是否确实是 IPv4。
在进行检查之后,内核并不立即继续对分组的处理,而是调用一个 netfilter
挂钩,使得用户空间可以对分组数据进行操作。netfilter
挂钩插入到内核源代码中定义好的各个位置,使得分组能够被外部动态操作。
深入理解Netfilter
流程:ip_rcv()
函数入口–>调用一个netfilter
挂钩–>ip_oute_input()
负责选择路由(判断路由的结果是选择一个函数进行一步分组处理)–>选择可用的函数ip_local_deliver()
和ip_forward()
【具体选择那个函数,取决于分组是交付到本地计算机下一个更高协议层例程(ip _local_deliver()
),还是转发到网络中另一个(ip_forward()
)】。
数据包在 Linux 内核中 netfilter 处理过程,其中有5个 HOOK 点执行:
- 数据报从进入系统进行IP检验以后,首先经过第一个HOOK函数
NF_IP_PRE_ROUTING
进行处理; - 然后再进入路由代码,具体决定该数据报是需要转发还是发给本机;
- 若数据报是被发到本机,由该数据经过HOOK函数
NF_IP_LOCAL_IN
处理以的然后传递给上层协议; - 若该数据报应该被转发则它被
NF_IP_FORWARD
处理;
- 若数据报是被发到本机,由该数据经过HOOK函数
- 经过转发的数据报经过最后个HOOK函数
NF_IP_POST_ROUTING
处理以后,再传输到网络上面; - 本地产生的数据经过HOOK函数
NF_IP_LOCAL_OUT
处理后,进行路由选择处理,然后经过NF_IP_POST_ROUTING
处理后发出去。
当一个IP包被接收到网卡中,函数iprcv()
被执行,具体源码如下:
/*
* Main IP Receive routine. 对IP头部合法性严格检査,然后把具体功能给 ip_rcv_finish 函数
*/
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.
*/
if (skb->pkt_type == PACKET_OTHERHOST)
goto drop;
// 关于网络层 SNMP 统计的信息,也可以通过netstat指令看到统计值信息
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;
}
// 确保 skb->data 指向内存包含的数据到少为 IP 头部大小,由于每个 IP 数据包包括 IP 分片必须包含一个完整的 IP 信息;
// 如果小于头部大小,则缺失的分部将从数据分片中拷贝。
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;
// pskb_may_pul1可能会调整skb中指针,所以需要重新定义IP头部
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
*/
// 检测IP首部长度及协议版本
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));
// 确保IP完整头部包括项在内存中相关信息
if (!pskb_may_pull(skb, iph->ihl*4))
goto inhdr_error;
iph = ip_hdr(skb);
// 验证IP头部的检验和
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
goto csum_error;
// 检测 IP 报文长度是否小于 skb->len
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).
*/
if (pskb_trim_rcsum(skb, len)) {
__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);
goto drop;
}
// 设置TCP报头指针
skb->transport_header = skb->network_header + iph->ihl*4;
/* Remove any debris in the socket control block */
// 删除任何套接字控制块碎片
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
IPCB(skb)->iif = skb->skb_iif;
/* Must drop socket now because of tproxy. */
// 因为tproxy,现在必须丢掉socket,tproxy是iptables的一附加控件,在mangle表的PREROUTING链中使用,不修改数据包包头
// 直接把数据传递给一个本地soket
skb_orphan(skb);
// 数据包在网络栈中传输过程,Netfilter子系统能够让你的5个挂接点注册回调函数,将指出这些挂接点名称
/*上面所有工作全部完成之后 ,就直接交给 NF_HOOK 管理,NF_HOOK 在做完 PRE_ROUTING 鍗选后,
PRE_ROUTING 点上注册的所有钩子都返回 NF_ACCEPT 才会执行后面的 ip_rcv_finish 函数,
然后继续执行路由等处理,如果是本地就会交给更高层的协议进行处理否则就执行 FORWARD*/
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
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;
}
2、分组转发
IP 分组可能如上所述交付给本地计算机处理,它们也可能离开互联网络层,转发到另一台计算机,而不牵涉本地计算机的高层协议实例。
分组的目标地址可分为以下两类:
(1) 目标计算机在某个本地网络中,发送计算机与该网络有连接;
(2) 目标计算机在地理上属于远程计算机,不连接到本地网络,只能通过网关访问。
第二种场景要复杂得多。首先必须找到剩余路由中的第一个站点,将分组转发到该站点,这是向最终目标地址的第一步传输。因此,不仅需要计算机所属本地网络结构的相关信息,还需要相邻网络结构和相关的外出路径的信息。
发送分组
内核提供几个通过互联网络层发送数据的函数,可由较高协议层使用。其中 ip_queue_xmit
是最常使用的一个,其代码流程图如下:
/* Note: skb->sk can be different from sk, in case of tunnels */
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
struct iphdr *iph;
int res;
/* Skip all of this if the packet is already routed,
* f.e. by something like SCTP.
*/
rcu_read_lock();
inet_opt = rcu_dereference(inet->inet_opt);
fl4 = &fl->u.ip4;
// 获取skb中的路由缓存
rt = skb_rtable(skb);
// skb中有缓存则跳转缓存
if (rt)
goto packet_routed;
/* Make sure we can route this packet. */
// 检查控制块中的路由缓存
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (!rt) { // 缓存过期
__be32 daddr;
/* Use correct destination address if we have options. */
daddr = inet->inet_daddr; // 目的地址
if (inet_opt && inet_opt->opt.srr) // 严格路由选项
daddr = inet_opt->opt.faddr;
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
// 查找路由缓存
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport,
inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS(sk),
sk->sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
// 设置控制块的路由缓存
sk_setup_caps(sk, &rt->dst);
}
// 将路由设置到skb中
skb_dst_set_noref(skb, &rt->dst);
packet_routed:
// 严格路由选项 使用网关 无路由
if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
// 加入ip头
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
skb_reset_network_header(skb);
// 构造ip头
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
/* Transport layer set skb->h.foo itself. */
// 构造ip选项
if (inet_opt && inet_opt->opt.optlen) {
iph->ihl += inet_opt->opt.optlen >> 2;
ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
}
// 设置id
ip_select_ident_segs(net, skb, sk,
skb_shinfo(skb)->gso_segs ?: 1);
/* TODO : should we use skb->sk here instead of sk ? */
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(net, sk, skb);
rcu_read_unlock();
return res;
no_route:
rcu_read_unlock();
IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}
EXPORT_SYMBOL(ip_queue_xmit);
ip_queue_xmit()
完成面向连接套接字的包输出,当套接字处于连接状态时,所有从套接字发出的包都具有确定的路由,无需为每一个输出包查询它的目的入口,可将套接字直接绑定到路由入口上,这由套接字的目的缓冲指针dst_cache
完成ip_queue_xmit
首先为输入包建立 IP 包头,经过本地包过滤器后,再将 IP 包分片输出(ip_fragment
)。是 ip 层提供给 tcp 层发送回调。
ip_build_and_send_pkt
函数是服务器端在给客户端回复syn+ack
时调用,该函数在构造ip
头之后,调用ip_local_out
发送数据包。
转移到网络访问层: ip_output
函数代码流程,其中根据分组是否需要分片。
通过网关访问
相邻网络结构和外出路径信息,该信息由路由表提供,路由表由内核通过多种数据结构实现并管理,在接收分组时调用的 ip_route_input
函数充当路由实现的接口,此方面因为为该函数能够识别出分组是交付到本地还是转发出去,另一个方面该函数能够找到通向目标地址的路由。目标地址存储在套接字缓冲区的 dst 字段当中。
ip forward 流程主要功能:根据报文信息得到路由、ipset安全检测、转发的基本逻辑,IP层提交本地处理的流程等,根据IP地址决定是提交给本地处理,还是报文转发的,报文转发的入口函数ip_forward
。
// 报文转发的入口函数
int ip_forward(struct sk_buff *skb)
{
u32 mtu;
struct iphdr *iph; /* Our header */
struct rtable *rt; /* Route we use */
struct ip_options *opt = &(IPCB(skb)->opt);
struct net *net;
/* that should never happen */
// 不允许处理非本host的报文,即报文目的 mac 是本机
if (skb->pkt_type != PACKET_HOST)
goto drop;
if (unlikely(skb->sk))
goto drop;
// 报文为非线性,gso_size 不为 0,但是 gso_type 为 0,丢弃此类报文
if (skb_warn_if_lro(skb))
goto drop;
if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb))
goto drop;
if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb))
return NET_RX_SUCCESS;
skb_forward_csum(skb);
net = dev_net(skb->dev);
/*
* 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 (ip_hdr(skb)->ttl <= 1) // ttl减至0,丢弃报文
goto too_many_hops;
if (!xfrm4_route_forward(skb)) // ipset路由安全规则检测,得到路由的信息
goto drop;
rt = skb_rtable(skb); // 得到路由表项
if (opt->is_strictroute && rt->rt_uses_gateway)
goto sr_failed;
IPCB(skb)->flags |= IPSKB_FORWARDED;
mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
if (ip_exceeds_mtu(skb, mtu)) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, // 文长度超过 mtu 且不允许 ip 分片,发送 icmp 消息给发送者
htonl(mtu));
goto drop;
}
/* We are about to mangle packet. Copy it! */
if (skb_cow(skb, LL_RESERVED_SPACE(rt->dst.dev)+rt->dst.header_len))
goto drop;
iph = ip_hdr(skb);
/* Decrease ttl after skb cow done */
ip_decrease_ttl(iph);
/*
* We now generate an ICMP HOST REDIRECT giving the route
* we calculated.
*/
if (IPCB(skb)->flags & IPSKB_DOREDIRECT && !opt->srr &&
!skb_sec_path(skb))
ip_rt_send_redirect(skb); // 通知发送端,路由重定向
skb->priority = rt_tos2priority(iph->tos); // 根据tos值计算priority
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
net, NULL, skb, skb->dev, rt->dst.dev,
ip_forward_finish); // 调用netfilter,实现iptables功能,通过后调用ip_forward_finish
sr_failed:
/*
* Strict routing permits no gatewaying
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
goto drop;
too_many_hops:
/* Tell the sender its packet died... */
__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
路由:在任何IP实现中,不仅在转发外部分组时需要,而且也用于发送本地计算机产生的分组。
每个接收到的分组属于3个类别之一:
(1)其目标是本地主机;
(2)其目标是当前主机直接连接的计算机;
(3)其目标是远程计算机,只能经由中间系统到达。
路由结果关联到一个套接字缓冲区,套接字缓冲区的 dst
成员指向一个dest_entry
结构的实例,此实例的内容是在路由查找期间填充的,具体内核源码如下:
struct neighbour
成员存储计算机在本地网络中的IP和硬件地址,这样就可以通过网络访问层直接到达。
neighbour
实例由内核中实现 ARP 的ARP层创建,ARP协议负责将IP地址转换为硬件地址。
tcp/ip 发包流程
https://blog.csdn.net/qq_53111905/article/details/126251324