架構
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報文,從而建立客戶端到服務器之間的隧道代理。
PS:為何需要CONNECT報文?
因為SSL隧道通信過程中,報文全部為加密的(包括請求頭和狀態行),代理無法解析響應內容,所以這個時候代理只能透明轉發。
請求發送成功后,服務器會返回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”參數。
refs:
anyconnect vpn 協議分析 https://nikmav.blogspot.com/2013/11/inside-ssl-vpn-protocol.html