一. VICI简介

本节介绍strongswan里的一个插件——VICI,全称为”Versatile IKE Control Interface”。

libcharon中的vici插件提供了多功能IKE控制接口,VICI试图通过提供稳定的IPC接口来改善系统集成商的情况,允许外部工具查询、配置和控制IKE守护进程。

1.1 传输协议

为了提供服务,插件使用可靠的、基于流的传输打开一个侦听套接字。客户端连接到此服务以访问功能。在关闭连接之前,它可以通过连接发送任意数量的数据包。为了交换数据​​,传输协议被分段为字节序列。每个字节序列都以网络顺序的32位长度标头为前缀,后面是数据。

1.2 数据包层

数据包的类型定义了其结构和用途。数据包类型是一个8位标识符,是传输层字节序列中的第一个字节。数据包的长度由传输层给出。
当前定义了以下数据包类型

  • CMD_REQUEST = 0: 一个带有标签的(被命名的)请求消息
  • CMD_RESPONSE = 1: 带有标签(被命名)的请求的未命名响应消息
  • CMD_UNKNOWN = 2:如果时非法请求消息,则返回一个类型为未知的消息
  • EVENT_REGISTER = 3:一个带有标签的(被命名的)事件注册请求
  • EVENT_UNREGISTER = 4:一个带有标签的(被命名的)事件解除注册请求
  • EVENT_CONFIRM = 5: 针对注册或者解注册时间消息的相应消息
  • EVENT_UNKNOWN = 6:如果注册或者解注册非法,则返回一个类型为未知事件的消息
  • EVENT = 7:一个命名的事件消息

对于具有命名类型的数据包,在数据包类型之后是名称的8位长度标头,指示名称标签的字符串长度(以字节为单位),不包括长度字段本身。该名称是一个不以null结尾的ASCII字符串。
数据包的其余部分形成交换的消息。

由以上数据包类型可以看出,数据包主要分为两类:一类是command;另一类是evnet

1. command

这类请求总是由客户端发起,服务端进行响应、或者返回一个CMD_UNKNOWN类型的错误消息来告知客户端当前服务端不支持这个命令请求。

2. event

客户端可以向服务端注册自己关注的事件,当事件发生时,将收到来自服务端的消息。当客户端决定不再接受消息时,可以进行解注册。服务器使用EVENT_CONFIRM确认事件注册,或者使用EVENT_UNKNOWN指示不存在此类事件源。

1.3 消息格式

消息使用类json结构,但更紧凑。消息的长度不是消息本身的一部分,而是包装层的一部分,通常根据传输字节序列长度计算得出。
消息编码由一系列元素组成。每个元素都以元素类型开头,后面可以选择元素名称和/或元素值。目前定义了以下消息元素类型:

  • SECTION_START = 1: 创建一个命名的盒子
  • SECTION_END = 2: 打包最近的一个盒子
  • KEY_VALUE = 3: 在当前盒子中创建一个键值对
  • LIST_START = 4: 创建一个命名列表
  • LIST_ITEM = 5: 在当前列表中创建一个未命名的值
  • LIST_END = 6: 打包最近的一个列表

这些类型被编码为8位值。具有名称(SECTION_START、KEY_VALUE 和 LIST_START)的类型在类型后面有一个ASCII字符串,该字符串本身使用8位长度的标头。字符串不以null结尾,字符串长度不包括长度字段本身。数字被编码为字符串。

1.4 编码示例

考虑以下的配置信息将如何被VICI协议编码:

1
2
3
4
5
6
7
key1 = value1
section1 = {
sub-section = {
key2 = value2
}
list1 = [ item1, item2 ]
}

上面的示例表示一个有效的树结构,它被编码为以下C数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
char msg[] = {
/* key1 = value1 */
3, 4,'k','e','y','1', 0,6,'v','a','l','u','e','1',
/* section1 */
1, 8,'s','e','c','t','i','o','n','1',
/* sub-section */
1, 11,'s','u','b','-','s','e','c','t','i','o','n',
/* key2 = value2 */
3, 4,'k','e','y','2', 0,6,'v','a','l','u','e','2',
/* sub-section end */
2,
/* list1 */
4, 5, 'l','i','s','t','1',
/* item1 */
5, 0,5,'i','t','e','m','1',
/* item2 */
5, 0,5,'i','t','e','m','2',
/* list1 end */
6,
/* section1 end */
2,
};

二. DAVICI

daviciVICI客户端协议的替代实现,旨在更好地集成其他软件堆栈。它使用异步、非阻塞API,并且可以集成到第三方主调度循环中,而无需使用线程。

davici需要应用框架提供监视文件描述符的状态即可(select()poll())。

2.1 command

davici支持命令调用。davici_new_cmd()创建一个命名的空消息请求。并且davici提供了一些接口用于遵循vici协议地进行填充消息。

一旦请求构建完成,就可以使用davici_queue()将这个请求加入发送队列。一旦收到服务器的响应,则异步调用用户注册在此次调用中的函数。

2.2 请求队列

每个请求都由struct davici_request表示,他是一个用链表来实现的队列。每个请求中包含请求的内容与请求相关的回调函数,以及一些辅助字段。

1
2
3
4
5
6
7
8
9
10
struct davici_request {
struct davici_request *next;
unsigned int allocated;
unsigned int used;
unsigned int sent;
char *buf;
int err;
davici_cb cb;
void *user;
};

2.3 davici_write

在构建了req请求队列后,当触发fd写事件时,调用davici_write()将会把所有消息按顺序发送给服务端。每次发送消息时会先发送一个四字节网络字节序的消息头,然后把构建好的请求发给服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int davici_write(struct davici_conn *c)
{
struct davici_request *req;

req = c->reqs;
while (req)
{
len = send(c->s, (char*)&size + req->sent,
sizeof(size) - req->sent, 0);
len = send(c->s, req->buf + req->sent - sizeof(size),
req->used - (req->sent - sizeof(size)), 0);
req = req->next;
}
return 0;
}

2.4 davici_read

服务端会依次处理接收到的消息,并进行回复。客户端fd触发读事件后,每次读取一个来自服务端的回复。

首先读取四字节网络字节序的内容,申请足够的缓存并继续从服务端读取剩余消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int davici_read(struct davici_conn *c)
{
while (!err)
{
len = recv(c->s, c->pkt.len + c->pkt.received,
sizeof(c->pkt.len) - c->pkt.received, 0);

memcpy(&size, c->pkt.len, sizeof(size));
size = ntohl(size);
c->pkt.buf = malloc(size);
len = recv(c->s, c->pkt.buf + c->pkt.received - sizeof(c->pkt.len),
size - (c->pkt.received - sizeof(c->pkt.len)), 0);
handle_message(c);
free(c->pkt.buf);
}
return err;
}

2.5 分析消息类型

读取到来自服务器的消息后,读取第一个字节的内容,这个字节标志着消息的类型。并依据消息类型,进行相关消息处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int handle_message(struct davici_conn *c)
{
struct davici_packet pkt = {
.buf = c->pkt.buf + 1,
.received = c->pkt.received - sizeof(c->pkt.len) - 1,
};

switch (c->pkt.buf[0])
{
case DAVICI_CMD_RESPONSE:
return handle_cmd_response(c, &pkt);
case DAVICI_CMD_UNKNOWN:
return handle_cmd_unknown(c);
case DAVICI_EVENT_UNKNOWN:
return handle_event_unknown(c);
case DAVICI_EVENT_CONFIRM:
return handle_event_confirm(c);
case DAVICI_EVENT:
return handle_event(c, &pkt);
default:
return 0;
}
}

2.6 cmd消息

收到来自服务器的回复后从请求队列中出队一个请求,调用注册的回调函数,最终释放这个请求。

1
2
3
4
5
6
7
8
9
static int handle_cmd_response(struct davici_conn *c, struct davici_packet *pkt)
{
char name[NAME_BUF_LEN];

req = pop_request(c, DAVICI_CMD_REQUEST, name, sizeof(name));
req->cb(c, 0, name, &res, req->user);
destroy_request(req);
return 0;
}

2.7 event消息

进行注册event消息不同于command消息,客户端分析消息,获取到事件名,并查询这条链接上注册的事件,一一进行匹配,并执行相关事件回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
static int handle_event(struct davici_conn *c, struct davici_packet *pkt)
{
err = copy_name(name, sizeof(name), pkt->buf + 1, pkt->buf[0]);
ev = c->events;
while (ev)
{
if (strcmp(name, ev->name) == 0)
ev->cb(c, 0, ev->name, &res, ev->user);

ev = ev->next;
}
return 0;
}

2.8 解析消息

收到VICI消息后可以使用davici_recurse()进行解析,针对消息类型,可以注册三类解析函数,分别是解析段、解析数组、解析键值对。

1
2
3
4
typedef int (*davici_recursecb)(struct davici_response *res, void *user);

int davici_recurse(struct davici_response *res, davici_recursecb section,
davici_recursecb li, davici_recursecb kv, void *user);

三. libzebra集成DAVICI

libzebra的处理框架就是事件驱动型(Reactor模式)应用。

有五个关键参与者:

  • 文件描述符集合:操作系统监控fd的accept、read、write事件,作为事件源。
  • 事件触发器:用来阻塞等待事件发生,核心为select()accept()或者poll()类函数。
  • 事件索引:用来标识不同服务的组件。
  • 事件处理函数:每个服务具体的接口实现。
  • reactor管理器:进行事件调度。

事件触发器阻塞等待来自客户端的连接,一旦文件描述符状态发送变化(收到来自客户端的连接请求),accept()返回cfd,reactor管理器将cfd添加到文件描述符集合,并将这个事件注册到读队列,进行事件调度。事件触发器检测到cfd可读后,将创建创建read thread,当read threadreactor管理器调度执行时,调用read进行读取数据。

读取到来自客户端的消息后,分析消息,在事件索引表中查找消息请求的具体服务,最终通过事件索引表查到事件处理函数,最终调用服务。

Reactor模式