Versatile IKE Control Interface
一. 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 | key1 = value1 |
上面的示例表示一个有效的树结构,它被编码为以下C数组
:
1 | char msg[] = { |
二. DAVICI
davici
是VICI
客户端协议的替代实现,旨在更好地集成其他软件堆栈。它使用异步、非阻塞
API,并且可以集成到第三方主调度循环中,而无需使用线程。
davici需要应用框架提供监视文件描述符的状态即可(select()
,poll()
)。
2.1 command
davici支持命令调用。davici_new_cmd()
创建一个命名的空消息请求。并且davici提供了一些接口用于遵循vici协议地进行填充消息。
一旦请求构建完成,就可以使用davici_queue()
将这个请求加入发送队列。一旦收到服务器的响应,则异步调用用户注册在此次调用中的函数。
2.2 请求队列
每个请求都由struct davici_request
表示,他是一个用链表来实现的队列。每个请求中包含请求的内容与请求相关的回调函数,以及一些辅助字段。
1 | struct davici_request { |
2.3 davici_write
在构建了req请求队列后,当触发fd写事件时,调用davici_write()
将会把所有消息按顺序发送给服务端。每次发送消息时会先发送一个四字节网络字节序的消息头,然后把构建好的请求发给服务器。
1 | int davici_write(struct davici_conn *c) |
2.4 davici_read
服务端会依次处理接收到的消息,并进行回复。客户端fd触发读事件后,每次读取一个来自服务端的回复。
首先读取四字节网络字节序的内容,申请足够的缓存并继续从服务端读取剩余消息。
1 | int davici_read(struct davici_conn *c) |
2.5 分析消息类型
读取到来自服务器的消息后,读取第一个字节的内容,这个字节标志着消息的类型。并依据消息类型,进行相关消息处理。
1 | static int handle_message(struct davici_conn *c) |
2.6 cmd消息
收到来自服务器的回复后从请求队列中出队一个请求,调用注册的回调函数,最终释放这个请求。
1 | static int handle_cmd_response(struct davici_conn *c, struct davici_packet *pkt) |
2.7 event消息
进行注册event消息不同于command消息,客户端分析消息,获取到事件名,并查询这条链接上注册的事件,一一进行匹配,并执行相关事件回调函数。
1 | static int handle_event(struct davici_conn *c, struct davici_packet *pkt) |
2.8 解析消息
收到VICI消息后可以使用davici_recurse()
进行解析,针对消息类型,可以注册三类解析函数,分别是解析段、解析数组、解析键值对。
1 | typedef int (*davici_recursecb)(struct davici_response *res, void *user); |
三. libzebra集成DAVICI
libzebra的处理框架就是事件驱动型(Reactor模式)应用。
有五个关键参与者:
- 文件描述符集合:操作系统监控fd的accept、read、write事件,作为事件源。
- 事件触发器:用来阻塞等待事件发生,核心为
select()
、accept()
或者poll()
类函数。 - 事件索引:用来标识不同服务的组件。
- 事件处理函数:每个服务具体的接口实现。
- reactor管理器:进行事件调度。
事件触发器
阻塞等待来自客户端的连接,一旦文件描述符状态发送变化(收到来自客户端的连接请求),accept()
返回cfd,reactor管理器
将cfd添加到文件描述符集合
,并将这个事件注册到读队列,进行事件调度。事件触发器
检测到cfd可读后,将创建创建read thread
,当read thread
被reactor管理器
调度执行时,调用read
进行读取数据。
读取到来自客户端的消息后,分析消息,在事件索引
表中查找消息请求的具体服务,最终通过事件索引表
查到事件处理函数
,最终调用服务。