《openswan》专栏系列文章主要是记录openswan源码学习过程中的笔记。
- Author : 叨陪鲤
- Email : vip_13031075266@163.com
- Date : 2020.11.22
- Copyright : 未经同意不得转载!!!
- Version : openswan-2.6.51.5
- Reference :https://download.openswan.org/openswan/
目录
1. 加密流程状态机实现:
如果还不知道状态机是什么东东的,可以参考文章《C语言实现有限状态机》,该博客里简单的说明了状态机的实现原理和步骤。这里跳过状态机的初级讲解,而是直接进入高阶实现。
下面是IPSec封装流程中的核心:加密状态机
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},
};
状态机核心调度处理流程代码:
void
ipsec_xsm(struct ipsec_xmit_state *ixs)
{
enum ipsec_xmit_value stat = IPSEC_XMIT_ENCAPFAIL;
unsigned more_allowed;
if (ixs == NULL) {
return;
}
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;
}
}
ixs->xsm_complete(ixs, stat);
}
2. 各状态函数基本功能总览:
状态 | 对应的动作(函数) | 作用 | 备注 |
IPSEC_XSM_INIT1 | ipsec_xmit_init1 | 根据outgoing_id查找对应的IPsec_sa | |
IPSEC_XSM_INIT2 | ipsec_xmit_init2 | 根据IPsec_sa封装策略确定统计封装需要空间大小 | |
IPSEC_XSM_ENCAP_INIT | ipsec_xmit_encap_init | 开始封装,开辟报文头部、尾部空间 | 真正开始修改报文内容 |
IPSEC_XSM_ENCAP_SELECT | ipsec_xmit_encap_select t | 根据IPsecsa封装协议选择封装套件 | |
IPSEC_XSM_ESP | ipsec_xmit_esp | ESP封装套件 | |
IPSEC_XSM_ESP_AH | ipsec_xmit_esp_ah | ESP+AH封装套件 | |
IPSEC_XSM_AH | ipsec_xmit_ah | AH封装套件 | |
IPSEC_XSM_IPIP | ipsec_xmit_ipip | 隧道模式封装套件 | |
IPSEC_XSM_IPCOMP | ipsec_xmit_ipcomp | IPcom封装套件 | |
IPSEC_XSM_CONT | ipsec_xmit_cont | 根据IPsecsa封装策略确定是否需要继续封装 | 是否需要继续封装 |
IPSEC_XSM_DONE | NULL | IPsec封装完毕 |
4. 各状态功能概要
4.1 ipsec_xmit_init1():
此函数最关键的功能便是根据outgoing_said获取到对应的IPSec SA。 outgoing_said是在IPSec匹配流程根据报文五元组找到对应的eroute,其中eroute结构中包含said指针。
- 根据outgoing_said查询ipsec sa
- ixs->ipsp = ipsec_sa_getbyid(&ixs->outgoing_said, IPSEC_REFTX);
- 查询失败,返回:IPSEC_XMIT_SAIDNOTFOUND
- 构建ixs->ips
4.2 ipsec_xmit_init2():
在IKE协商流程成功时,会构建IPSec SA时用来加密业务流量信息。IPSec SA的是一组用来描述封装信息的结构。一个IPSec 隧道可能包含多个IPSec SA节点(视封装协议决定),这几个节点共同构成此隧道的IPSecSA链,在进行报文封装时,依据IPSecSA链进行依次封装,封装包含严格的顺序:通常情况下按下图顺序进行封装:
下面介绍ipsec_xmit_init2()函数的主要功能:
①保存IPSec SA链的头部:
save_ipsp=ixs->ipsp; |
②统计IPsec封装需要的headroom和tailroom
- AH协议
ixs->headroom += sizeof(struct ahhdr); |
-
ESP协议
ESP协议实现较为复杂。在openswan源码实现中提到了一个OCF技术,这是一种开源加密加速框架,OCF是一种开源加速框架,支持DES, 3DES, AES, MD5, SHA1等算法。详情可参考:http://ocf-linux.sourceforge.net/
下面考虑系统不支持OCF加密框架:
1) 获取加密算法:
ixs->ixt_e = ixs->ipsp->ips_alg_enc |
ixs->blocksize = ixs->ixt_e->xxx |
ixs->headroom = ixs->ixt_e->ooo |
2) 获取认证算法:
ixs->ixt_a=ixs->ipsp->ips_alg_auth |
ixs->tailroom += AHHMAC_HASHLEN |
【note】:加密、认证算法都会至少要求4字节对齐,因此openswan中使用了一套组合拳顺利将我打蒙:
ixs->tailroom += ixs->blocksize != 1 ?
((ixs->blocksize - ((ixs->pyldsz + 2) % ixs->blocksize)) % ixs->blocksize) + 2 :
((4 - ((ixs->pyldsz + 2) % 4)) % 4) + 2;
这里我做了一个简单计算:
Pyldsz | ((ixs->pyldsz + 2) % 4) | (4 - ((ixs->pyldsz + 2) % 4)) | ((4 - ((ixs->pyldsz + 2) % 4)) % 4) | ((4 - ((ixs->pyldsz + 2) % 4)) % 4) + 2 |
0 | 2 | 2 | 2 | 4 |
1 | 3 | 1 | 1 | 3 |
2 | 0 | 4 | 0 | 2 |
3 | 1 | 3 | 3 | 5 |
4 | 2 | 2 | 2 | 4 |
5 | 3 | 1 | 1 | 3 |
6 | 0 | 4 | 0 | 2 |
7 | 1 | 3 | 3 | 5 |
8 | 2 | 2 | 2 | 4 |
9 | 3 | 1 | 1 | 3 |
从上表中可以看出:第一列值+最后一列值都是4的整数倍。因此上述那个计算表达式应该就是这个意思:【tailroom+pyldsz 要保证4字节对齐】
3) NAT-T封装空间预留:
如果IPSec SA上表明需要使用NAT-T,则需要根据报文类型确定采用封装方式:
3.1)IKE协商报文
使用ESPINUDP_WITH_NON_IKE格式封装: | UPD头 + 8字节 |
3.2)业务流量报文
使用ESPINUDP_WITH_NON_ESP格式封装 | UPD头 |
【特别说明】:
ESPINUDP_WITH_NON_IKE封装包,UDP头部后面的8字节全部为0,而ESPINUDP_WITH_NON_ESP报文UDP头部之后紧跟SPI值,此参数不可能全部为0(8个字节全部为0)。从而区分开来是IKE协商报文or业务流量报文。
最后:计算NAT-T需要预留的空间大小: | ixs->tailroom += ixs->natt_head; |
-
IPIP协议
1)添加一个IP头部空间(IPv4 or IPv6):
ixs->headroom += sizeof(struct ipv6hdr); |
ixs->headroom += sizeof(struct iphdr); |
2)在ixs上记录下一个头部协议:
ixs->ipip_proto = IPPROTO_IPV6 or IPPROTO_IPIP |
-
IPcom协议
略。(openswan此版本未实现...)
将headroom, tailroom更新到ixs上,遍历此链上后续的IPSec SA协议(重新从②开始); | ixs->ipsp = ixs->ipsp->ips_next; |
③IPSec封装链上的多个IPSec SA协议的headroom、tailroom统计完毕后,将ixs->ipsp指向此链表头部:
ixs->ipsp = saved_ipsp; |
④计算MTU相关
⑤传输模式时:根据mtu调整TCP协议SYN包中的MSS值
tcph->syn && !tcph->ack | ipsec_adjust_mss(ixs->skb, tcph, ixs->cur_mtu) |
⑥保存二层头,并将报文中的二层头去除
If(!ixs->hard_header_stripped) |
|
| memcpy(&ixs->saved_header[0], &ixs->skb->data[0], ixs->hard_header_len) |
| skb_pull(ixs->skb, ixs->hard_header_len); |
| ixs->hard_header_stripped = 1; |
【追踪溯源】:
在IPSec_xmit_init2()函数中会根据算法信息(encalg, authalg, keylen, iv,etc.)计算需要的headroom 和 tailroom。其中算法的来源如下:
4.3 ipsec_xmit_encap_init():
① 获取IP头部长度和报文负载长度
ixs->iphlen = osw_ip4_hdr(ixs)->ihl << 2; |
ixs->pyldsz = ntohs(osw_ip4_hdr(ixs)->tot_len) - ixs->iphlen; |
② 根据ipsecsa链上的封装协议移动报文数据(head, tail)
1) AH协议
ixs->headroom += sizeof(struct ahhdr); |
2) ESP协议
2.1) 加密算法
-
OCF加速框架?
(i)DES/3DES
ixs->blocksize = 8; |
ixs->headroom += ESP_HEADER_LEN + 8/*IV size* |
(ii)AES
ixs->blocksize = 16; |
ixs->headroom += ESP_HEADER_LEN + 16/*IV size*/; |
-
从加密策略上获取blocksize和headroom
ixs->blocksize = ixs->ixt_e->xxx; |
ixs->headroom += ESP_HEADER_LEN + ixs->ixt_e->ooo; |
2.2) 认证算法
-
OCF加速框架?
(i)MD5/SHA1
ixs->authlen = AHHMAC_HASHLEN; |
-
从加密策略上获取authlen:MD5/SHA1
ixs->authlen = AHHMAC_HASHLEN; |
2.3) tailroom
(i)填充到四字节整数倍
(ii)加上认证长度(12字节)
3)IPIP协议
- 外层为IPv6头部
ixs->headroom += sizeof(struct ipv6hdr); |
ixs->iphlen = sizeof(struct ipv6hdr); |
new_version = 6; |
- 外层为IPv4头部
ixs->headroom += sizeof(struct iphdr); |
ixs->iphlen = sizeof(struct iphdr); |
new_version = 4; |
3)IPIP协议
目前不支持
③ 根据headroom和tailroom修改报文head、tail
ixs->dat = skb_push(ixs->skb, ixs->headroom); |
skb_put(ixs->skb, ixs->tailroom); |
ixs->ilen = ixs->skb->len - ixs->tailroom; |
ixs->len = ixs->skb->len; |
④ 填充新头部信息
memmove((void *)ixs->dat, (void *)(ixs->dat + ixs->headroom), ixs->iphlen); |
ixs->iph = (void *)ixs->dat; |
osw_ip4_hdr(ixs)->tot_len = htons(ixs->skb->len); |
这里需要注意的是:新头和旧IP头类型相同时,直接上旧IP头部移动(memmove)到新头位置,此时旧头中的ip->id是有效的。此外新封装的IP报文却是沿用了原先的ip->id。