架构
1. 支持哪些特性?
1.1 支持 anyconnect 和 nc 两种VPN协议,从 vpn_proto 中,可以看出整个程序的大致功能。
anyconnect (cisco): 包括 cstp 和 udp
nc (juniper network): 包括 oncp 和 esp
1.2 支持 win 和 unix-like 两种客户端。
1.3 支持 openssl 和 gnutls 两种协议。
目前使用的是GNUTLS
2. 代码架构
如本章1.1所示,VPN支持 anyconnect 和 juniper 两种VPN协议。篇幅所限,下文中只介绍了 anyconnect 协议的实现细节。
整个代码层级可以分为四层:
第一层(总的对外接口): library
第二层(主要过程,即认证和心跳): mainloop auth auth-common
第三层(细分的逻辑模块,包括应用层的通信协议和虚拟网卡管理): tun/tun-windows dtls cstp http http auth
第四层: 其他基础功能模块
library | 主要的对外接口 |
auth | 实现认证功能 |
auth-common | 处理表单时,生成token的其他途径 |
mainloop | 实现心跳功能 |
http | 实现认证时,所涉及的HTTP通信功能 |
http-auth | 实现认证时,处理HTTP--401状态码的几种手段 |
cstp | 认证成功后,建立TCP连接,发送CONNECT报文,建立HTTP隧道 基于TCP的心跳 重连 |
dtls | 认证成功后,建立UDP连接,建立DTLS连接 基于UDP的心跳 重连 |
gnutls | 为TCP协议提供TLS相关接口 |
gnutls-dtls | 为UDP协议提供TLS相关接口 |
gnutls-pkcs12 | 提供证书校验相关接口 |
ssl | 提供TCP、UDP连接接口 提供cmd_fd的监控接口 |
tun/tun-windows | 提供隧道的建立和心跳接口 |
script | 支持隧道建立时的脚本执行 |
lzs | 提供lzs压缩算法的接口 |
名称
|
功能
|
---|
认证
1. 认证概览
认证阶段涉及至少三次(如果密码输入错误,则不只三次)的HTTP请求。参见本章第三节
整个认证过程分为四个阶段:
阶段1: 第一次HTTP请求返回相应的form表单,包括分组选择和账号密码填写。参见3.1
阶段2:程序提示用户选择分组,之后进入第二次HTTP请求,本次请求中新增了分组选择。参见3.2
阶段3: 分组发送后,服务端继续返回表单,客户端提示用户输入账户密码,之后发送给服务端。参见3.3
阶段4:(可选)根据最后一次HTTP通信返回的profile-uri和profile-hash,对服务端的数据进行验证。参见2.3和3.3
2. 认证过程中的关键步骤
2.1 do_http_request
do_http_request 包括 HTTP-request 和 HTTP-response 两个过程。
HTTP-request
报文准备 | |
openconnect_open_https | 负责HTTP连接的建立,在cstp_connect中也会被调用 |
ssl_write | 发送报文 |
名称
|
功能
|
---|
HTTP-response
process_http_response | 处理报文的头部,并按照transfor-encoding读取body内容,通常为chunk。 connection正常为keep-alive,如果为close,则直接不再处理本次通信。 location用于保存重定向url,如果循环重定向或者超过三次重定向,则认证失败。 第三个函数参数 http_auth_hdrs 用于匹配401相关的报文。 |
gen_authoriztion_hdr | 处理401 unauthorized,实现了四种http认证方式 |
handle_redirect | 更新url并进入下一次http请求 |
名称
|
功能
|
---|
2.2 openconnect_open_https
openconnect_open_https,负责HTTP连接的建立,在cstp_connect中也会被调用。
connect_https_socket | 建立TCP连接,如果之前保存过对端的ip则直接连接。 |
verify_peer | 被注册的回调,调用gnutls库实现客户端的证书验证 |
gnutls初始化 | 涉及gnutls会话的初始化,以及绑定到之前建立的tcp套接字 |
cstp_handshake | 一个标准的select非阻塞连接场景 |
名称
|
功能
|
---|
2.3 parse_xml_response
auth | 三次http通信中,前两次都是“main”,最后一次是“success” 用于handle_auth_form中,检查到“success”则不再处理 |
session-token | 即重连中使用的cookie |
config | 主要读取其中的profile-uri和profile-hash,配合obtain_cookie中的fetch_config进行二次校验 |
error | 认证出错,则包含相应的错误信息 |
名称
|
功能
|
---|
2.4 handle_auth_form
该函数会被调用两次:
第一次:处理选择分组,返回newgroup。对应三次HTTP请求的第一次和第二次。参见3.1和3.2
第二次:处理账户密码,返回OK。对应三次HTTP请求的第二次和第三次。(可以重复进入)参见3.2和3.3
注:opeconnect还支持四种token模式,具体没有深究。
3. 认证中涉及的三次HTTP请求报文
报文涉及到敏感数据,有兴趣的话可以自己使用openconnect参数自行打印HTTP报文。
连接
1. 建立CSTP连接
1.1 连接过程
cstp连接负责发送CONNECT报文,从而建立客户端到服务器之间的隧道代理。
请求发送成功后,服务器会返回cstp和dtls两种报文配置,用于之后的本地网络配置(虚拟网卡的ip、dns、netmask等)、通信配置(加密算法、压缩算法、dtls的通信端口、mtu等)、心跳配置(心跳间隔)等。
openconnect_open_https | 参见2.2 |
start_cstp_connection | dtsl-sessionid 用于cstp重连过程中,与重连报文检查类似,当发现数据变化则直接放弃重连 |
名称
|
功能
|
---|
2. 建立DTLS连接
dtls_attempt_period | 根据dtls_setup最后一个参数配置 |
DTLS配置 | 根据CONNECT返回的报文配置 |
udp_sockaddr | 初始化UDP套接字相关属性 |
udp_connect | 创建socket连接 |
start_dtls_handshake |
一系列的DTLS初始化,并绑定套接字 |
dtls_try_handshake | 建立DTLS连接 使用二分法探测MTU |
名称
|
功能
|
---|
心跳
1. 心跳概览
reconnect_timeout和reconnect_interval | 根据mainloop函数参数配置 |
cmd_fd | win下为socket,mac下为pipe 用于消息线程与心跳线程之间通信 |
心跳循环 | 包含三种心跳 可通过cmd_fd终止 也会因为重连失败、捕获异常等终止 |
cstp_bye |
发送服务端断开VPN连接 |
os_shutdown_tun | 清理本地的网卡配置 |
名称
|
功能
|
---|
2. 隧道
2.1 建立隧道
prepare_script_env | 根据CSTP建立连接时返回的报文配置,将本地网络环境加入环境变量 |
socketpair | 本地的UDP读写端口,用于父子进程间的通信 |
子进程 | 应用环境变量并执行vpnc脚本 |
openconnect_setup_tun_fd |
即父进程保存的sockectpair文件描述符 设置为非阻塞,并加入监控 用于tun_mainloop中 |
名称
|
功能
|
---|
2.2 tun_mainloop
隧道建立是win和mac区别点最大的地方。隧道建立成功后,会产生tun_mainloop。
tun_mainloop实现了客户端和网卡设备之间的通信,在TUN建立时会产生相应的网卡文件描述符。
WIN和MAC在文件描述符方面有着显著的区别:
win的tun_fd被赋值为tap网卡的文件描述符;
mac的tun_fd被赋值为socketpair,与子进程vpnc脚本之间互相通信。
3. DTLS心跳
3.1 dtls的状态
DTLS_SECRET和DTLS_NOSECRET目前没有使用。
DTLS_DISABLED:如果被设置为这个状态,则不会有DTLS相关的任何操作。
DTLS_SLEEPING:当接收到上层传递的PAUSED命令(cmd_fd)时,处于这个状态。
DTLS_CONNECTING:start_dtls_handshake后处于这个状态。(详见第二章第二节)
DTLS_CONNECTTED:dtls_try_handshake后处于这个状态。(详见第二章第二节)
3.2 dtls心跳
dtls心跳分为四个步骤:
步骤1: 检查dtls状态,并执行相应操作;
步骤2: 检查是否有输入,如果有,则更新dtls_times.last_rx,检查包的内容并执行相应操作;
AC_PKT_DATA | DTLS接收到的数据包,在tun_mainloop中传递给tun_fd |
AC_PKT_DPD_OUT | 服务器要求客户端发送DPD包request |
AC_PKT_DPD_RESP | 服务器返回的DPD包response |
AC_PKT_KEEPALIVE | 服务器发送的keep-alive包 |
AC_PKT_COMPRESSED | 接收到需要解压的UDP包 |
AC_PKT_DISCONN | 服务端发起断开本次VPN连接 (只有CSTP处理) |
AC_PKT_TERM_SERVER | 同AC_PKT_DISCONN (只有CSTP处理) |
名称
|
功能
|
---|
步骤3: 调用keepalive_action检查是否需要执行探活操作;
KA_DPD | 需要发送DPD包 |
KA_DPD_DEAD | 检测到DPD超时,发起重连 |
KA_KEEPALIVE | 发送keep-alive包 |
KA_REKEY | 重连 |
名称
|
功能
|
---|
步骤4: 转发TUN设备发送的数据至VPN服务器;
3.3 dtls重连
调用connect_dtls_socket。(详见上一章第二节)
4. CSTP心跳
4.1 cstp心跳
CSTP心跳分为四个步骤:
步骤1: 同DTLS步骤1;
步骤2: 检查是否有需要被写入的包,如果有,则尝试写入;如果写入失败,则迅速判断是否需要重连;
步骤3: 同DTLS步骤3;
步骤4: 如果DTLS没有连接,执行DTLS的第四步操作;
4.2 cstp重连
CSTP心跳分为两个步骤:
步骤1: 调用cstp_connect建立连接:(详细过程见上一章1.1节)
步骤2: 调用调用vpnc脚本,传递“reconnect”参数。