Linux内核中的IPSEC实现(7)
本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性,严禁用于任何商业用途。
9. IPSEC封装流程
IPSEC
数据包的封装过程是在数据包发出前完成的, 是和路由选择密切相关的, 根据前面的发出分析可知封装是通过对数据设置安全路由链表来实现的, 因此对数据包的IPSEC封装流程可以简单描述如下:
1) 对于进入的数据包, 进行路由选择, 如果是转发的, 进入路由输入, 然后查找安全策略检查是否需要IPSEC封装, 如果需要封装, 就查找和创建相关的安全路由, 进入路由输出处理, 在路由输出时即按照安全路由一层层地封装数据包最后得到IPSEC包发出;
2) 对于自身发出的数据包, 需要进行路由选择, 选定路由后进入路由输入, 查找安全策略进行处理, 以后和转发的数据包IPSEC封装就是完全相同了。
9.1 转发包的封装
数据的转发入口点函数是ip_forward, 进入该函数的数据包还是普通数据包,数据包的路由也是普通路由:
/* net/ipv4/ip_forward.c */
int ip_forward(struct sk_buff *skb)
{
struct iphdr *iph; /* Our header */
struct rtable *rt; /* Route we use */
struct ip_options * opt = &(IPCB(skb)-<opt);
// 对转发的数据包进行安全策略检查, 检查失败的话丢包
if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb))
goto drop;
if (IPCB(skb)-<opt.router_alert && ip_call_ra_chain(skb))
return NET_RX_SUCCESS;
// 转发包也是到自身的包, 不是的话丢包
if (skb-<pkt_type != PACKET_HOST)
goto drop;
skb-<ip_summed = CHECKSUM_NONE;
/*
* According to the RFC, we must first decrease the TTL field. If
* that reaches zero, we must reply an ICMP control message telling
* that the packet's lifetime expired.
*/
// TTL到头了, 丢包
if (skb-<nh.iph-<ttl < br style='font-size:12px;font-style:normal;font-weight:400;color:#666;' /> goto too_many_hops;
// 进入安全路由选路和转发处理, 在此函数中构造数据包的安全路由
if (!xfrm4_route_forward(skb))
goto drop;
// 以下是一些常规的路由和TTL处理
rt = (struct rtable*)skb-<dst;
if (opt-<is_strictroute && rt-<rt_dst != rt-<rt_gateway)
goto sr_failed;
/* We are about to mangle packet. Copy it! */
if (skb_cow(skb, LL_RESERVED_SPACE(rt-<u.dst.dev)+rt-<u.dst.header_len))
goto drop;
iph = skb-<nh.iph;
/* Decrease ttl after skb cow done */
ip_decrease_ttl(iph);
/*
* We now generate an ICMP HOST REDIRECT giving the route
* we calculated.
*/
if (rt-<rt_flags&RTCF_DOREDIRECT && !opt-<srr)
ip_rt_send_redirect(skb);
skb-<priority = rt_tos2priority(iph-<tos);
// 进行FORWARD点过滤, 过滤后进入ip_forward_finish函数
return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb-<dev, rt-<u.dst.dev,
ip_forward_finish);
sr_failed:
/*
* Strict routing permits no gatewaying
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
goto drop;
too_many_hops:
/* Tell the sender its packet died... */
IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
// ip_forward_finish函数主要就是调用dst_output函数
static inline int ip_forward_finish(struct sk_buff *skb)
{
struct ip_options * opt = &(IPCB(skb)-<opt);
IP_INC_STATS_BH(IPSTATS_MIB_OUTFORWDATAGRAMS);
if (unlikely(opt-<optlen))
ip_forward_options(skb);
return dst_output(skb);
}
核心函数是xfrm4_route_forward函数
/* include/net/xfrm.h */
static inline int xfrm4_route_forward(struct sk_buff *skb)
{
return xfrm_route_forward(skb, AF_INET);
}
static inline int xfrm_route_forward(struct sk_buff *skb, unsigned short family)
{
// 如果没有发出方向的安全策略的话返回
return !xfrm_policy_count[XFRM_POLICY_OUT] ||
// 如果路由标志专门设置不进行IPSEC封装的话也返回
(skb-<dst-<flags & DST_NOXFRM) ||
__xfrm_route_forward(skb, family);
}
/* net/xfrm/xfrm_policy.c */
int __xfrm_route_forward(struct sk_buff *skb, unsigned short family)
{
struct flowi fl;
// 路由解码, 填充流结构参数,
// 对IPV4实际调用的是_decode_session4(net/ipv4/xfrm4_policy.c)函数
if (xfrm_decode_session(skb, &fl, family) < 0 br style='font-size:12px;font-style:normal;font-weight:400;color:#666;' /> return 0;
// 根据流结构查找安全路由, 没找到的话创建新的安全路由, 最后形成安全路由链表
// 见前几节中的分析
return xfrm_lookup(&skb-<dst, &fl, NULL, 0) == 0;
}
因此数据进行转发处理后, 最终进入dst_output函数处理
转发函数流程小结:
ip_forward
-< xfrm4_route_forward (net/xfrm.h, get xfrm_dst)
-< xfrm_route_forward
-< __xfrm_route_forward
-< xfrm_lookup
-< xfrm_find_bundle
-< afinfo-<find_bundle == __xfrm4_find_bundle
-< xfrm_bundle_create
-< afinfo-<bundle_create == __xfrm4_bundle_create
tunnel mode
-< xfrm_dst_lookup
-< afinfo-<dst_lookup == xfrm4_dst_lookup
-< __ip_route_output_key
-< dst_list: dst-<list=policy_bundles, policy-<bundles = dst
-< NF_HOOK(NF_FORWARD)
-< ip_forward_finish
-< dst_output
9.2 自身数据发出
对于IPv4包的发出, 通常出口函数是ip_queue_xmit或ip_push_pending_frames, 如果是后者, 数据包是已经经过了路由选择的, 而前者还没有进行路由选择, 两者最后都会调用dst_output()函数进行数据的发出.
/* net/ipv4/ip_output.c */
int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
{
struct sock *sk = skb-<sk;
struct inet_sock *inet = inet_sk(sk);
struct ip_options *opt = inet-<opt;
struct rtable *rt;
struct iphdr *iph;
/* Skip all of this if the packet is already routed,
* f.e. by something like SCTP.
*/
// 已经路由过的数据跳过路由查找过程
rt = (struct rtable *) skb-<dst;
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 = inet-<daddr;
if(opt && opt-<srr)
daddr = opt-<faddr;
{
struct flowi fl = { .oif = sk-<sk_bound_dev_if,
.nl_u = { .ip4_u =
{ .daddr = daddr,
.saddr = inet-<saddr,
.tos = RT_CONN_FLAGS(sk) } },
.proto = sk-<sk_protocol,
.uli_u = { .ports =
{ .sport = inet-<sport,
.dport = 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(&rt, &fl, sk, 0))
goto no_route;
}
sk_setup_caps(sk, &rt-<u.dst);
}
skb-<dst = dst_clone(&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. */
iph = (struct iphdr *) skb_push(skb, sizeof(struct iphdr) + (opt ? opt-<optlen : 0));
*((__u16 *)iph) = htons((4 < inet->tos & 0xff));
iph-<tot_len = htons(skb-<len);
if (ip_dont_fragment(sk, &rt-<u.dst) && !ipfragok)
iph-<frag_off = htons(IP_DF);
else
iph-<frag_off = 0;
iph-<ttl = ip_select_ttl(inet, &rt-<u.dst);
iph-<protocol = sk-<sk_protocol;
iph-<saddr = rt-<rt_src;
iph-<daddr = rt-<rt_dst;
skb-<nh.iph = iph;
/* Transport layer set skb-<h.foo itself. */
if (opt && opt-<optlen) {
iph-<ihl += opt-<optlen << 2;
ip_options_build(skb, opt, inet-<daddr, rt, 0);
}
ip_select_ident_more(iph, &rt-<u.dst, sk,
(skb_shinfo(skb)-<gso_segs ?: 1) - 1);
/* Add an IP checksum. */
ip_send_check(iph);
skb-<priority = sk-<sk_priority;
// 进入OUTPUT点进行过滤, 过滤完成后进入dst_output()函数
return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt-<u.dst.dev,
dst_output);
no_route:
IP_INC_STATS(IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}
// 路由查找函数
int ip_route_output_flow(struct rtable **rp, struct flowi *flp, struct sock *sk, int flags)
{
int err;
// 普通的路由查找过程, 此过程不是本文重点, 分析略
if ((err = __ip_route_output_key(rp, flp)) != 0)
return err;
// 如果流结构协议非0(基本是肯定的)进行xfrm路由查找
if (flp-<proto) {
// 指定流结构的源地址和目的地址
if (!flp-<fl4_src)
flp-<fl4_src = (*rp)-<rt_src;
if (!flp-<fl4_dst)
flp-<fl4_dst = (*rp)-<rt_dst;
// 根据流结构查找安全路由, 没找到的话创建新的安全路由, 最后形成安全路由链表
// 见前几节中的分析
return xfrm_lookup((struct dst_entry **)rp, flp, sk, flags);
}
return 0;
}
对于不是进入ip_queue_xmit()发送的数据包, 在发送前必然也是经过ip_route_output_flow()函数的路由选择处理, 因此如果需要IPSEC封装的话, 也就设置了相关的安全路由链表.
这样, 对于自身发出的数据包, 最终也是进入dst_output()函数进行发送, 转发和自身发出的数据殊途同归了, 以后的处理过程就都是相同的了
函数流程小结:
ip_queue_xmit
-< ip_route_output_flow
-< xfrm_lookup
-< xfrm_find_bundle
-< bundle_create
-< afinfo-<bundle_create == __xfrm4_bundle_create
-< xfrm_dst_lookup
-< afinfo-<dst_lookup == xfrm4_dst_lookup
-< __ip_route_output_key
-< dst_list
-< dst-<list=policy_bundles, policy-<bundles = dst
-< NF_HOOK(NF_OUTPUT)
-< dst_output
-< dst-<output
9.3 dst_output
/* include/net/dst.h */
/* Output packet to network from transport. */
static inline int dst_output(struct sk_buff *skb)
{
return skb-<dst-<output(skb);
}
dst_output()函数就是调用路由项的输出函数, 对于安全路由, 该函数是xfrm4_output()函数, 对于普通路由, 是ip_output()函数
对于xfrm4_output()函数的分析见7.6, 执行完所有安全路由的输出函数, 每执行一个安全路由输出函数就是一次IPSEC封装处理过程, 封装结束后的数据包会设置IPSKB_REROUTED标志, 到路由链表的最后一项是普通路由, 进入普通路由的输出函数ip_output:
int ip_output(struct sk_buff *skb)
{
struct net_device *dev = skb-<dst-<dev;
IP_INC_STATS(IPSTATS_MIB_OUTREQUESTS);
skb-<dev = dev;
skb-<protocol = htons(ETH_P_IP);
// 如果是带IPSKB_REROUTED标志的数据包, 不进入POSTROUTING的SNAT处理, 直接执行
// ip_finish_output函数
return NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)-<flags & IPSKB_REROUTED));
}
因此对于封装的数据包而言, 在封装过程中可以进行OUTPUT点的过滤和POSTROUTING点的SNAT处理, 但一旦封装完成, 就不会再进行SNAT操作了.
函数调用小结:
xfrm_lookup: find xfrm_dst for the skb, create dst_list
-< xfrm_sk_policy_lookup
-< flow_cache_lookup
-< xfrm_find_bundle
-< xfrm_policy_lookup_bytype
-< xfrm_tmpl_resolve
-< xfrm_tmpl_resolve_one
-< xfrm_get_saddr
-< afinfo-<get_saddr == xfrm4_get_saddr
-< xfrm4_dst_lookup
-< xfrm_state_find
-< __xfrm_state_lookup
-< xfrm_state_alloc
-< km_query
-< km-<acquire (pfkey_acquire, xfrm_send_acquire)
-< xfrm_state_sort
-< afinfo-<state_sort == NULL
-< km_wait_queue
-< xfrm_bundle_create
dst_output: loop dst_list
-< dst-<output == xfrm_dst-<output == xfrm4_output == xfrm4_state_afinfo-<output
-< NF_HOOK(POSTROUTING)
-< xfrm4_output_finish
-< gso ?
-< xfrm4_output_finish2
-< xfrm4_output_one
-< mode-<output
-< type-<output
-< skb-<dst=dst_pop(skb-<dst)
-< nf_hook(NF_OUTPUT)
-< !dst-<xfrm
-< dst_output
-< nf_hook(POSTROUTING)
-< dst-<output == ip_output
-< NF_HOOK(POSTROUTING)
-< ip_finish_output
-< ip_finish_output2
-< hh_output == dev_queue_xmit
10. 总结
Linux自带的native ipsec实现xfrm是通过路由来实现IPSEC封装处理的, 这和freeswan是类似的, 只不过freeswan构造了虚拟的ipsec*网卡设备, 这样就可以通过标准的网络工具如iproute2等通过配置路由和ip rule等实现安全策略, 进入该虚拟网卡的数据包就进行IPSEC解封, 从虚拟网卡发出的包就是进行IPSEC封装,因此实现比较独立,除了NAT-T需要修改udp.c源码外,其他基本不需要修改内核源码,对于进入的IPSEC包,在物理网卡上可以抓到原始的IPSEC包,而从虚拟网卡上可以抓到解密后的数据包。而xfrm没有定义虚拟网卡,都是在路由查找过程中自动查找安全策略实现ipsec的解封或封装,因此该实现是必须和内核网络代码耦合在一起的,对于进入的IPSEC包,能在物理网卡抓到两次包,一次是IPSEC原始包,一次是解密后的包。由于还是需要根据路由来进行封装,所以本质还不是基于策略的IPSEC,不过可以通过定义策略路由方式来实现基于策略IPSEC,要是能把IPSEC封装作为一个netfilter的target就好了,这样就可以进行标准的基于策略的IPSEC了。
xfrm和网络代码耦合,这样进行路由或netfilter过滤时都可以通过相关标志进行处理或旁路,如经过IPSEC处理后的数据包是自动不会进行SNAT操作的,而freeswan的实现就不能保证,如果设置SNAT规则不对,是有可能对封装好的包进行SNAT操作而造成错误。但两个实现对于封装前的数据包都是可以进行SNAT操作的,因此那种实现同网段VPN的特殊NAT可以在xfrm下实现。
在RFC2367中只定义了SA相关操作的消息类型,而没有定义SP的操作类型,也没有定义其他扩展的IPSEC功能的相关消息类型,如NAT-T相关的类型,那些SADB_X_*的消息类型就是非标准的,这就造成各种IPSEC实现只能自己定义这些消息类型,因此可能会造成不兼容的现象,应该尽快出新的RFC来更新2367了。