本地发送IP数据包是指数据包包括:传输层产生的数据包、裸IP、SCTP、IGMP,TCP和网络层的接口函数是ip_queue_xmit,UDP和网络层接口函数是ip_push_pending_frames,数据包对外发送在内核要做以下几件事情。
a、查找下一个站点
IP层需要知道完成数据包输出功能的是那个网络设备,以及下一个站点的路由信息,寻找路由的任务由ip_route_output_flow函数完成。
b、初始化IP头
初始化ip协议头,处理IP选项、数据包分片、处理校验和。
c、调用网络过滤子系统做安全检查,不合法就丢掉
d、更新统计信息
linux在传输层实现了多种不同传输层协议,不同的协议不同组织数据包不同,向网络层发送数据包的方式也不同,如图是传输层与网络层接口函数关系。
从上图看从传输层到网络层,发送数据包的方式主要分为两种
1)、数据包传给网络层时已经对数据包的分片做了大量的预处理,留给IP层的工作很多少了,比如(TCP、SCTP)。
2)、UDP和裸IP将数据包分片的工作都留给了IP层。
函数说明:
ip_queue_xmit:传输层TCP协议调用,将数据包从传输层发送到网络层,创建协议头和IP选项,然后调用dst_output发送。
ip_append_data:传输层中UDP协议调用,缓存传输层到网络层的请求发送的数据包缓冲区。
ip_append_page:传输层UDP协议调用,缓存传输层到网络层的请求发送数据面。
ip_push_pending_frames:将ip_append_data和ip_append_page创建的数据包发送队列发送出去。
dst_output:数据包发送函数,当数据包的目标地址是其他主机初始化为ip_output。
ip_build_and_send_pkt:用于TCP发送同步回答消息时创建IP协议头和选项并发送。
ip_send_reply:用户TCP发送回答消息和复位是创建IP协议头和选项并发送。
对于裸IP和IGMP他们自己构造IP协议头,所以直接调用dst_output,TCP协议头管理连接并向对端发送回答消息和复位消息时调用ip_send_reply,ip_send_reply又调用ip_append_data和ip_push_pending_frames来回答消息数据包。
一、关键数据结构体
1.1.struct sock
linux内核实现支持各种协议栈,struct sock接头体是通用套接字数据结构,这个结构体数据很庞大,定义在include/net/sock.h中
struct sock {
/*
* Now struct inet_timewait_sock also uses sock_common, so please just
* don't add nothing before this first member (__sk_common) --acme
*/
//套接字在网络中最小描述,
//内核管理套接最重要的信息结构体
struct sock_common __sk_common;
//定义sock_common元素的别名方便访问
#define sk_node __sk_common.skc_node
#define sk_nulls_node __sk_common.skc_nulls_node
#define sk_refcnt __sk_common.skc_refcnt
#define sk_tx_queue_mapping __sk_common.skc_tx_queue_mapping
#define sk_copy_start __sk_common.skc_hash
#define sk_hash __sk_common.skc_hash
#define sk_family __sk_common.skc_family
#define sk_state __sk_common.skc_state
#define sk_reuse __sk_common.skc_reuse
#define sk_bound_dev_if __sk_common.skc_bound_dev_if
#define sk_bind_node __sk_common.skc_bind_node
#define sk_prot __sk_common.skc_prot
#define sk_net __sk_common.skc_net
kmemcheck_bitfield_begin(flags);
//tcp关闭套接字会发送RST数据包
...
}
1.2.struct inet_sock结构体
struct inet_sock继承了通用套接字属性(struct sock),是linux内核实现TCP/IP协议栈PF_INET协议族的套接字数据结构,struct inet_sock包含了发送数据包大部分控制信息:数据包目的ip、发送网络设备、ip选项。struct sock是struct inet_sock的第一个元素,所以他们的基础地址是一样的。
struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
//通用套接字结构
struct sock sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct ipv6_pinfo *pinet6;
#endif
/* Socket demultiplex comparisons on incoming packets. */
//目标地址
__be32 inet_daddr;
//本机绑定的ip地址
__be32 inet_rcv_saddr;
//目的端口
__be16 inet_dport;
//本地应用程序创建套接字的端口号
__u16 inet_num;
//发送数据包源地址
__be32 inet_saddr;
//数据包存活时间ttl
__s16 uc_ttl;
__u16 cmsg_flags;
//发送数据包源端口
__be16 inet_sport;
//为分片数据包标识符
__u16 inet_id;
//ip选项
struct ip_options *opt;
//数据包服务类型tos
__u8 tos;
__u8 min_ttl;
//组发送存活时间ttl
__u8 mc_ttl;
//发送路由最大mtu
__u8 pmtudisc;
__u8 recverr:1,
...
mc_all:1;
//组发送网络设备的索引号
int mc_index;
//组发送地址
__be32 mc_addr;
struct ip_mc_socklist *mc_list;
struct {
//标志位
unsigned int flags;
//产生的分片数据段大小
unsigned int fragsize;
//ip选项
struct ip_options *opt;
//路由表缓冲去入口
struct dst_entry *dst;
//ip数据包大小
int length; /* Total length of all frames */
//发送数据包目标地址
__be32 addr;
//存放tcp两端连接信息
struct flowi fl;
} cork;
};
1.3、struct inet_sock struct sock关键函数
a、sk_dst_set 和__sk_dst_set函数
支持TCP协议的套机字,需要管理TCP连接,保存目标地址使用的路由,
b、sk_dst_check和__sk_dst_check
检查达到目标地址路由是否有效。
c、skb_set_owner_w
指定一个数据包所属的套接字,也就是设定skb->sk数据域。
d、sock_alloc_send_skb和sock_wmalloc函数
分片sk_buff所需要的内存空间,sock_alloc_send_skb分配单个缓冲区或一系列分片数据包的第一个缓冲区,sock_wmalloc管理其余的子分片。
二、ip_queue_ximit函数
ip_queue_xmit是传输层TCP协议和网络层的接口,ip_queue_xmit对数据包做一系列的IP层的初始化工作,本机产生的每个数据包都属于某个套接字,包含套接字数据结构的指针skb->sk。ip_queue_ximit主要做以下工作
1、设置路由
先检查skb->rtable是否为空,如果为空就要为数据包分配路由,如果没有设置路由就调用ip_route_ouput_flow设置skb的路由,然后调用__sk_dst_check检查目标路由是否可达。
...
rcu_read_lock();
//获取路由
rt = skb_rtable(skb);
if (rt != NULL)
goto packet_routed;
/* Make sure we can route this packet. */
//检查路由是否可达
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (rt == NULL) {
__be32 daddr;
/* Use correct destination address if we have options. */
//设置了源路由选项,重新查询新路由和daddr取自源路由IP地址列表
daddr = inet->inet_daddr;
if(opt && opt->srr)
daddr = opt->faddr;
{
...
//路由不可达,重新选路由
if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0))
goto no_route;
}
//发送数据包的输出网络设备信息
sk_setup_caps(sk, &rt->u.dst);
}
...
2、构建IP协议头
到此时skb只包含了IP数据包的常规负载数据、协议头、从传输层传来的负载数据。TCP为Socket Buffer分配内存是按找最大可能需要的来向系统申请内存,主要是考虑了下层协议头需要空间,这样做的可以避免IP层或更低协议在空间不够的情况下做缓冲区复制或重分配提高效率。
...
/* OK, we know where to send it, allocate and build IP header. */
//查看是否有足够空间存放IP协议头
skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->u.dst) && !skb->local_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
//初始化IP协议头的iph结构体各数据域
iph->ttl = ip_select_ttl(inet, &rt->u.dst);
//协议
iph->protocol = sk->sk_protocol;
//原地址
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
/* Transport layer set skb->h.foo itself. */
//构建IP选项
if (opt && opt->optlen) {
iph->ihl += opt->optlen >> 2;
ip_options_build(skb, opt, inet->inet_daddr, rt, 0);
}
//设置分片数据包的表示符ID
ip_select_ident_more(iph, &rt->u.dst, sk,
(skb_shinfo(skb)->gso_segs ?: 1) - 1);
//优先级设置
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
...
3、函数结束处理
最后调用ip_local_out,再调用__ip_local_out,然后调用ip_send_check计算校验和,最后调用网络过滤子系统OUT链上的钩子处理函数,钩子处理函数结束调用dst_output接续。
int __ip_local_out(struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
//ip校验和
ip_send_check(iph);
//调用OUT链上的钩子处理函数
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
skb_dst(skb)->dev, dst_output);
}
以下是ip_queue_xmit完整代码:
int ip_queue_xmit(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
struct inet_sock *inet = inet_sk(sk);
struct ip_options *opt = inet->opt;
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();
//获取路由
rt = skb_rtable(skb);
if (rt != NULL)
goto packet_routed;
/* Make sure we can route this packet. */
//检查路由是否可达
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (rt == NULL) {
__be32 daddr;
/* Use correct destination address if we have options. */
//设置了源路由选项,重新查询新路由和daddr取自源路由IP地址列表
daddr = inet->inet_daddr;
if(opt && opt->srr)
daddr = opt->faddr;
{
struct flowi fl = { .oif = sk->sk_bound_dev_if,
.mark = sk->sk_mark,
.nl_u = { .ip4_u =
{ .daddr = daddr,
.saddr = inet->inet_saddr,
.tos = RT_CONN_FLAGS(sk) } },
.proto = sk->sk_protocol,
.flags = inet_sk_flowi_flags(sk),
.uli_u = { .ports =
{ .sport = inet->inet_sport,
.dport = inet->inet_dport } } };
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
security_sk_classify_flow(sk, &fl);
//路由不可达,重新选路由
if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0))
goto no_route;
}
//发送数据包的输出网络设备信息
sk_setup_caps(sk, &rt->u.dst);
}
skb_dst_set_noref(skb, &rt->u.dst);
packet_routed:
if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
//查看是否有足够空间存放IP协议头
skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->u.dst) && !skb->local_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
//初始化IP协议头的iph结构体各数据域
iph->ttl = ip_select_ttl(inet, &rt->u.dst);
//协议
iph->protocol = sk->sk_protocol;
//原地址
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
/* Transport layer set skb->h.foo itself. */
//构建IP选项
if (opt && opt->optlen) {
iph->ihl += opt->optlen >> 2;
ip_options_build(skb, opt, inet->inet_daddr, rt, 0);
}
//设置分片数据包的表示符ID
ip_select_ident_more(iph, &rt->u.dst, sk,
(skb_shinfo(skb)->gso_segs ?: 1) - 1);
//优先级设置
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(skb);
rcu_read_unlock();
return res;
no_route:
rcu_read_unlock();
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}