文章目录
1. 序言
openswan源码中有关隧道协商的文章已经比较久没有更新了,那么从这篇开始再重新回到更新流程上。这中间停了将近2个月,第一个月几乎没有更新任何博客,而第二个月主要整理翻译QAT相关的文章,接下来我将继续更新openswan源码相关的内容。初步计划按下表的顺序进行更新:
序号 | 内容 | 说明 |
---|---|---|
1 | 快速模式三个报文流程 | 计划一周2篇文章 |
2 | IKEv2协商流程 | 首先还是先整理协商流程接口和基本知识点 |
3 | 国密标准 | 国密标准存在多个,计划以2014标准为例进行说明 |
4 | 野蛮模式 | 目前接触的比较少,后续再进行补充 |
5 |
下面开始介绍IPSec 快速模式协商流程中的第①包,主要函数的入口为quick_outI1():
2. quick_outI1()流程图
3. quick_outI1()源码分析
quick_outI1()
接口是第二阶段快速模式第一包的入口函数,它最主要的工作就是将第一阶段协商的ipsecsa状态信息转换为第二阶段的状态信息。通过duplicate_state
实现状态的拷贝,然后将新的状态插入到全局的状态表中。之后就是根据隧道的配置信息(PFS, 算法信息)等做秘钥申请等准备工作。
- 复制第一阶段ipsecsa状态,并将其插入全局状态表中
- 显示第二阶段算法相关的debug信息
- 根据配置做秘钥申请
stf_status
quick_outI1(int whack_sock
, struct state *isakmp_sa
, struct connection *c
, lset_t policy
, unsigned long try
, so_serial_t replacing
, struct xfrm_user_sec_ctx_ike * uctx UNUSED
)
{
struct state *st = duplicate_state(isakmp_sa);/*复制第一阶段的状态,包括了所有的基本信息*/
struct qke_continuation *qke;
stf_status e;
const char *pfsgroupname;
char p2alg[256];
st->st_whack_sock = whack_sock;
st->st_connection = c;/*从这里可以看出每一个连接c可对应多个state结构,比如一个phase1,一个phase2.因此在隧道状态时需要特别注意*/
passert(c != NULL);
if(st->st_calculating) {
return STF_IGNORE;
}
set_cur_state(st); /* we must reset before exit */
st->st_policy = policy;
st->st_try = try;
#ifdef HAVE_LABELED_IPSEC
st->sec_ctx=NULL;
if(uctx != NULL) {
st->sec_ctx = clone_thing(*uctx, "sec ctx structure");
DBG(DBG_CONTROL, DBG_log("pending phase 2 with security context %s, %d", st->sec_ctx->sec_ctx_value, st->sec_ctx->ctx_len));
}
#endif
/*本端协议、对端协议、本端端口、对端端口*/
st->st_myuserprotoid = c->spd.this.protocol;
st->st_peeruserprotoid = c->spd.that.protocol;
st->st_myuserport = c->spd.this.port;
st->st_peeruserport = c->spd.that.port;
st->st_msgid = generate_msgid(isakmp_sa);/*随机生成唯一的msgid*/
change_state(st, STATE_QUICK_I1);/*设置当前状态为STATE_QUICK_I1*/
insert_state(st); /* needs cookies, connection, and msgid */
strcpy(p2alg, "defaults");
if(st->st_connection->alg_info_esp) {
/*将alg_info_esp中的算法(第二阶段算法信息)解析转换为字符串,存储在p2alg*/
alg_info_snprint_phase2(p2alg, sizeof(p2alg)/*只是为了显示使用*/
, (struct alg_info_esp *)st->st_connection->alg_info_esp);
}
pfsgroupname="no-pfs";
/*
* See if pfs_group has been specified for this conn,
* if not, fallback to old use-same-as-P1 behaviour
*/
if (st->st_connection) {
/*获取pfs组*/
st->st_pfs_group = ike_alg_pfsgroup(st->st_connection
, st->st_policy);
}
/* If PFS specified, use the same group as during Phase 1:
* since no negotiation is possible, we pick one that is
* very likely supported.
*/
if (!st->st_pfs_group)
st->st_pfs_group = policy & POLICY_PFS? isakmp_sa->st_oakley.group : NULL;
if(policy & POLICY_PFS && st->st_pfs_group) {
pfsgroupname = enum_name(&oakley_group_names, st->st_pfs_group->group);
}
{
char replacestr[32];
replacestr[0]='\0';
if(replacing != SOS_NOBODY)
snprintf(replacestr, 32, " to replace #%lu", replacing);
openswan_log("initiating Quick Mode %s%s {using isakmp#%lu msgid:%08x proposal=%s pfsgroup=%s}"
, prettypolicy(policy)
, replacestr
, isakmp_sa->st_serialno, st->st_msgid, p2alg, pfsgroupname);
}
qke = alloc_thing(struct qke_continuation , "quick_outI1 KE");
qke->replacing = replacing;
pcrc_init(&qke->qke_pcrc);
qke->qke_pcrc.pcrc_func = quick_outI1_continue;/*对于KE载荷、NONCE载荷的填充是在此回调函数中实现的*/
if(policy & POLICY_PFS) {
/*生成KE载荷*/
e=build_ke(&qke->qke_pcrc, st, st->st_pfs_group, st->st_import);
} else {
/*生成NONCE载荷*/
e=build_nonce(&qke->qke_pcrc, st, st->st_import);
}
reset_globals();
return e;
}
这个函数中应该注意到一点:就是一个connection(隧道)可以对应多个state结构。这有什么影响呢?
我们在查询隧道状态时,是通过查询该隧道(connection)对应的state来获取到协商的阶段,但是我们在遍历全局state表时只有全部遍历一遍才能查到最新的协商阶段,否则可能只是查询其中的一个state,这个可能不是最新的state。这样的话如果不采用效率高的数据结构存储状态,随着state增多,遍历的效率会很低。
**PFS(Perfect Forward Secrecy,完善的前向安全性)**是一种安全特性,指一个密钥被破解(例如说协商的第一阶段秘钥被破解),并不影响第二阶段密钥的安全性,因为这些密钥间没有派生关系。此特性是通过在IKE第二阶段的协商中增加密钥交换来实现的,因此源码实现中,如果策略启动了PFS,则再次增加一个KE载荷进行秘钥交换。
4. quick_outI1_continue()源码分析
这个continue函数与之前的函数功能基本一致,通过pcrc中的状态序号获取到相应的状态,然后调用后续的函数进行报文封装操作。
static void
quick_outI1_continue(struct pluto_crypto_req_cont *pcrc
, struct pluto_crypto_req *r
, err_t ugh)
{
struct qke_continuation *qke = (struct qke_continuation *)pcrc;
struct state *const st = state_with_serialno(qke->qke_pcrc.pcrc_serialno);/*一个效率比较低的接口*/
stf_status e;
DBG(DBG_CONTROLMORE
, DBG_log("quick outI1: calculated ke+nonce, sending I1"));
if (st == NULL) {
loglog(RC_LOG_SERIOUS, "%s: Request was disconnected from state",
__FUNCTION__);
if (qke->md)
release_md(qke->md);
return;
}
st->st_calculating = FALSE;
/* XXX should check out ugh */
passert(ugh == NULL);
passert(cur_state == NULL);
passert(st != NULL);
set_cur_state(st); /* we must reset before exit */
set_suspended(st, NULL);
e = quick_outI1_tail(pcrc, r, st);
if (e == STF_INTERNAL_ERROR)
loglog(RC_LOG_SERIOUS, "%s: quick_outI1_tail() failed with STF_INTERNAL_ERROR", __FUNCTION__);
reset_globals();
}
这个有一个需要说明的地方,state_with_serialno
函数需要遍历全局state哈希表,虽然O(n)的时间复杂度,但是如果state结构非常多的情况下,效率很低。因此如果应用场景中可添加的隧道比较多(成百上千条),那么需要对该接口进行优化。
state_with_serialno()
源码实现如下:
/* Find the state object with this serial number.
* This allows state object references that don't turn into dangerous
* dangling pointers: reference a state by its serial number.
* Returns NULL if there is no such state.
* If this turns out to be a significant CPU hog, it could be
* improved to use a hash table rather than sequential seartch.
*/
struct state *
state_with_serialno(so_serial_t sn)
{
if (sn >= SOS_FIRST)
{
struct state *st;
int i;
for (i = 0; i < STATE_TABLE_SIZE; i++)
for (st = statetable[i]; st != NULL; st = st->st_hashchain_next)
if (st->st_serialno == sn)