FunProxy - 使用 Rust 構(gòu)建跨平臺(tái)全鏈路測(cè)試抓包代理工具
一、背景介紹
1.1 什么是全鏈路測(cè)試
全鏈路測(cè)試就是“驗(yàn)證整個(gè)軟件系統(tǒng)在不同組件、服務(wù)和模塊之間協(xié)同工作時(shí)的性能、功能和穩(wěn)定性”。在這里我們舉一個(gè)非常簡(jiǎn)單的例子。
比如用戶在某商城購(gòu)買商品。
我們是先打開商城,接著瀏覽商品,加入購(gòu)物車,然后提交訂單,支付,等待收貨,最后完成。在整個(gè)購(gòu)買流程中,我們其實(shí)并不是一個(gè)功能模塊就完成全部步驟,而是調(diào)用了很多系統(tǒng)模塊。
比如:
用戶行為 | 對(duì)應(yīng)系統(tǒng)模塊 |
瀏覽商品 | 商品系統(tǒng) |
加入購(gòu)物車 | 購(gòu)物系統(tǒng) |
提交訂單 | 訂單系統(tǒng) |
支付 | 支付系統(tǒng) |
收貨 | 物流系統(tǒng) |
全鏈路測(cè)試就是驗(yàn)證一個(gè)流程中所有涉及的系統(tǒng),協(xié)同工作時(shí)的性能、功能和穩(wěn)定性。
1.2 全鏈路測(cè)試的現(xiàn)狀和痛點(diǎn)
通過上面的購(gòu)物流程我們可以梳理出全鏈路測(cè)試的現(xiàn)狀和痛點(diǎn):
1. 系統(tǒng)數(shù)量眾多
全鏈路測(cè)試涉及到多個(gè)系統(tǒng)、服務(wù)和模塊的協(xié)同工作,系統(tǒng)的多樣性增加了測(cè)試的復(fù)雜性。
2. 各系統(tǒng)環(huán)境配置不一致
各個(gè)系統(tǒng)的環(huán)境配置可能完全不一樣,導(dǎo)致各個(gè)系統(tǒng)對(duì)接相當(dāng)麻煩。
就拿我們剛剛所舉出用戶購(gòu)買商品的例子。涉及到商品系統(tǒng)、購(gòu)物車系統(tǒng)、訂單系統(tǒng)、支付系統(tǒng)、和物流系統(tǒng)。因?yàn)闅v史或者技術(shù)方案的原因,他們選擇的環(huán)境管理方式各不相同。
- 其中商品和購(gòu)物車系統(tǒng)可能是由團(tuán)隊(duì) A 負(fù)責(zé),他們團(tuán)隊(duì)通過 hosts 方式來管理環(huán)境。
- 訂單和支付系統(tǒng),由團(tuán)隊(duì)B負(fù)責(zé),通過自定義請(qǐng)求頭的方式,來進(jìn)行環(huán)境配置。
- 物流系統(tǒng)由團(tuán)隊(duì) C 負(fù)責(zé),通過域名的方式來配置環(huán)境。
這只是精簡(jiǎn)版的購(gòu)物流程,其真實(shí)情況可能遠(yuǎn)比我們的例子更加復(fù)雜。這時(shí)候如果想要完整的測(cè)試一條購(gòu)物全流程,環(huán)境配置還是相當(dāng)復(fù)雜的。
當(dāng)前測(cè)試流程
為了能夠測(cè)試所有的流程:
- 要安裝一個(gè)抓包軟件
- 安裝抓包軟件的證書
- 配置各種抓包的規(guī)則
- 配置抓包的系統(tǒng)代理
- 配置hosts
- 配置終端(安裝證書、設(shè)置手機(jī)代理)
這一套流程下來其實(shí)需要配置的非常復(fù)雜,并且不能共享,每個(gè)人在自己的電腦和手機(jī)上都需要重新配置一遍。
二、全鏈路測(cè)試的愿景及目標(biāo)
因?yàn)樯鲜霰尘?,我們?duì)vivo的全鏈路測(cè)試提出以下的愿景和目標(biāo)。
我們的愿景是:讓 vivo 的全鏈路抓包和環(huán)境代理,如絲綢般絲滑。
我們的目標(biāo)是:構(gòu)建一款跨平臺(tái)、高性能、易于擴(kuò)展、安全性高的全鏈路測(cè)試抓包代理工具。
2.1 技術(shù)目標(biāo)詳解
跨平臺(tái)
能夠支持幾乎所有主流平臺(tái),包括但不限于Windows/macOS/Linux/Android/iOS,為什么要兼容這么多平臺(tái),因?yàn)槲覀兊臉I(yè)務(wù)在這些平臺(tái)上面幾乎都有涉及。
高性能
面對(duì)超高并發(fā)量的接口請(qǐng)求,能夠輕松應(yīng)對(duì),確保系統(tǒng)在高負(fù)載下的穩(wěn)定運(yùn)行。
易拓展
提供靈活的架構(gòu)和工具,支持官方和用戶根據(jù)個(gè)人需要對(duì)工具進(jìn)行拓展。
安全性高
通過全面的安全測(cè)試,確保產(chǎn)品在各個(gè)層級(jí)的安全性,包括數(shù)據(jù)傳輸、存儲(chǔ)和訪問控制,防止?jié)撛诘陌踩{和數(shù)據(jù)泄露。
三、技術(shù)選型
3.1 為何選擇 Rust
我們首選了 Rust 語言作為我們的基礎(chǔ)語言,主要原因如下:
- Rust 是系統(tǒng)級(jí)別的通用編程語言,上至web 下至OS 統(tǒng)統(tǒng)能輕松駕馭,并且能夠保證內(nèi)存安全和并發(fā)安全;
- Rust有豐富的工具鏈,比如rustup,cargo等等,開發(fā)體驗(yàn)非常棒;
- Rust有非常龐大和活躍的社區(qū),社區(qū)貢獻(xiàn)了非常多好用的各種庫(kù)。
并且最近使用 Rust 構(gòu)建應(yīng)用的公司越來越多,比如我們公司的 Blue OS ,就是行業(yè)首個(gè)系統(tǒng)框架由 Rust 語言編寫的操作系統(tǒng),這也是我們堅(jiān)定不移的選擇Rust的重要原因之一。
3.2 為何選擇 Tauri
選擇了 Rust ,我們順其自然的選擇了 Rust 多端開發(fā)框架 Tauri。Tauri 是一個(gè)基于 Rust 構(gòu)建的跨平臺(tái)應(yīng)用框架。
主要有以下幾個(gè)優(yōu)勢(shì):
- 獨(dú)立的前端工程:Tauri 支持任何前端框架,所以你不需要改變你的技術(shù)棧;
- 最小化體積:使用操作系統(tǒng)本地的網(wǎng)頁(yè)渲染器,Tauri 應(yīng)用的體積可以達(dá)到最小 600KB;
- 跨平臺(tái):同一套代碼可以編譯運(yùn)行到幾乎所有的OS系統(tǒng)中;
- 高安全性:Tauri 團(tuán)隊(duì)的首要目標(biāo),推動(dòng) Tauri 的首要任務(wù)和最大的創(chuàng)新。
- 進(jìn)程間通訊:用 JavaScript 編寫前端,用 Rust 編寫應(yīng)用程序邏輯,并使用 Swift 和 Kotlin 在系統(tǒng)中深入集成;
- 由 Rust 驅(qū)動(dòng):以性能和安全性為中心,Rust 是下一代應(yīng)用程序的語言。
使用Tauri框架,讓我們不僅能獲得Rust的安全性和高效性,還能享受Web開發(fā)的熟悉感和靈活性。
四、方案介紹
通過剛才的技術(shù)方案,我們打造出了FunProxy 一站式代理工具。FunProxy 是一款用 Rust 開發(fā)的純自研的一站式抓包 & 代理工具,全平臺(tái)支持,天生更安全,天生更流暢?。?!
主打一個(gè)方便、快捷和無邊界。
4.1 優(yōu)勢(shì)及亮點(diǎn)
FunProxy 主要的優(yōu)勢(shì)及亮點(diǎn)如下:
- 全功能抓包能力
- 全平臺(tái)獨(dú)立應(yīng)用支持
- 云端 hosts
- 云端 Rules
- 協(xié)同抓包
- 極致性能
4.1.1 全功能抓包能力
協(xié)議
支持HTTP/1.x和HTTP/2協(xié)議、WebSocket、TLS 1.1-1.3的加密協(xié)議
工具
支持重寫、斷點(diǎn)、數(shù)據(jù)對(duì)比、模擬超時(shí)、全局搜索、篩選、對(duì)比
文件
支持導(dǎo)入導(dǎo)出會(huì)話、支持Fun格式的文件,HAR格式文件、Charles格式文件
4.1.2 全平臺(tái)獨(dú)立應(yīng)用支持
我們支持幾乎所有的主流操作系統(tǒng),例如Windows/macOS/Linux/Android/iOS/還有網(wǎng)頁(yè)版本,并且多操作系統(tǒng)可以相互配合,F(xiàn)unProxy 讓代理打破邊界。
4.1.3 云端 hosts
hosts維護(hù)是一個(gè)大的痛點(diǎn),因?yàn)楦鱾€(gè)系統(tǒng)的hosts會(huì)經(jīng)常變化,導(dǎo)致有些同學(xué)的hosts因?yàn)闆]有及時(shí)同步變更,而出現(xiàn)問題。所以我們提供了云端hosts功能。云端hosts 可以讓項(xiàng)目中的hosts由一人設(shè)置,人人共享。保證hosts能夠?qū)崟r(shí)同步到FunProxy的各個(gè)用戶。
云端hosts支持靜態(tài)和遠(yuǎn)程兩種hosts。
靜態(tài) hosts:系統(tǒng)中的hosts文件格式一樣,一一對(duì)應(yīng)。
遠(yuǎn)程hosts:可以提供一個(gè)鏈接,我們會(huì)遠(yuǎn)程去拉取這個(gè)hosts,在FunProxy運(yùn)行的時(shí)候再去加載這個(gè)Hosts。
順便說一句:FunProxy 所有的hosts都是虛擬hosts,并不會(huì)修改系統(tǒng)hosts,保證系統(tǒng)hosts的干凈
4.1.4 云端規(guī)則
同云端 hosts 一樣,規(guī)則也可以配置到云端讓所有人共享。
所有的規(guī)則都有一些公共的配置。比如規(guī)則名稱、匹配模式和匹配鏈接。
我們的匹配模式支持通配符和正則表達(dá)式。幾乎可以適配所有的鏈接。
同時(shí)我們也提供了檢測(cè)工具,幫助大家檢測(cè)鏈接是否被匹配上。
目前規(guī)則主要有三個(gè)分類:分別是遠(yuǎn)程映射,本地映射,和解密。
遠(yuǎn)程映射
在遠(yuǎn)程映射這個(gè)功能上,我們可以配置遠(yuǎn)程主機(jī)的相關(guān)信息,這樣就可以將需要轉(zhuǎn)發(fā)的鏈接轉(zhuǎn)發(fā)到對(duì)應(yīng)的服務(wù)器上面。
本地映射
在本地映射模式中,支持直接修改本地映射的內(nèi)容,同時(shí)我們提供了強(qiáng)大的代碼編輯器功能,幫助大家在編寫本地映射內(nèi)容的時(shí)候, 能夠有比較好的代碼提示。
解密功能
這個(gè)功能對(duì)于很多加密傳輸網(wǎng)站測(cè)試尤為重要。因?yàn)槊總€(gè)網(wǎng)站的加解密方式可能不一樣,所有我們提供了運(yùn)行時(shí)函數(shù)。用戶可以自己編寫JavaScript或者Python運(yùn)行函數(shù),動(dòng)態(tài)的根據(jù)傳入的參數(shù)進(jìn)行對(duì)應(yīng)的加解密計(jì)算。
動(dòng)態(tài)函數(shù)解析讓 FunProxy 幾乎能解所有的密文,這樣我們?cè)诓榭唇涌诘臅r(shí)候就非常的方便,不用再去解密工具中查看。
同時(shí)也請(qǐng)大家放心,我們的解密工具有著非常高的安全性,保證只有項(xiàng)目管理員授權(quán)的人才能查看,大家也不用擔(dān)心自己的解密方法被別人盜取。
4.1.5 協(xié)同抓包
通過 FunProxy 左下角提供的分享功能,可以讓當(dāng)前抓包的所有數(shù)據(jù)實(shí)時(shí)同步給所有擁有這個(gè)鏈接的人。所有的操作都能同步到所有端,無需下載,讓大家更加方便的進(jìn)行數(shù)據(jù)包的共享。
4.1.6 極致性能
我們對(duì)比了市場(chǎng)上主流的抓包軟件,無論從安裝包大小、啟動(dòng)時(shí)間、安裝空間、使用內(nèi)存來說,F(xiàn)unProxy 都有非常大的優(yōu)勢(shì)。
4.2 技術(shù)架構(gòu)
FunProxy是基于Tauri框架進(jìn)行跨平臺(tái)的開發(fā),我們封裝了fun-core 和fun-mitm兩個(gè)核心庫(kù)。
fun-core
主要是所有端的公共能力,比如配置文件,存儲(chǔ)等。
fun-mitm
主要負(fù)責(zé)所有端的man in the middle 也就是中間人的功能,提供抓包的核心能力。
tauri-plugin-funproxy
因?yàn)橛行┒说墓δ芸赡苄枰{(diào)用系統(tǒng)特定的能力,在移動(dòng)端我們封裝了tauri-plugin-funproxy來調(diào)用移動(dòng)端的特定能力,比如VPN。
其他在各個(gè)桌面端我們封裝了各個(gè)系統(tǒng)的核心crates來調(diào)用桌面端的特定能力。其他能力則是通過Rust直接調(diào)用。
為了提供云端能力,我們使用Go語言搭建了一個(gè)服務(wù)。包括:項(xiàng)目管理、用戶管理、升級(jí)管理、健康檢查、云端Host、云端規(guī)則、云端工具等等功能。服務(wù)只是一個(gè)增值功能,所有的FunProxy都可以離線運(yùn)行。
同時(shí)為了保證各端APP核心能力的正常,使用了playwright搭建了自動(dòng)化測(cè)試能力。重點(diǎn)測(cè)試的功能有:登錄、代理、抓包、升級(jí)、權(quán)限、兼容性、性能等。
最后通過vitepress搭建FunProxy的文檔功能。方便用戶使用和查看。包括:介紹、使用說明、常見問題、更新日志、API、貢獻(xiàn)指南、問題反饋等。
五、核心實(shí)現(xiàn)
5.1 MITM
這是FunProxy的MITM(Man-in-the-Middle,中間人)方案。所謂的中間人,就是在服務(wù)端和客戶端之間加入一個(gè)節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)負(fù)責(zé)接收客戶端發(fā)送的內(nèi)容,通過自己的規(guī)則然后再轉(zhuǎn)發(fā)到服務(wù)端。我們這里用HTTP和HTTPS舉例:
HTTP中間人方案:
客戶端通過發(fā)送HTTP內(nèi)容到FunProxy,FunProxy拿到HTTP內(nèi)容后,在轉(zhuǎn)發(fā)到服務(wù)器中。服務(wù)器處理完成后再將信息發(fā)送給FunProxy,F(xiàn)unProxy在轉(zhuǎn)發(fā)給客戶端。這就完成了一個(gè)請(qǐng)求的中間人接收和轉(zhuǎn)發(fā)方案。
由于HTTP都是明文傳輸,所以比較簡(jiǎn)單。HTTPS就比較復(fù)雜了,涉及到SSL證書的認(rèn)證,具體的大家可以看圖上的流程。
了解完了中間人的過程。下面我們根據(jù)剛才分析的過程,可以設(shè)計(jì)出Fun-MITM的模塊需要包含哪些模塊。
CA模塊:負(fù)責(zé)根證書的生成和加載,以及各個(gè)網(wǎng)站證書的認(rèn)證
Client:這個(gè)Client就是模擬真實(shí)用戶發(fā)送請(qǐng)求的那個(gè)客戶端
Server:這個(gè)Server就是攔截用戶請(qǐng)求的那個(gè)服務(wù)端
http_handler:更改請(qǐng)求和響應(yīng)的內(nèi)容
socket_handker:處理socket請(qǐng)求
let mitm = Proxy::builder()
.with_addr(addr)
.with_client(client)
.with_ca(ca)
.with_http_handler(custom_handler.clone())
.with_websocket_handler(custom_handler.clone())
.with_graceful_shutdown(async {
close_rx.await.unwrap_or_default();
})
.build();
pub struct WantsHandlers<C, CA, H, W, F> {
al: AddrOrListener,
client: Client<C, Body>,
ca: CA,
http_handler: H,
websocket_handler: W,
websocket_connector: Option<Connector>,
server: Option<Builder<TokioExecutor>>,
graceful_shutdown: F,
}
impl<C, CA, H, W, F> ProxyBuilder<WantsHandlers<C, CA, H, W, F>> {
/// Set the HTTP handler.
pub fn with_http_handler<H2: HttpHandler>(
self,
http_handler: H2,
) -> ProxyBuilder<WantsHandlers<C, CA, H2, W, F>> {
}
/// Set the WebSocket handler.
pub fn with_websocket_handler<W2: WebSocketHandler>(
self,
websocket_handler: W2,
) -> ProxyBuilder<WantsHandlers<C, CA, H, W2, F>> {
}
/// Set the connector to use when connecting to WebSocket servers.
pub fn with_websocket_connector(self, connector: Connector) -> Self {
}
/// Set a custom server builder to use for the proxy server.
pub fn with_server(self, server: Builder<TokioExecutor>) -> Self {
}
/// Set a future that when ready will gracefully shutdown the proxy server.
pub fn with_graceful_shutdown<F2: Future<Output = ()> + Send + 'static>(
self,
graceful_shutdown: F2,
) -> ProxyBuilder<WantsHandlers<C, CA, H, W, F2>> {
}
/// Build the proxy.
pub fn build(self) -> Proxy<C, CA, H, W, F> {
}
}
5.2 虛擬 hosts
在中間人方案中如何實(shí)現(xiàn)虛擬Hosts能力?我們?cè)诙xclient的時(shí)候,可以定義一個(gè)resolver。這個(gè)resolver在每次hosts解析的時(shí)候都會(huì)調(diào)用。
首先他會(huì)去云端獲取當(dāng)前hosts的配置的IP是哪個(gè),如果有的話,直接返回。如果沒有配置,那么調(diào)用系統(tǒng)的loopup_ip函數(shù)來直接解析hosts。最后將解析好的hosts返回。
在定義HttpConnector的時(shí)候,使用new_with_resolver加載這個(gè)dns-resolver。這就完成了一個(gè)簡(jiǎn)單的自定義dns-resolver。
let resolver = tower::service_fn(move |name: Name| async move {
let ip_option = host::get_ip(name.as_str());
match ip_option {
Some(ip) => {
let ip: IpAddr = ip.parse().unwrap();
host::add(name.as_str(), ip.to_string().as_str());
Ok::<_, Infallible>(iter::once(SocketAddr::from((ip, 80))))
}
None => {
let sys_resolver = Resolver::from_system_conf().unwrap();
let future = spawn_blocking(move || {
let lookup_ip = sys_resolver.lookup_ip(name.as_str()).unwrap();
if let Some(ip) = lookup_ip.iter().next() {
host::add(name.as_str(), ip.to_string().as_str());
Ok(SocketAddr::from((ip, 80)))
} else {
Err("No IP address found")
}
});
let addrs = future.await.unwrap().unwrap();
Ok::<_, Infallible>(iter::once(addrs))
}
}
});
let mut http_connector = HttpConnector::new_with_resolver(resolver);
5.3 自動(dòng)安裝證書
在使用FunProxy中可以自動(dòng)安裝并設(shè)置證書為信任。具體的是在macOS中調(diào)用security add-trusted-cert。在Windows中主要調(diào)用certutil方法具體的命令行和rust代碼如圖片所示:
5.4 流量攔截
接下來給大家分享一下我們是怎么攔截流量的。首先是PC系統(tǒng),比如Windows/macOS/Linux這些系統(tǒng)上,APP授權(quán)后可以獲取很高的權(quán)限,所以我們直接調(diào)用系統(tǒng)提供的接口能力。
比如在macOS上面就是通過networksetup這個(gè)命令行工具實(shí)現(xiàn)對(duì)系統(tǒng)代理的設(shè)置,在Windows上面可以通過ProxyServer設(shè)置。
而在移動(dòng)端APP上面,一般APP的權(quán)限特別低,APP沒有直接設(shè)置系統(tǒng)代理的能力,所以我們采用的是通過VPN的方式來實(shí)現(xiàn)流量的攔截與轉(zhuǎn)發(fā)。
我們通過VpnService來創(chuàng)建VPN服務(wù)。然后調(diào)用VpnService的各種能力。
這里我們用了一個(gè)安卓Vpn的setHttpProxy方法,就可以將所有的流量主動(dòng)打到我們的fun-mitm 服務(wù)器上面。fun-mitm 在通過自己的規(guī)則對(duì)所有的流量進(jìn)行處理,包括DNS解析或者請(qǐng)求響應(yīng)的修改等等。
5.5 調(diào)用系統(tǒng)能力
我們以macOS應(yīng)用沉浸式頭部為例給大家做個(gè)分享。在macOS應(yīng)用設(shè)計(jì)中,其實(shí)更加通用性的設(shè)計(jì)是沉浸式的頭部,而Tauri生成的應(yīng)用,默認(rèn)都會(huì)有一個(gè)頭部,如屏幕上黃色所示,這個(gè)頭部其實(shí)沒多少作用。接下來我將用這個(gè)例子,給大家分享一下在 Rust 中如何通過調(diào)用系統(tǒng)的原生的能力,來去掉這個(gè)頭部。
- 我們通過 cargo new macos --lib 創(chuàng)建一個(gè)lib庫(kù)。在lib庫(kù)中新建一個(gè)window.swift文件,主要使用window.titlebarAppearsTransparent方法來設(shè)置透明度。
- 接著在lib中通過swift_rs中的swift宏來調(diào)用這個(gè)函數(shù),并將這個(gè)函數(shù)設(shè)置為pub給其他方法調(diào)用。
- 給這個(gè) lib 添加 build.rs 來編譯這個(gè)庫(kù)??梢灾付╩acOS的版本等其他內(nèi)容。最后我們?cè)谧约旱捻?xiàng)目中就可以直接引入這個(gè)lib庫(kù)。直接調(diào)用里面的set_titlebar_style方法。從而通過swift_rs調(diào)用macOS中的原生能力。
5.6 協(xié)同抓包
協(xié)同抓包能夠讓一端的抓包數(shù)據(jù),通過鏈接分享的方式,實(shí)時(shí)同步給其他所有擁有這個(gè)鏈接的人。
主要核心是我們的fun-core這個(gè)庫(kù)。通過fun-core這個(gè)庫(kù)啟動(dòng)的時(shí)候我們會(huì)啟動(dòng)兩個(gè)服務(wù),一個(gè)是api,負(fù)責(zé)接口服務(wù)。
另一個(gè)就是會(huì)啟動(dòng)websocket服務(wù)。用戶打開分享過來的鏈接,會(huì)自動(dòng)通瀏覽器的websocket方法來直接對(duì)fun-core的websocket進(jìn)行通訊。當(dāng)有新的抓包數(shù)據(jù)產(chǎn)生的時(shí)候,fun-core會(huì)通過websocket推送給所有當(dāng)前連接的用戶。從而達(dá)到一端抓包,多端同步的功能。
六、規(guī)劃總結(jié)
6.1 規(guī)劃
接下來我們重點(diǎn)投入的是workflow這個(gè)功能,支持對(duì)請(qǐng)求響應(yīng)的全生命周期管理。用戶可以直觀的選擇想要的工具,并構(gòu)建工作流對(duì)請(qǐng)求響應(yīng)進(jìn)行處理。工作流不僅支持請(qǐng)求響應(yīng)的處理,還支持邏輯的判斷,比如將符合某種條件下的請(qǐng)求高亮顯示出來。等等,可玩性更高。
6.2 總結(jié)
通過上面我們對(duì)FunProxy的介紹,相信大家對(duì)FunProxy有了比較不錯(cuò)的了解。FunProxy使用 Rust 構(gòu)建的跨平臺(tái)全鏈路測(cè)試&抓包代理工具。通過FunProxy,我們只需要簡(jiǎn)單的通過選擇,比如選擇項(xiàng)目,選擇環(huán)境,選擇規(guī)則,就能將全鏈路測(cè)試過程中復(fù)雜的配置流程,變得異常的簡(jiǎn)單。同時(shí)通過云端功能讓所有人能夠共享配置,極大的提升全鏈路測(cè)試的環(huán)境配置和抓包效率,讓全鏈路測(cè)試變得更加絲滑流暢。