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 <= 1)
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)
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 << 12) | (5 << 8) | (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了。