TCP实现之:L3的IP协议
1. 报文的接收
从上一篇文章TCP实现之:Hellow World我们已经知道了,网卡驱动(也可以理解为L2层)是通过IP协议层注册的ip_packet_type
来讲数据传递给IP层处理的,其处理函数为ip_rcv
,下面我们来重点分析一下该函数。
ip_rcv
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct net *net = dev_net(dev);
/* skb主要的校验函数,包括丢弃非发往本机的数据包(混杂模式)、IP报头校验等 */
skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;
/* 调用防火墙的钩子,在通过防火墙规则后将报文传递给ip_rcv_finish函数 */
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
ip_rcv_core
static struct sk_buff *ip_rcv_core(struct sk_buff *skb, struct net *net)
{
const struct iphdr *iph;
u32 len;
/* 网卡混杂模式,丢弃非本机的数据报。此时该本文在L2已经经历了报文嗅探器,
* 没啥用了,可以丢弃。
*/
if (skb->pkt_type == PACKET_OTHERHOST)
goto drop;
/* 信息统计 */
__IP_UPD_PO_STATS(net, IPSTATS_MIB_IN, skb->len);
/* 检测skb的引用计数。skb可能被其他地方使用,如L2层的嗅探器等。如果被其他
* 地方使用,即引用计数>1,则创建一份skb的副本来使用
*/
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb) {
__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);
goto out;
}
/* IP头部完整性校验 */
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;
iph = ip_hdr(skb);
/* 丢弃头部不完整或者非IPv4版本的数据报 */
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));
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).
*/
if (pskb_trim_rcsum(skb, len)) {
__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);
goto drop;
}
iph = ip_hdr(skb);
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. */
skb_orphan(skb);
return skb;
csum_error:
__IP_INC_STATS(net, IPSTATS_MIB_CSUMERRORS);
inhdr_error:
__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
drop:
kfree_skb(skb);
out:
return NULL;
}
ip_rcv_finish
下面我们来分析一下真正的IP报文接收处理函数-ip_rcv_finish
。这个函数会先调用ip_rcv_finish_core
函数对skb做一些初始化的工作,然后调用dst_input
来根据skb的路由对数据报进行进一步的处理。
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
int ret;
/* 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;
ret = ip_rcv_finish_core(net, sk, skb, dev);
if (ret != NET_RX_DROP)
调用
ret = dst_input(skb);
return ret;
}
ip_rcv_finish_core
ip_rcv_finish_core
函数所做的工作主要包括:(1)判断是否需要提前分流;(2)初始化skb路由;(3)处理IP选项。
static int ip_rcv_finish_core(struct net *net, struct sock *sk,
struct sk_buff *skb, struct net_device *dev)
{
const struct iphdr *iph = ip_hdr(skb);
int (*edemux)(struct sk_buff *skb);
struct rtable *rt;
int err;
/* 提前分流 */
if (net->ipv4.sysctl_ip_early_demux &&
!skb_dst(skb) &&
!skb->sk &&
!ip_is_fragment(iph)) {
const struct net_protocol *ipprot;
/* IP的上层协议 */
int protocol = iph->protocol;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot && (edemux = READ_ONCE(ipprot->early_demux))) {
/* 调用上层协议的提前分流函数,如tcp_v4_early_demux */
err = edemux(skb);
if (unlikely(err))
goto drop_error;
/* 重新加载IP头部 */
iph = ip_hdr(skb);
}
}
/*
* 初始化skb的路由缓存。这决定了skb在协议栈中的处理方式,是deliver到上层协议
* 还是进行ip_forward转发等。
* /
if (!skb_valid_dst(skb)) {
err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
iph->tos, dev);
if (unlikely(err))
goto drop_error;
}
#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选项不像TCP选项那样,很少被用到 */
if (iph->ihl > 5 && ip_rcv_options(skb))
goto drop;
/* 找到skb的路由表 */
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(dev);
if (in_dev &&
IN_DEV_ORCONF(in_dev, DROP_UNICAST_IN_L2_MULTICAST))
goto drop;
}
return NET_RX_SUCCESS;
drop:
kfree_skb(skb);
return NET_RX_DROP;
drop_error:
if (err == -EXDEV)
__NET_INC_STATS(net, LINUX_MIB_IPRPFILTER);
goto drop;
}
ip_local_deliver
dst_input(skb)
函数会调用skb路由选项的input
函数,该函数在初始化路由缓存时确定,可能为ip_local_deliver
或者ip_forward
。这里我们先分析一下ip_local_deliver
函数,该函数顾名思义,就是将skb交给上层协议的。
int ip_local_deliver(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
/* 当skb为IP分片时,对其进行重组 */
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
/* 调用防火墙的钩子,并在通过验证后调用ip_local_deliver_finish */
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
ip_local_deliver_finish
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
__skb_pull(skb, skb_network_header_len(skb));
rcu_read_lock();
{
/* 获取L4协议 */
int protocol = ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
int raw;
resubmit:
/* 处理原始套接字(即SOCK_RAW) */
raw = raw_local_deliver(skb, protocol);
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
/*
* 这里我们可以看到,即使使用的是SOCK_RAW,在将报文交给套接字处理函数
* 处理后,还是会将报文继续向上层协议传递。
*/
int ret;
if (!ipprot->no_policy) {
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
kfree_skb(skb);
goto out;
}
nf_reset(skb);
}
/* 调用L4的skb处理函数 */
ret = ipprot->handler(skb);
if (ret < 0) {
protocol = -ret;
goto resubmit;
}
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
} else {
if (!raw) {
/* 找不到处理协议,向发送方发送一条ICMP消息,告诉它一声 */
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
__IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH,
ICMP_PROT_UNREACH, 0);
}
kfree_skb(skb);
} else {
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
}
out:
rcu_read_unlock();
return 0;
}
从上面可以看出,在将报文向上层协议传递时,它会找到L4协议注册到IP层的struct net_protocol
,注册函数为inet_add_protocol
。以TCP协议为例,其定义为:
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
2. 报文的发送
__ip_make_skb
首先我们来讲一下__ip_make_skb
这个函数。在进行报文发送时,一般待发送的报文都会放在套接字的sk_write_queue
发送队列等待发送。而__ip_make_skb
函数的作用正是将队列中所有的IP片段合并成一个IP数据报,然后将其从队列中取出以进行发送。
struct sk_buff *__ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork)
{
struct sk_buff *skb, *tmp_skb;
struct sk_buff **tail_skb;
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options *opt = NULL;
struct rtable *rt = (struct rtable *)cork->dst;
struct iphdr *iph;
__be16 df = 0;
__u8 ttl;
/* 取出队列中的一个skb */
skb = __skb_dequeue(queue);
if (!skb)
goto out;
tail_skb = &(skb_shinfo(skb)->frag_list);
/* move skb->data to ip header from ext header */
if (skb->data < skb_network_header(skb))
__skb_pull(skb, skb_network_offset(skb));
/* 下面的循环将queue队列中的skb一个个地取出,并加到skb的frag_list链表的尾部 */
while ((tmp_skb = __skb_dequeue(queue)) != NULL) {
__skb_pull(tmp_skb, skb_network_header_len(skb));
*tail_skb = tmp_skb;
tail_skb = &(tmp_skb->next);
skb->len += tmp_skb->len;
skb->data_len += tmp_skb->len;
skb->truesize += tmp_skb->truesize;
tmp_skb->destructor = NULL;
tmp_skb->sk = NULL;
}
......
/*
* Steal rt from cork.dst to avoid a pair of atomic_inc/atomic_dec
* on dst refcount
*/
cork->dst = NULL;
skb_dst_set(skb, &rt->dst);
if (iph->protocol == IPPROTO_ICMP)
icmp_out_count(net, ((struct icmphdr *)
skb_transport_header(skb))->type);
ip_cork_release(cork);
out:
return skb;
}
不同的上层协议调用的IP层处理函数也不同,下面我们以UDP协议为例来分析一下UDP报文在IP层的处理逻辑。UDP协议报文的发送函数为udp_send_skb
,该函数会调用ip_send_skb
来将报文交给IP层进行处理,下面我们就从这个函数来展开讨论。
ip_send_skb
ip_send_skb
函数的定义如下:
int ip_send_skb(struct net *net, struct sk_buff *skb)
{
int err;
err = ip_local_out(net, skb->sk, skb);
if (err) {
if (err > 0)
err = net_xmit_errno(err);
if (err)
IP_INC_STATS(net, IPSTATS_MIB_OUTDISCARDS);
}
return err;
}
从上面我们可以看出该函数直接调用了ip_local_out
来进行报文的发送,并调用net_xmit_errno
对结果进行了处理,该函数用于将错误代码转换为上层协议可以理解的代码。下面我们来看看ip_local_out
函数的处理过程。
ip_local_out
int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int err;
err = __ip_local_out(net, sk, skb);
if (likely(err == 1))
err = dst_output(net, sk, skb);
return err;
}
从上面的代码中我们可以看出该函数直接调用了__ip_local_out
函数,并在返回值为1的时候将其交给路由层进行处理。至于为什么这样,我们先不管,我们先看看__ip_local_out
的处理逻辑。
__ip_local_out
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
ip_send_check(iph);
/* if egress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
/* 新加内容,还不知道干啥的 */
skb = l3mdev_ip_out(sk, skb);
if (unlikely(!skb))
return 0;
/* 将报文协议设置为IP协议 */
skb->protocol = htons(ETH_P_IP);
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)->dev,
dst_output);
}
这个函数主要做了三件事:
- 设置IP报头里的数据长度;
- 调用
ip_send_check
来计算报文的校验和,这个函数会调用ip_fast_csum
来对校验和进行计算; - 将报文交给防火墙钩子,并在通过后调用
dst_output
函数。
dst_output
dst_output
属于路由模块的功能,这个函数的定义很简单,它会直接调用skb
的dst
实体的output
方法,根据dst
实体的不同,不同的处理函数会被调用,从而使得报文的处理变得更加灵活。在UDP报文发送过程中,udp_sendmsg
函数调用了ip_route_output_flow
函数,该函数又调用了__ip_route_output_key
函数,最后调用了__mkroute_output
函数。这个函数会创建skb的路由,并根据路由的不同来创建不同的dst
实体。据说,在大多数情况下这个实体的函数都是ip_output
。
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
return skb_dst(skb)->output(net, sk, skb);
}
ip_output
进到这个函数里面,报文离发出去就不远啦!这个函数首先根据skb的路由将skb的网络设备设置为出口设备;然后它将报文协议设为IP协议,并将其交给防火墙的钩子,在通过防火墙后交给ip_finish_output
处理(这个流程怎么这么眼熟呢)。
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev;
IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);
skb->dev = dev;
skb->protocol = htons(ETH_P_IP);
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
net, sk, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
ip_finish_output
这个函数可谓是IP层的一个关键函数,它所做的工作主要有:
BPF_CGROUP_RUN_PROG_INET_EGRESS
:还不知道这个是干嘛的;- 检查skb的路由是否是xfrm,xfrm是IPSEC的一个实现,是一种在不打乱原有协议栈的基础上实现的一种基于策略的高扩展性网络安全架构,这里不再展开讨论;
- 检查skb是否是gso报文,由于gso报文不在IP层进行分片,因此它不能走常规路线,而是调用了
ip_finish_output_gso
函数; - skb长度大于mtu,则需要进行分片,从而交给
ip_fragment
,这个函数会对报文进行分片,并在完成后调用ip_finish_output2
函数; - 代用
ip_finish_output2
处理报文。
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
unsigned int mtu;
int ret;
ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb);
if (ret) {
kfree_skb(skb);
return ret;
}
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
/* Policy lookup after SNAT yielded a new policy */
if (skb_dst(skb)->xfrm) {
IPCB(skb)->flags |= IPSKB_REROUTED;
return dst_output(net, sk, skb);
}
#endif
mtu = ip_skb_dst_mtu(sk, skb);
if (skb_is_gso(skb))
return ip_finish_output_gso(net, sk, skb, mtu);
if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
return ip_finish_output2(net, sk, skb);
}
ip_finish_output2
下面我们来看看ip_finish_output2
这个函数,这个函数首先检查报文是否是多播或组播,是的话就把对应的计数器更新。
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
struct rtable *rt = (struct rtable *)dst;
struct net_device *dev = dst->dev;
unsigned int hh_len = LL_RESERVED_SPACE(dev);
struct neighbour *neigh;
u32 nexthop;
if (rt->rt_type == RTN_MULTICAST) {
IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTMCAST, skb->len);
} else if (rt->rt_type == RTN_BROADCAST)
IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTBCAST, skb->len);
然后,对skb的头部长度进行检查,看其是否有足够的空间来添加别的协议头部(报文的发送要一层层的添加报文头部),长度不够的话则进行空间的分配。
/* Be paranoid, rather than too clever. */
if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
struct sk_buff *skb2;
skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
if (!skb2) {
kfree_skb(skb);
return -ENOMEM;
}
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
consume_skb(skb);
skb = skb2;
}
这部分的代码好像是处理什么网络设备的,类似虚拟网卡啥的,还不太清楚,后面再研究。
if (lwtunnel_xmit_redirect(dst->lwtstate)) {
int res = lwtunnel_xmit(skb);
if (res < 0 || res == LWTUNNEL_XMIT_DONE)
return res;
}
最后,函数会根据报文的路由来查询skb的下一跳,并根据下一跳来获取报文的邻居子系统缓存。如果缓存不存在,则创建新的缓存。邻居子系统的作用是根据下一跳的IP地址来设置报文的MAC地址,一般查询到的结果会保存到缓存里,所以只有当第一次向主机发送报文时才会进行邻居子系统的创建。获取到邻居子系统后,通过调用neigh_output
函数来将报文交给邻居子系统进行发送。
rcu_read_lock_bh();
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res;
sock_confirm_neigh(skb, neigh);
res = neigh_output(neigh, skb);
rcu_read_unlock_bh();
return res;
}
rcu_read_unlock_bh();
net_dbg_ratelimited("%s: No header cache and no neighbour!\n",
__func__);
kfree_skb(skb);
return -EINVAL;
}
neigh_output
这个函数首先对邻居的状态进行了检查,如果状态为NUD_CONNECTED
的话则调用neigh_hh_output
函数。状态为NUD_CONNECTED
意味着这是以下情况之一:
- NUD_PERMANENT:这是一个静态路由;
- NUD_NOARP:不需要ARP(多播、组播等报文);
- NUD_REACHABLE:邻居可以到达,即通过IP查询到了MAC地址。
在邻居的状态为NUD_CONNECTED
,且hh
(还不知道是干啥的)已缓存的情况下,neigh_hh_output
函数会被调用,否则邻居的output
函数会被调用。
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
struct hh_cache *hh = &n->hh;
if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
return neigh_hh_output(hh, skb);
else
return n->output(n, skb);
}
neigh_hh_output
和n->output
函数最终都会调用dev_queue_xmit
函数来将报文交给网卡驱动处理。下面我们来看一下这两个函数都做了什么。
neigh_hh_output
static inline int neigh_hh_output(struct hh_cache *hh, struct sk_buff *skb)
{
unsigned int hh_alen = 0;
unsigned int seq;
unsigned int hh_len;
do {
seq = read_seqbegin(&hh->hh_lock);
hh_len = hh->hh_len;
if (likely(hh_len <= HH_DATA_MOD)) {
hh_alen = HH_DATA_MOD;
/* skb_push() would proceed silently if we have room for
* the unaligned size but not for the aligned size:
* check headroom explicitly.
*/
if (likely(skb_headroom(skb) >= HH_DATA_MOD)) {
/* this is inlined by gcc */
memcpy(skb->data - HH_DATA_MOD, hh->hh_data,
HH_DATA_MOD);
}
} else {
hh_alen = HH_DATA_ALIGN(hh_len);
if (likely(skb_headroom(skb) >= hh_alen)) {
memcpy(skb->data - hh_alen, hh->hh_data,
hh_alen);
}
}
} while (read_seqretry(&hh->hh_lock, seq));
if (WARN_ON_ONCE(skb_headroom(skb) < hh_alen)) {
kfree_skb(skb);
return NET_XMIT_DROP;
}
__skb_push(skb, hh_len);
return dev_queue_xmit(skb);
}
参考链接:
Linux IP Networking
Monitoring and Tuning the Linux Networking Stack: Sending Data