1. 序言
openswan源码中有关隧道协商的文章已经比较久没有更新了,那么从这篇开始再重新回到更新流程上。这中间停了将近2个月,第一个月几乎没有更新任何博客,而第二个月主要整理翻译QAT相关的文章,接下来我将继续更新openswan源码相关的内容。
下面开始介绍IPSec 快速模式协商流程中的第①包,主要函数的入口为**quick_outI1()**:
2. quick_outI1()流程图
3. quick_outI1()源码分析
quick_outI1()
接口是第二阶段快速模式第一包的入口函数,它最主要的工作就是将==第一阶段协商的ipsecsa状态信息转换为第二阶段的状态信息==。通过duplicate_state
实现状态的拷贝,然后将新的状态插入到全局的状态表中。之后就是根据隧道的配置信息(PFS, 算法信息)等做秘钥申请等准备工作。
- 复制第一阶段ipsecsa状态,并将其插入全局状态表中
- 显示第二阶段算法相关的debug信息
- 根据配置做秘钥申请
1 | stf_status |
这个函数中应该注意到一点:就是一个connection(隧道)可以对应多个state结构。这有什么影响呢?
我们在查询隧道状态时,是通过查询该隧道(connection)对应的state来获取到协商的阶段,但是我们在遍历全局state表时只有全部遍历一遍才能查到最新的协商阶段,否则可能只是查询其中的一个state,这个可能不是最新的state。这样的话如果不采用效率高的数据结构存储状态,随着state增多,遍历的效率会很低。
PFS(Perfect Forward Secrecy,完善的前向安全性)是一种安全特性,指一个密钥被破解(例如说协商的第一阶段秘钥被破解),并不影响第二阶段密钥的安全性,因为这些密钥间没有派生关系。此特性是通过在IKE第二阶段的协商中增加密钥交换来实现的,因此源码实现中,如果策略启动了PFS,则再次增加一个KE载荷进行秘钥交换。
4. quick_outI1_continue()源码分析
这个continue函数与之前的函数功能基本一致,通过pcrc中的状态序号获取到相应的状态,然后调用后续的函数进行报文封装操作。
1 | static void |
这个有一个需要说明的地方,state_with_serialno
函数需要遍历全局state哈希表,虽然O(n)的时间复杂度,但是如果state结构非常多的情况下,效率很低。因此如果应用场景中可添加的隧道比较多(成百上千条),那么需要对该接口进行优化。
state_with_serialno()
源码实现如下:
1 | /* Find the state object with this serial number. |
5. quick_outI1_tail()源码分析
quick_outI1_tail()函数的作用:构造第二阶段首包报文,他包括:
- 第二阶段的加解密算法、哈希(认证)算法、PFS等策略信息
- 构造SA建议载荷out_sa()
- 如果启动PFS,则重新进行秘钥交换,生成KE载荷
- 生成Nonce载荷
- 构造本端标识和对端标识载荷
- NAT穿越中的 OA载荷
- ==计算报文的完整性(哈希算法)==
- ==对报文进行加密==
源码如下:
1 |
|
下面对quick_outI1_tail()
中的几个重要函数做个简单说明:
5.1 out_sa()
这个函数在第一阶段的前两个报文中使用过,当时使用的第一阶段的SA载荷,现在使用第二阶段的SA载荷;out_sa
同时实现了第一阶段和第二阶段SA载荷封装的功能,它通过bool oakley_mode
参数来确定使用第一阶段还是第二阶段的封装流程。如果说out_struct
等封装接口已经比较熟的话,那么这个函数可能会比较容易,否则基本流程看起来还是有点吃力。这里只简单说明out_struct
各个参数的作用:
1 | /**************************************************************** |
openswan源码在对齐上做的不敢恭维,而代码是不忍卒读(看不懂)。
1 | bool |
一般而言,比较关心我们配置的参数在哪里生效? 例如加密算法、认证算法、隧道模式or传输模式都是在out_sa()
中通过属性载荷封装在报文中的。下图为属性载荷结构:
属性类型的最高比特位AF指定数据为定长还是变长,如果为0表示定长;如果为1表示变长。
具体属性类型有以下几种(全是定长类型):
属性类型 | 属性类型取值 | 属性值说明 |
---|---|---|
SA生存周期 | 1 | 0: 保留 1:秒 2:千字节 |
SA生存期 | 2 | 0: 保留 1:秒 2:千字节 |
组描述 | 3 | 略 |
封装模式 | 4 | 0:保留 1:隧道模式 2:传输模式 |
认证算法 | 5 | 0:RESERVED 1:HMAC-MD5 2:HMAC-SHA …. |
密钥长度 | 6 | 略 |
密钥轮数 | 7 | 略 |
压缩字典长度 | 8 | 略 |
私有压缩算法 | 9 | 略 |
注:加密算法不适用属性载荷进行封装。
5.2 emit_subnet_id()
第二阶段除了协商加解密算法信息,还会对双方的保护子网进行匹配。而保护子网是通过ID载荷来传输的。在第一阶段中使用build_id_payload()
接口将我们在配置隧道的“身份标识”发送对方以供双方认证,第二阶段使用emit_subnet_id()
来协商两端的保护子网信息。
每一条隧道有本端和对端两个节点,这两个节点都是用struct end
结构描述,而两端的保护子网使用struct end
中的ip_subnet client;
描述,ip_subnet
结构如下:
1 | typedef struct { |
1 | /* Initiate quick mode. |
ID载荷(标识载荷)包含以下几种类型:
ID类型 | 描述 | 取值 |
---|---|---|
ID_NONE | 未使用 | 0 |
ID_IPV4_ADDR | 单独的一个IPv4地址 | 1 |
ID_FQDN | 全域名字符串,如topsec.com.cn | 2 |
ID_USER_FQDN | 用户名字符串,如li_si@topsec.com.cn | 3 |
ID_RFC822_ADDR | 同ID_USER_FQDN | ID_USER_FQDN |
ID_IPV4_ADDR_SUBNET | IPv4类子网地址,如192.168.1.1 255.255.255.0 | 4 |
ID_IPV6_ADDR | 单独IPv6地址 | 5 |
ID_IPV6_ADDR_SUBNET | IPv6子网地址 | 6 |
ID_IPV4_ADDR_RANGE | IPv4地址范围区间, 如192.168.2.3 192.168.2.200 | 7 |
ID_IPV6_ADDR_RANGE | IPv6地址范围区间 | 8 |
ID_DER_ASN1_DN | x.500编码格式 | 9 |
ID_DER_ASN1_GN | x.500编码格式 | 10 |
ID_KEY_ID | 传递特定厂商信息的字节流 | 11 |
5.3 encrypt_message()
报文的加密范围:除了ISAKMP头部之外都需要进行加密。加密使用第一阶段协商的加密秘钥(报文认证时同时也会用到认证密钥)。
1 | /* encrypt message, sans fixed part of header |
5.4 out_modify_previous_np()
此函数的作用在于修改前一个载荷头部中的下一个载荷字段。它在填充NAT-T相关的OA载荷时用到。基本原理是从头部开始向后遍历每一个载荷,直到找到最后一个载荷的头部(尚未填充新的载荷,因此它还是最后一个载荷)。
1 | bool |
6. 小结
略