IP是TCP/IP协议族中最为核心的协议,所有的TCP,UDP,ICMP及IGMP数据都是以IP数据报的形式传输的
IP是一种不可靠的协议,也就是说,它并不能保证每个IP数据报都能成功地到达目的地,而只是提供最好的传输服务。如果发生某种错误(例如:某个路由器暂时用完了缓冲区),IP有一个简单的错误处理算法,即丢弃该数据报,然后发送ICMP消息报给发送方。每个数据报的处理是相互独立的,因此IP数据报可以不按发送顺序接收。任何可靠性都必须由上层协议来提供,如TCP
IP数据报的输入、输出和转发,数据报控制信息处理以及对端信息块的管理涉及以下文件
include/net/ip.h 定义IP层相关的结构、宏和函数原型
include/linux/inetdevice.h 定义IPv4专用的网络设备相关的结构、宏等
include/linux/errqueue.h 定义差错处理相关的结构
include/net/inetpeer.h 定义对端信息块的结构、宏和函数原型
include/net/dst.h 定义目的路由缓存相关结构、宏和函数原型
net/ipv4/ip_output.c IP数据报的输出
net/ipv4/ip_sockglue.c IP层套接口选项
net/ipv4/ip_input.c IP数据报的输入
net/ipv4/ip_forward.c IP数据报的转发
net/ipv4/inetpeer.c 对端信息块的管理
net/ipv4/af_inet.c 网络层和传输层接口
IP数据报的报文格式
参见tcp/ip协议学习笔记(3)Internet
Protocol(IP)
IP数据报的输入与输出
网络层处于传输层和链路层之间,同时还需要与路由表以及邻居子系统打交道。在输入数据时,提供输入接口给链路层调用,并调用传输层的输入接口将数据传递到传输层。在数据输出时,提供输出接口给传输层调用,并调用链路层的输出接口将数据输出到链路层。输入和输出过程中,都需要查找路由,通过netfilter处理等
IP的私有信息控制块
IP层在SKB中有个信息控制块struct inet_skb_parm结构,存储在skb_buff结构中的cb成员中。IP层用宏IPCB访问该结构以增强代码的可读性。这个私有的信息控制块主要存储IP选项以及在IP处理中需要设置的标志。
在IP层,无论是输入还是输出都需要处理IP选项。例如,输入时,ip_rcv_option()会解析IP首部中的选项保存到inet_skb_parm结构的opt;在输出时,ip_options_build()会根据inet_skb_parm结构的opt将其组织后在IP首部生成选项,而在转发时,ip_forward_options()会根据选项做适当处理。
struct inet_skb_parm
{
struct ip_options opt; /* Compiled IP options */
unsigned char flags;
#define IPSKB_FORWARDED 1
#define IPSKB_XFRM_TUNNEL_SIZE 2
#define IPSKB_XFRM_TRANSFORMED 4
#define IPSKB_FRAG_COMPLETE 8
#define IPSKB_REROUTED 16
};
#define IPCB(skb) ((struct inet_skb_parm*)((skb)->cb))
IP层套接口选项
IP层套接口选项入口为ip_setsockopt()。该函数首先判断选项的级别,如果不是SOL_IP级别,则返回无效的协议,否则调用do_ip_setsockopt()进行具体的选项处理。而在do_ip_setsockopt()中,组播路由相关的选项由ip_mroute_opt()来处理,其他选项则由该函数自己处理。
IP层套接口选项如下:
1)IP_OPTIONS
设置或获取将由套接口发送的每个数据报IP首部中的IP选项,最长可达40B。该参数是一个指向数据报含选项和选项长度的存储缓冲区的指针。
2)IP_PKTINFO
控制是否允许通过IP_PKTOPTIONS选项或recvmsg系统调用来获取与本地地址等信息相关的IP_PKTOPTIONS选项
3)IP_TTL
设置输出IP数据报的生存时间,有效值为1至255。IP数据报每经过一个路由器,路由器都会判断TTL的值,并将该值递减1,一旦发现其为0,即丢弃数据报,以免由于路由循环,造成数据报无休止地在网络中的”游荡“
具体选择如下,不一一列举了
include/linux/in.h
#define IP_TOS 1
#define IP_TTL 2
#define IP_HDRINCL 3
#define IP_OPTIONS 4
#define IP_ROUTER_ALERT 5
#define IP_RECVOPTS 6
#define IP_RETOPTS 7
#define IP_PKTINFO 8
#define IP_PKTOPTIONS 9
#define IP_MTU_DISCOVER 10
#define IP_RECVERR 11
#define IP_RECVTTL 12
#define IP_RECVTOS 13
#define IP_MTU 14
#define IP_FREEBIND 15
#define IP_IPSEC_POLICY 16
#define IP_XFRM_POLICY 17
#define IP_PASSSEC 18
#define IP_TRANSPARENT 19
/* BSD compatibility */
#define IP_RECVRETOPTS IP_RETOPTS
/* TProxy original addresses */
#define IP_ORIGDSTADDR 20
ipv4_devconf
ipv4_devconf结构是网络设备接口的IPv4系统配置。在内核中有一个名为ipv4_devconf的系统全局变量,该配置对所有接口有效。另外,每个网络设备的IP控制块中也都存在一份配置,但该配置只对所在网络设备有效。
enum
{
NET_IPV4_CONF_FORWARDING=1,
NET_IPV4_CONF_MC_FORWARDING=2,
NET_IPV4_CONF_PROXY_ARP=3,
NET_IPV4_CONF_ACCEPT_REDIRECTS=4,
NET_IPV4_CONF_SECURE_REDIRECTS=5,
NET_IPV4_CONF_SEND_REDIRECTS=6,
NET_IPV4_CONF_SHARED_MEDIA=7,
NET_IPV4_CONF_RP_FILTER=8,
NET_IPV4_CONF_ACCEPT_SOURCE_ROUTE=9,
NET_IPV4_CONF_BOOTP_RELAY=10,
NET_IPV4_CONF_LOG_MARTIANS=11,
NET_IPV4_CONF_TAG=12,
NET_IPV4_CONF_ARPFILTER=13,
NET_IPV4_CONF_MEDIUM_ID=14,
NET_IPV4_CONF_NOXFRM=15,
NET_IPV4_CONF_NOPOLICY=16,
NET_IPV4_CONF_FORCE_IGMP_VERSION=17,
NET_IPV4_CONF_ARP_ANNOUNCE=18,
NET_IPV4_CONF_ARP_IGNORE=19,
NET_IPV4_CONF_PROMOTE_SECONDARIES=20,
NET_IPV4_CONF_ARP_ACCEPT=21,
NET_IPV4_CONF_ARP_NOTIFY=22,
NET_IPV4_CONF_SRC_VMARK=24,
__NET_IPV4_CONF_MAX
};
struct ipv4_devconf
{
void *sysctl;
int data[__NET_IPV4_CONF_MAX - 1];
DECLARE_BITMAP(state, __NET_IPV4_CONF_MAX - 1);
};
NET_IPV4_CONF_FORWARDING
标识是否启用IP数据报转发功能
NET_IPV4_CONF_PROXY_ARP
标识是否启用ARP代理功能
套接口错误队列
在传输控制块中有一个用于保存错误信息的队列sk_error_queue,当ICMP接收到差错信息或者UDP套接口和RAW套接口输出报文出错时,会产生描述错误信息的SKB添加到该队列上。应用程序为能通过系统调用获取详细的错误信息,需要设置IP_RECVERR套接口选项,之后可通过参数flags为MSG_ERRQUEUE的recvmsg系统调用来获取详细的出错信息。
错误信息的数据流及函数调用关系
在基于连接的套接口上,IP_RECVERR意义则会有所不同。并不保存错误信息到错误队列中,而是立即传递所有接收到的错误信息给用户进程。这对于基于短连接的TCP应用是很有用的,因为TCP要求快速的错误处理。需要注意的是,TCP没有错误队列,MSG_ERRQUEUE对于基于连接的套接口时无效的。
错误信息传递给用户时,并不将错误信息作为报文的内容传递给用户进程,而是以错误信息块的形式保存在SKB控制块中,通过SKB_EXT_ERR来访问SKB控制块中的错误信息块
#define SKB_EXT_ERR(skb) ((struct sock_exterr_skb *) ((skb)->cb))
struct sock_exterr_skb
{
union {
struct inet_skb_parm h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm h6;
#endif
} header;
struct sock_extended_err ee;
u16 addr_offset;
__be16 port;
};
由于该错误也在IP层中处理,为了与IP控制块兼用,错误信息块的前部由IP控制块组成,之后才是出错信息
添加ICMP差错信息
当接收到ICMP差错信息时,ICMP模块会根据出错的原始数据报的传输层协议,调用传输层的差错处理例程,而传输层的差错处理例程会进而调用ip_icmp_error()将出错信息添加到输出该错误数据报的传输控制块错误队列上。
void ip_icmp_error(struct sock *sk, struct sk_buff *skb, int err,
__be16 port, u32 info, u8 *payload)
{
struct inet_sock *inet = inet_sk(sk);
struct sock_exterr_skb *serr;
if (!inet->recverr)
return;
skb = skb_clone(skb, GFP_ATOMIC);
if (!skb)
return;
serr = SKB_EXT_ERR(skb);
serr->ee.ee_errno = err;
serr->ee.ee_origin = SO_EE_ORIGIN_ICMP;
serr->ee.ee_type = icmp_hdr(skb)->type;
serr->ee.ee_code = icmp_hdr(skb)->code;
serr->ee.ee_pad = 0;
serr->ee.ee_info = info;
serr->ee.ee_data = 0;
serr->addr_offset = (u8 *)&(((struct iphdr *)(icmp_hdr(skb) + 1))->daddr) -
skb_network_header(skb);
serr->port = port;
if (skb_pull(skb, payload - skb->data) != NULL) {
skb_reset_transport_header(skb);
if (sock_queue_err_skb(sk, skb) == 0)
return;
}
kfree_skb(skb);
}
sk,输出错误数据报的传输控制块
skb,从ICMP模块传递到传输层的ICMP差错报文
err,发生错误的错误码
port,对于UDP是出错报文的目的端口,其他情况下都为0
info,错误信息的扩展信息
payload,指向产生ICMP差错的原始数据报中应用层的内容
添加由本地产生的差错信息
当UDP套接口或RAW套接口发送数据时,如果待发送数据的长度超过IP数据报能负载的长度,会调用ip_local_error()将数据报数据超长的出错信息添加到输出该出错报文的传输控制块错误队列上。ip_local_error()的实现和ip_icmp_error()的实现类似,区别在于出错信息来源为本地
void ip_local_error(struct sock *sk, int err, __be32 daddr, __be16 port, u32 info)
{
struct inet_sock *inet = inet_sk(sk);
struct sock_exterr_skb *serr;
struct iphdr *iph;
struct sk_buff *skb;
if (!inet->recverr)
return;
skb = alloc_skb(sizeof(struct iphdr), GFP_ATOMIC);
if (!skb)
return;
skb_put(skb, sizeof(struct iphdr));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
iph->daddr = daddr;
serr = SKB_EXT_ERR(skb);
serr->ee.ee_errno = err;
serr->ee.ee_origin = SO_EE_ORIGIN_LOCAL;
serr->ee.ee_type = 0;
serr->ee.ee_code = 0;
serr->ee.ee_pad = 0;
serr->ee.ee_info = info;
serr->ee.ee_data = 0;
serr->addr_offset = (u8 *)&iph->daddr - skb_network_header(skb);
serr->port = port;
__skb_pull(skb, skb_tail_pointer(skb) - skb->data);
skb_reset_transport_header(skb);
if (sock_queue_err_skb(sk, skb))
kfree_skb(skb);
}
读取错误信息
通常,recvmsg()是用来接收远端发送到到所在套接口的数据的,但也可通过设置flags为MSG_ERRQUEUE来读取传输控制块错误队列上的错误信息。在UDP套接口和RAW套接口的recvmsg()的实现中,先检测是否存在MSG_ERRQUEUE标志,如果有,则直接调用ip_recv_error()从传输控制块的错误队列中读取错误信息后返回。
/*
* Handle MSG_ERRQUEUE
*/
int ip_recv_error(struct sock *sk, struct msghdr *msg, int len)
{
struct sock_exterr_skb *serr;
struct sk_buff *skb, *skb2;
struct sockaddr_in *sin;
struct {
struct sock_extended_err ee;
struct sockaddr_in offender;
} errhdr;
int err;
int copied;
err = -EAGAIN;
skb = skb_dequeue(&sk->sk_error_queue);
if (skb == NULL)
goto out;
copied = skb->len;
if (copied > len) {
msg->msg_flags |= MSG_TRUNC;
copied = len;
}
err = skb_copy_datagram_iovec(skb, 0, msg->msg_iov, copied);
if (err)
goto out_free_skb;
sock_recv_timestamp(msg, sk, skb);
serr = SKB_EXT_ERR(skb);
sin = (struct sockaddr_in *)msg->msg_name;
if (sin) {
sin->sin_family = AF_INET;
sin->sin_addr.s_addr = *(__be32 *)(skb_network_header(skb) +
serr->addr_offset);
sin->sin_port = serr->port;
memset(&sin->sin_zero, 0, sizeof(sin->sin_zero));
}
memcpy(&errhdr.ee, &serr->ee, sizeof(struct sock_extended_err));
sin = &errhdr.offender;
sin->sin_family = AF_UNSPEC;
if (serr->ee.ee_origin == SO_EE_ORIGIN_ICMP) {
struct inet_sock *inet = inet_sk(sk);
sin->sin_family = AF_INET;
sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
sin->sin_port = 0;
memset(&sin->sin_zero, 0, sizeof(sin->sin_zero));
if (inet->cmsg_flags)
ip_cmsg_recv(msg, skb);
}
put_cmsg(msg, SOL_IP, IP_RECVERR, sizeof(errhdr), &errhdr);
/* Now we could try to dump offended packet options */
msg->msg_flags |= MSG_ERRQUEUE;
err = copied;
/* Reset and regenerate socket error */
spin_lock_bh(&sk->sk_error_queue.lock);
sk->sk_err = 0;
skb2 = skb_peek(&sk->sk_error_queue);
if (skb2 != NULL) {
sk->sk_err = SKB_EXT_ERR(skb2)->ee.ee_errno;
spin_unlock_bh(&sk->sk_error_queue.lock);
sk->sk_error_report(sk);
} else
spin_unlock_bh(&sk->sk_error_queue.lock);
out_free_skb:
kfree_skb(skb);
out:
return err;
}
对端信息块
对端信息块由inet_peer结构描述,用来保存对端的一些信息,包括对端的地址以及传输层使用的时间戳等。主要用于在组装IP数据报时防止分片攻击,在建立TCP连接时检测连接请求段是否有效以及其序号是否回绕。
struct inet_peer
{
/* group together avl_left,avl_right,v4daddr to speedup lookups */
struct inet_peer *avl_left, *avl_right;
__be32 v4daddr; /* peer's address */
__u16 avl_height;
__u16 ip_id_count; /* IP ID for the next packet */
struct list_head unused;
__u32 dtime; /* the time of last use of not
* referenced entries */
atomic_t refcnt;
atomic_t rid; /* Frag reception counter */
__u32 tcp_ts;
unsigned long tcp_ts_stamp;
};
对端信息块的创建和查找
对端信息块的创建和查找都是通过inet_getpeer()来实现的,由参数create来区分是创建还是查找
/* Called with or without local BH being disabled. */
struct inet_peer *inet_getpeer(__be32 daddr, int create)
{
struct inet_peer *p, *n;
struct inet_peer **stack[PEER_MAXDEPTH], ***stackptr;
/* Look up for the address quickly. */
read_lock_bh(&peer_pool_lock);
p = lookup(daddr, NULL);
if (p != peer_avl_empty)
atomic_inc(&p->refcnt);
read_unlock_bh(&peer_pool_lock);
if (p != peer_avl_empty) {
/* The existing node has been found. */
/* Remove the entry from unused list if it was there. */
unlink_from_unused(p);
return p;
}
if (!create)
return NULL;
/* Allocate the space outside the locked region. */
n = kmem_cache_alloc(peer_cachep, GFP_ATOMIC);
if (n == NULL)
return NULL;
n->v4daddr = daddr;
atomic_set(&n->refcnt, 1);
atomic_set(&n->rid, 0);
n->ip_id_count = secure_ip_id(daddr);
n->tcp_ts_stamp = 0;
write_lock_bh(&peer_pool_lock);
/* Check if an entry has suddenly appeared. */
p = lookup(daddr, stack);
if (p != peer_avl_empty)
goto out_free;
/* Link the node. */
link_to_pool(n);
INIT_LIST_HEAD(&n->unused);
peer_total++;
write_unlock_bh(&peer_pool_lock);
if (peer_total >= inet_peer_threshold)
/* Remove one less-recently-used entry. */
cleanup_once(0);
return n;
out_free:
/* The appropriate node is already in the pool. */
atomic_inc(&p->refcnt);
write_unlock_bh(&peer_pool_lock);
/* Remove the entry from unused list if it was there. */
unlink_from_unused(p);
/* Free preallocated the preallocated node. */
kmem_cache_free(peer_cachep, n);
return p;
}
对端信息块的删除
当使用完对端信息块之后,需要将其删除并释放。实际上,inet_putpeer()只是将该对端信息块添加到unused_peers链表中,表示该对端信息块当前没有被使用。而真正的删除和释放,由垃圾回收机制来处理
void inet_putpeer(struct inet_peer *p)
{
spin_lock_bh(&inet_peer_unused_lock);
if (atomic_dec_and_test(&p->refcnt)) {
list_add_tail(&p->unused, &unused_peers);
p->dtime = (__u32)jiffies;
}
spin_unlock_bh(&inet_peer_unused_lock);
}
垃圾回收
处理垃圾回收的方式有两种---同步和异步。同步方式通常是在创建对端信息块时发现当前对端信息块数达到inet_peer_threshold时触发,而异步方式则是在定时器超时触发。
正在使用的对端信息块是不会过期的,只有闲置的对端信息块才有可能过期,因为对端信息块一旦闲置,就会被添加到unused_peers链表中,并记录闲置的时间。在同步或异步清理是,一旦发现闲置时间达到阈值,对端信息块就会过期。对端信息块的过期与inet_peer_minttl、inet_peer_maxttl和inet_peer_threshold有关
同步清理
struct inet_peer *inet_getpeer(__be32 daddr, int create)
{
...
if (peer_total >= inet_peer_threshold)
/* Remove one less-recently-used entry. */
cleanup_once(0);
...
}
异步清理
同步回收来及用于处理内存压力较大的特殊情况,事实上这种情况比较少见,同时也是非常影响性能的。因此为了避免这种特殊情况的出现或降低其出现的概率,使用peer_periodic_timer定时器来进行周期性回收,该定时器处理例程为peer_check_expire()。其时间间隔在inet_initpeers()中设置
/* Called with local BH disabled. */
static void peer_check_expire(unsigned long dummy)
{
unsigned long now = jiffies;
int ttl;
if (peer_total >= inet_peer_threshold)
ttl = inet_peer_minttl;
else
ttl = inet_peer_maxttl
- (inet_peer_maxttl - inet_peer_minttl) / HZ *
peer_total / inet_peer_threshold * HZ;
while (!cleanup_once(ttl)) {
if (jiffies != now)
break;
}
/* Trigger the timer after inet_peer_gc_mintime .. inet_peer_gc_maxtime
* interval depending on the total number of entries (more entries,
* less interval). */
if (peer_total >= inet_peer_threshold)
peer_periodic_timer.expires = jiffies + inet_peer_gc_mintime;
else
peer_periodic_timer.expires = jiffies
+ inet_peer_gc_maxtime
- (inet_peer_gc_maxtime - inet_peer_gc_mintime) / HZ *
peer_total / inet_peer_threshold * HZ;
add_timer(&peer_periodic_timer);
}