PF_KEY协议是IPSec的重要组成部分。密钥管理进程利用PF_KEY与内核的SADB进行通信,实现SA(Security Association,安全联盟)和SP(Security Policy,安全策略)的管理。本文将从PF_KEY协议构造和PF_KEY相关系统调用等方面描述OpenBSD内核中的PF_KEY实现。
利用IPSec技术可在IP层实现对数据包进行保护,降低了互联网通信的风险。IPsec的安全服务是由通信双方建立的安全联盟(SA)来提供的。IPsec系统在处理输入/输出IP 流时必须参考安全策略库(SPD),并根据从SPD中提取的策略对IP流进行不同的处理,例如拒绝、绕过和进行IPsec保护。SA和SPD的管理是利用PF_key API来实现用户进程和内核之间的通信,可以通过手工进行,也可以通过IKE来进行动态协商。
PF_KEY是用户进程操作内核中的SADB(Security Associations Database)和SPD的编程接口。下面将从协议族构造、系统调用和PF_KEY socket实现三部分加以描述。
协议族构造
Net/3组把协议关联到一个域中,并且用一个协议族常量来标识每个域。在OpenBSD定义的域包含以下的四个,分别是路由协议族、IP协议族、Unix协议族和PF_KEY协议族。同时定义了全局指针型变量domains,由它将协议族结构链接在一起。
初始化完成后,内核构建了图1所示的数据结构。pfkey_domain定义的协议族为PF_KEY,Socket地址为PF_KEY。
在PF_KEY协议初始化过程中,系统还定义了指针数组,用于指向含有Socket操作函数的不同版本pfkey_version结构。pfkey_version结构定义在sys/net/pfkeyv2.h文件中。
图1 初始化后的domain链表和protosw数组
目前OpenBSD实现的PF_KEY版本是2.0,该指针数组结构定义如下:
struct pfkey_version
{
int protocol;
int (*create)(struct socket socket);
int (*release)(struct socket *socket);
int (*send)(struct socket *socket, void *message, int len);
} = { PFKEYV2_PROTOCOL,
&pfkeyv2_create, &pfkeyv2_release, &pfkeyv2_send };
PF_KEY socket实现
PF_KEY socket系统调用
当一个应用调用Socket时,进程用系统调用机制将三个独立的整数传给内核。syscall将参数复制到32bit值的数组中,并将数组指针作为第二个参数传给Socket的内核版。内核版的Socket将第二个参数“uap”作为指向sys_socket_args结构的指针。图2显示了上述过程。
图2 用户空间Socket参数到内核空间转换图
当进程调用socket(PF_KEY,SOCK_RAW, PF_KEY_V2)时,内核sys_socket()向PF_KEY协议中pfkey_protosw(见图1)定义的pfkey_usrreq()发送PRU_ATTACH,调用如下:
error=(*prp-pr_usrreq)(so,PRU_ATTACH,NULL,(struct mbuf *)(long)proto,NULL);
Pfkey_usrreq()处理PRU_ATTACH请求,创建协议控制块。
PF_KEY close()系统调用
内核中sys_close()对应用户空间的close()系统调用,用来关闭各类描述符。当fd是引用对象的最后描述符时,与对象有关的close函数被调用如下:
error=(*fp-f_ops-fo_close)(fp, p);
Socket的fp-f_ops-fo_close是soo_close()函数。soo_close()是soclose()函数的封装器。soclose()取消Socket上相关连接并释放不需要的数据结构,发送PRU_DETACH请求断开Socket与PF_KEY协议的联系,最后调用sofree()释放PF_KEY socket。
error2=(*so-so_proto-pr_usrreq)(so, PRU_DETACH, NULL,NULL, NULL);
Pfkey_usrreq()响应soclose()的PRU_DETACH请求,释放该Socket协议控制块,并将该Socket从pfkeyv2_sockets链表中删除。
PF_KEY 读写系统调用
所有的写系统调用都要直接或间接地调用sosend。sosend的功能是将进程来的数据复制到内核,并将数据传递给与插口相关的协议,如下:
error=(*so-so_proto-pr_usrreq)(so, PRU_SEND, top, addr, control);
对于PF_KEY的Socket,pfkey_usrreq()间接调用pfkey_output(),pfkey_output()直接调用pfkeyv2_send()。
所有处理用户消息的工作全部在pfkeyv2_send()内完成,包括SA的增加、删除、清空和请求操作等。pfkeyv2_send()通过pfkeyv2_sendup()将结果放入Socket的接收缓冲区中,并通知用户进程接收缓存已改变。
同写调用一样,用户空间的读调用如read和recv等,都是由内核中的一个公共函数soreceive()来完成所有的工作。sorecevice()函数将数据从Socket的接口缓存传送到进程指定的缓存中。
sorecevice是一个复杂的函数,导致其复杂的主要原因是繁琐的指针操作及对多种类型的数据(带外数据、地址、控制信息和正常数据)和多目标(进程缓存和mbuf链)的处理。
最后在这里列出PF_KEY的接口实现的相关系统调用图,见图3。
图3 PF_KEY内核调用图
结束语
本文主要描述了PF_KEY接口在OpenBSD内核中的实现。通过该接口,可以实现IPSec的SA和SP的自动和手工配置管理。作为IPSec的重要组成部分,SADB和SPD与PF_KEY有着紧密的联系,但本文中没有具体描述。想要深入了解的IPSec的整体实现,阅读源码是比较有效的方法之一。