- Author : 叨陪鲤
- Email : vip_13031075266@163.com
- Date : 2020.11.30
- Copyright : 未经同意不得转载!!!
- Version : openswan-2.6.51.5
- Reference:https://download.openswan.org/openswan/
目录
1. ipsec封装状态机:
ipsec封装状态机是一个很经典有限状态机实现:(当前状态,执行动作,下一个状态),同时它的核心调度流程也是比较简单:如果当前状态执行成功,则跳转至下一状态。此外在ipsec状态机中,可以通过实际参数配置(如ESP封装、IP隧道封装、AH封装等)来自动选择下一状态,这个是在ipsec_xmit_cont()函数中实现的。
我们直接来看状态机和核心调度流程:
struct { enum ipsec_xmit_value (*action)(struct ipsec_xmit_state *ixs); int next_state; } xmit_state_table[] = { [IPSEC_XSM_INIT1] = {ipsec_xmit_init1, IPSEC_XSM_INIT2 }, [IPSEC_XSM_INIT2] = {ipsec_xmit_init2, IPSEC_XSM_ENCAP_INIT }, [IPSEC_XSM_ENCAP_INIT] = {ipsec_xmit_encap_init, IPSEC_XSM_ENCAP_SELECT }, [IPSEC_XSM_ENCAP_SELECT] = {ipsec_xmit_encap_select, IPSEC_XSM_DONE }, [IPSEC_XSM_ESP] = {ipsec_xmit_esp, IPSEC_XSM_ESP_AH }, [IPSEC_XSM_ESP_AH] = {ipsec_xmit_esp_ah, IPSEC_XSM_CONT }, [IPSEC_XSM_AH] = {ipsec_xmit_ah, IPSEC_XSM_CONT }, [IPSEC_XSM_IPIP] = {ipsec_xmit_ipip, IPSEC_XSM_CONT }, [IPSEC_XSM_IPCOMP] = {ipsec_xmit_ipcomp, IPSEC_XSM_CONT }, [IPSEC_XSM_CONT] = {ipsec_xmit_cont, IPSEC_XSM_DONE }, [IPSEC_XSM_DONE] = {NULL, IPSEC_XSM_DONE}, }; |
ipsec_xsm(struct ipsec_xmit_state *ixs) { enum ipsec_xmit_value stat = IPSEC_XMIT_ENCAPFAIL; unsigned more_allowed;
more_allowed = 1000; while (ixs->state != IPSEC_XSM_DONE && --more_allowed) { ixs->next_state = xmit_state_table[ixs->state].next_state; stat = xmit_state_table[ixs->state].action(ixs);
if (stat == IPSEC_XMIT_OK) { ixs->state = ixs->next_state; } else if (stat == IPSEC_XMIT_PENDING) { return; } else { /* bad result, force state change to done */ ixs->state = IPSEC_XSM_DONE; } } /* * let the caller continue with their processing */ ixs->xsm_complete(ixs, stat); } |
IP报文在经过ipsec_xsm状态机完成报文封装后,进入到xsm_complete函数进行后续收尾处理,之后发送此ipsec报文。
2. ipsec发送流程:
此部分函数入口为:
ipsec_tunnel_xsm_complete(ixs, stat); |
此函数的函数调用关系基本如下所示:
|
- 判断封装完毕后的状态是否成功:
If (stat != IPSEC_XMIT_OK) |
如果不正确,则说明封装有错误,则将释放相应的资源。
- 获取新封装完毕的报文五元组信息,重新组成ixs->matcher:
ixs->matcher.sen_type = SENT_IP4 |
协议族类型 |
ixs->matcher.sen_ip_src.s_addr = osw_ip4_hdr(ixs)->saddr; |
源IP |
ixs->matcher.sen_ip_dst.s_addr = osw_ip4_hdr(ixs)->daddr; |
目的IP |
nexthdr = osw_ip4_hdr(ixs)->protocol; |
四层协议 |
ixs->matcher.sen_proto = nexthdr; |
- |
- 获取新封装完毕的报文端口信息,存储在ixs->matcher:
udp = skb_header_pointer(skb, nexthdroff, sizeof(*udp), &_udp); |
ixs->matcher.sen_sport = udp->source; |
ixs->matcher.sen_dport = udp->dest; |
- 再次根据ixs->matcher查询eroute路由表:
ixs->eroute = ipsec_findroute(&ixs->matcher); |
查询eroute路由表 |
ixs->outgoing_said = ixs->eroute->er_said; |
查到后记录said |
ixs->eroute_pid = ixs->eroute->er_pid; |
|
ixs->eroute->er_count++; |
|
- 如果封装后的报文仍然处于其他隧道的保护子网中,则再次进行封装:
ip_address_cmp(&ixs->orgedst, &ixs->outgoing_said.dst) != 0 && |
!ip_address_isany(&ixs->outgoing_said.dst) && |
ixs->eroute |
上述三个条件同时满足的情况下,则进入封装状态机ipsec_xsm(ixs)继续新的隧道封装。
- 如果隧道中间经过NAT设备,则进行NAT-T封装
NAT-T封装报文基本格式如下:
在此基础上,需要区分IKE协商报文和业务流量报文(一般是ESP)。当ipsec隧道经过NAT(NATP)设备时,IKE通过端口浮动(port floating)将端口由500浮动到4500;而业务流量报文为了适应NAT环境,也需要进行UDP格式封装,两者仅仅通过端口是无法区分的(目的端口都是4500,源端口可由NAT修改)。因此采用了一个特殊标记来分区:
IKE协商过程中(IKEv1或者IKEv2),最初的两个字段分别为Initiator SPI和Responder SPI, 它们各占用8Bytes。 在IKE协商过程中有一条规则:Initiator SPI不能为0, 而Responder SPI在第一包中可以为0。基于上述规则,IPsec业务流量报文封装时,在UDP头部之后添加8字节的额外空间,全部填充0,NAT-T便是通过这8个字节是否全部为0来区分协商报文和业务报文的。规则如下:
UDP后面的8个字节如果不为0, 则是IKE协商报文(IKE协议要求的) |
UDP后面的8个字节全为0, 则是IPSec业务流量报文. |
因此,在Ipsec_xmit_init2()函数中,预留头部和尾部空间时,NAT-T的相关操作为:
switch (ixs->natt_type) { case ESPINUDP_WITH_NON_IKE: ixs->natt_head = sizeof(struct udphdr)+(2*sizeof(__u32)); break; case ESPINUDP_WITH_NON_ESP: ixs->natt_head = sizeof(struct udphdr); break; default: goto cleanup; break; } |
后续操作便是将报文内容后移,填充UDP头部(UDP头部中的校验和未填充):
源端口 |
udp->source = htons(ixs->natt_sport); |
目的端口 |
udp->dest = htons(ixs->natt_dport); |
udp长度 |
udp->len = htons(ntohs(ipp->tot_len) - ixs->iphlen); |
IP头部协议字段 |
ipp->protocol = IPPROTO_UDP; |
IP头部校验和 |
ipp->check = 0; ipp->check = ip_fast_csum((unsigned char *)ipp, ipp->ihl); |
至此, NAT-T场景下,UDP封装IPsec报文操作完毕。
- 填充封装后报文的二层头
因为我们在ipsec_xmit_init2中已经将原先IP报文的二层头存储在ixs->saved_header中,在此,我们再将其添加到封装完毕的IP报文前。
if(ixs->saved_header) { skb_push(ixs->skb, ixs->hard_header_len);/*skb_push:添加头部*/ { int i; for (i = 0; i < ixs->hard_header_len; i++) { ixs->skb->data[i] = ixs->saved_header[i]; } } } |
- 最后交由ipsec_tunnel_send()函数来发送封装完毕的报文。