Linux IP in IP隧道簡述


前言:IPIP隧道是一種三層隧道,通過把原來的IP包封裝在新的IP包里面,來創建隧道傳輸。本篇簡單分析Linux(2.6.32版本)中的IPIP隧道的實現過程,期望有所借鑒,造出輪子:-)

一. IPIP的初始化

Linux中的IPIP隧道文件主要分布在tunnel4.cipip.c文件中。因為是三層隧道,在IP報文中填充的三層協議自然就不能是常見的TCP和UDP,所以,Linux抽象了一個隧道層,位置就相當於傳輸層,主要的實現就是在tunnel4.c中。來看看他們的初始化:

抽象的隧道層和IPIP模塊都是以注冊模塊的方式進行初始化

module_init(tunnel4_init);

module_init(ipip_init);

首先看隧道層的初始化,主要的工作就是注冊隧道協議和對應的處理函數:

static int __init tunnel4_init(void)
{
	if (inet_add_protocol(&tunnel4_protocol, IPPROTO_IPIP)) {
		printk(KERN_ERR "tunnel4 init: can't add protocol\n");
		return -EAGAIN;
	}
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
	if (inet_add_protocol(&tunnel64_protocol, IPPROTO_IPV6)) {
		printk(KERN_ERR "tunnel64 init: can't add protocol\n");
		inet_del_protocol(&tunnel4_protocol, IPPROTO_IPIP);
		return -EAGAIN;
	}
#endif
	return 0;
}

inet_add_protocol(&tunnel4_protocol, IPPROTO_IPIP)把IPIP隧道協議注冊進inet_protos全局數組中,而inet_protos中的其他協議注冊是在inet_init()中:

	if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
		printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");
	if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
		printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
	if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
		printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
#ifdef CONFIG_IP_MULTICAST
	if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
		printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n");
#endif

看一下隧道層的處理函數:

static const struct net_protocol tunnel4_protocol = {
	.handler	=	tunnel4_rcv,
	.err_handler	=	tunnel4_err,
	.no_policy	=	1,
	.netns_ok	=	1,
};

這樣注冊完后,當接收到三層類型是IPPROTO_IPIP時,就會調用tunnel4_rcv進行下一步的處理。可以說在隧道層對隧道協議進行的注冊,保證能夠識別接收到隧道包。而對隧道包的處理則是在IPIP中完成的。

for (handler = tunnel4_handlers; handler; handler = handler->next)
		if (!handler->handler(skb))
			return 0;

icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

在隧道層的處理函數中進一步調用注冊的不同隧道協議的處理函數,分別處理。

接下來進一步看IPIP的初始化部分:

static int __init ipip_init(void)
{
	int err;

	printk(banner);

	if (xfrm4_tunnel_register(&ipip_handler, AF_INET)) {
		printk(KERN_INFO "ipip init: can't register tunnel\n");
		return -EAGAIN;
	}

	err = register_pernet_gen_device(&ipip_net_id, &ipip_net_ops);
	if (err)
		xfrm4_tunnel_deregister(&ipip_handler, AF_INET);

	return err;
}

IPIP模塊初始化的部分也十分精簡,主要就是兩部分的工作,一個是注冊協議相關的處理函數等;另一個是創建對應的虛擬設備。

首先是注冊了IPIP對應的處理函數

static struct xfrm_tunnel ipip_handler = {
	.handler	=	ipip_rcv,
	.err_handler	=	ipip_err,
	.priority	=	1,
};

可以看到,從隧道層的處理函數進一步找到IPIP的處理函數后,IPIP報文就會最終進入ipip_rcv()處理,這部分在后面再詳細說明。

再來看創建設備部分:

register_pernet_gen_device()->register_pernet_operations(),在其中,最后調用了操作集中的初始化函數

if (ops->init == NULL)
		return 0;
return ops->init(&init_net);

對應的操作函數集如下:

static struct pernet_operations ipip_net_ops = {
	.init = ipip_init_net,
	.exit = ipip_exit_net,
};

這樣,就進入到ipip_init_net()中,終於看到創建設備咯

ipn->fb_tunnel_dev = alloc_netdev(sizeof(struct ip_tunnel),
					   "tunl0",
					   ipip_tunnel_setup);

if (!ipn->fb_tunnel_dev) {
  err = -ENOMEM;
  goto err_alloc_dev;
}

在創建設備時,對設備還進行了初始化配置ipip_tunnel_setup()

static void ipip_tunnel_setup(struct net_device *dev)
{
	dev->netdev_ops		= &ipip_netdev_ops;
	dev->destructor		= free_netdev;

	dev->type		= ARPHRD_TUNNEL;
	dev->hard_header_len 	= LL_MAX_HEADER + sizeof(struct iphdr);
	dev->mtu		= ETH_DATA_LEN - sizeof(struct iphdr);
	dev->flags		= IFF_NOARP;
	dev->iflink		= 0;
	dev->addr_len		= 4;
	dev->features		|= NETIF_F_NETNS_LOCAL;
	dev->priv_flags		&= ~IFF_XMIT_DST_RELEASE;
}

這里看到有設備的操作集dev->netdev_ops = &ipip_netdev_ops;,通過這個,我們能知道這個設備都能進行哪些操作:

static const struct net_device_ops ipip_netdev_ops = {
	.ndo_uninit	= ipip_tunnel_uninit,
	.ndo_start_xmit	= ipip_tunnel_xmit,
	.ndo_do_ioctl	= ipip_tunnel_ioctl,
	.ndo_change_mtu	= ipip_tunnel_change_mtu,

};

可以看出設備最后的發送函數就是ipip_tunnel_xmit()

之后在ipip_fb_tunnel_init()中對IPIP隧道進行了參數的設置,包括名字,協議號什么的。最后就注冊這個新創建的設備吧

if ((err = register_netdev(ipn->fb_tunnel_dev)))
		goto err_reg_dev;

這樣整個的初始化過程就做完了,下面簡單分析一下發送和接收的過程。

二. IPIP的接收

我們之前說到過,對應從網卡收上來的報文,過完鏈路層后就會到ip_rcv()中,大概是這樣的路線:

ip_rcv()->ip_rcv_finish()->ip_local_deliver()->ip_local_deliver_finish(),最終會在其中看到

ret = ipprot->handler(skb);
if (ret < 0) {
  protocol = -ret;
  goto resubmit;
}

調用注冊的協議的處理函數,也就是最終會調到tunnel4_rcv()->ipip_rcv()

if ((tunnel = ipip_tunnel_lookup(dev_net(skb->dev),
					iph->saddr, iph->daddr)) != NULL) { /* 查找對應的tunnel */
		if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
			read_unlock(&ipip_lock);
			kfree_skb(skb);
			return 0;
		}

		secpath_reset(skb);

		skb->mac_header = skb->network_header; /* 修改報文的mac頭指向網絡層開始,為了下面使用netif_rx		能傳給上層? */
		skb_reset_network_header(skb);
		skb->protocol = htons(ETH_P_IP);
		skb->pkt_type = PACKET_HOST;  /* 填充報文信息 */

		tunnel->dev->stats.rx_packets++;
		tunnel->dev->stats.rx_bytes += skb->len;
		skb->dev = tunnel->dev;
		skb_dst_drop(skb);
		nf_reset(skb);
		ipip_ecn_decapsulate(iph, skb);
		netif_rx(skb);  /* 傳遞給上層協議棧 */
		read_unlock(&ipip_lock);
		return 0;
	}

三. IPIP的發送

在初始化的時候,我們看到IPIP報文的發送時通過ipip_tunnel_xmit()函數進行的。在發送時,要給原有的IP報文頭前添加新的IP頭,我們略過這個函數的前面的路由處理的部分,直接看關鍵的添加報文頭的地方:

max_headroom = (LL_RESERVED_SPACE(tdev)+sizeof(struct iphdr));

	if (skb_headroom(skb) < max_headroom || skb_shared(skb) ||
	    (skb_cloned(skb) && !skb_clone_writable(skb, 0))) {
		struct sk_buff *new_skb = skb_realloc_headroom(skb, max_headroom);/* 為新的報文頭分配空間 */
		if (!new_skb) {  
			ip_rt_put(rt);
			stats->tx_dropped++;
			dev_kfree_skb(skb);
			return NETDEV_TX_OK;
		}
		if (skb->sk)
			skb_set_owner_w(new_skb, skb->sk);
		dev_kfree_skb(skb);
		skb = new_skb;
		old_iph = ip_hdr(skb);
	}

	skb->transport_header = skb->network_header; /* 重新設置傳輸層的頭位置 */
	skb_push(skb, sizeof(struct iphdr));
	skb_reset_network_header(skb);
	memset(&(IPCB(skb)->opt), 0, sizeof(IPCB(skb)->opt));
	IPCB(skb)->flags &= ~(IPSKB_XFRM_TUNNEL_SIZE | IPSKB_XFRM_TRANSFORMED |
			      IPSKB_REROUTED);
	skb_dst_drop(skb);
	skb_dst_set(skb, &rt->u.dst);

	/*
	 *	Push down and install the IPIP header.
	 */

	/* 設置新的IP頭字段 */
	iph 			=	ip_hdr(skb);
	iph->version		=	4;
	iph->ihl		=	sizeof(struct iphdr)>>2;
	iph->frag_off		=	df;
	iph->protocol		=	IPPROTO_IPIP;
	iph->tos		=	INET_ECN_encapsulate(tos, old_iph->tos);
	iph->daddr		=	rt->rt_dst;
	iph->saddr		=	rt->rt_src;

	if ((iph->ttl = tiph->ttl) == 0)
		iph->ttl	=	old_iph->ttl;

最后調用IPTUNNEL_XMIT()宏發送出去。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM