2021-05-12 14:32:11
Linux IP in IP隧道簡述
前言:IPIP隧道是一種三層隧道,通過把原來的IP包封裝在新的IP包裡面,來建立隧道傳輸。本篇簡單分析Linux(2.6.32版本)中的IPIP隧道的實現過程,期望有所借鑑,造出輪子:-)
一. IPIP的初始化
Linux中的IPIP隧道檔案主要分布在tunnel4.c
和ipip.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 protocoln");
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 protocoln");
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 protocoln");
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
printk(KERN_CRIT "inet_init: Cannot add UDP protocoln");
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
printk(KERN_CRIT "inet_init: Cannot add TCP protocoln");
#ifdef CONFIG_IP_MULTICAST
if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
printk(KERN_CRIT "inet_init: Cannot add IGMP protocoln");
#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 tunneln");
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()
宏傳送出去。
相關文章