作者:vivo 互联网大前端团队- Song Jiachao
在软件开发过程中,软件测试对于保障软件质量和用户满意度起着关键作用。为最大程度上提升软件品质,我们积极开展全链路测试实践,打造了用Rust语言开发的自研一站式抓包代理工具FunProxy,基于其跨平台、高性能、易于扩展、安全性高等特性,让全链路抓包和环境代理如丝绸般丝滑。
一、背景介绍
1.1 什么是全链路测试
全链路测试就是"验证整个软件系统在不同组件、服务和模块之间协同工作时的性能、功能和稳定性"。在这里我们举一个非常简单的例子。
比如用户在某商城购买商品。

我们是先打开商城,接着浏览商品,加入购物车,然后提交订单,支付,等待收货,最后完成。在整个购买流程中,我们其实并不是一个功能模块就完成全部步骤,而是调用了很多系统模块。
比如:
全链路测试就是验证一个流程中所有涉及的系统,协同工作时的性能、功能和稳定性。
1.2 全链路测试的现状和痛点
通过上面的购物流程我们可以梳理出全链路测试的现状和痛点:
1. 系统数量众多
全链路测试涉及到多个系统、服务和模块的协同工作,系统的多样性增加了测试的复杂性。
2. 各系统环境配置不一致
各个系统的环境配置可能完全不一样,导致各个系统对接相当麻烦。

就拿我们刚刚所举出用户购买商品的例子。涉及到商品系统、购物车系统、订单系统、支付系统、和物流系统。因为历史或者技术方案的原因,他们选择的环境管理方式各不相同。
-
其中商品和购物车系统可能是由团队 A 负责,他们团队通过 hosts 方式来管理环境。
-
订单和支付系统,由团队B负责,通过自定义请求头的方式,来进行环境配置。
-
物流系统由团队 C 负责,通过域名的方式来配置环境。
这只是精简版的购物流程,其真实情况可能远比我们的例子更加复杂。这时候如果想要完整的测试一条购物全流程,环境配置还是相当复杂的。
当前测试流程
为了能够测试所有的流程:
-
要安装一个抓包软件
-
安装抓包软件的证书
-
配置各种抓包的规则
-
配置抓包的系统代理
-
配置hosts
-
配置终端(安装证书、设置手机代理)
这一套流程下来其实需要配置的非常复杂,并且不能共享,每个人在自己的电脑和手机上都需要重新配置一遍。

二、全链路测试的愿景及目标
因为上述背景,我们对vivo的全链路测试提出以下的愿景和目标。
我们的愿景是:让 vivo 的全链路抓包和环境代理,如丝绸般丝滑。
我们的目标是:构建一款跨平台、高性能、易于扩展、安全性高的全链路测试抓包代理工具。

2.1 技术目标详解
跨平台
能够支持几乎所有主流平台,包括但不限于Windows/macOS/Linux/Android/iOS,为什么要兼容这么多平台,因为我们的业务在这些平台上面几乎都有涉及。
高性能
面对超高并发量的接口请求,能够轻松应对,确保系统在高负载下的稳定运行。
易拓展
提供灵活的架构和工具,支持官方和用户根据个人需要对工具进行拓展。
安全性高
通过全面的安全测试,确保产品在各个层级的安全性,包括数据传输、存储和访问控制,防止潜在的安全威胁和数据泄露。

三、技术选型
3.1 为何选择 Rust
我们首选了 Rust 语言作为我们的基础语言,主要原因如下:
-
Rust 是系统级别的通用编程语言,上至web 下至OS 统统能轻松驾驭,并且能够保证内存安全和并发安全;
-
Rust有丰富的工具链,比如rustup,cargo等等,开发体验非常棒;
-
Rust有非常庞大和活跃的社区,社区贡献了非常多好用的各种库。
并且最近使用 Rust 构建应用的公司越来越多,比如我们公司的 Blue OS ,就是行业首个系统框架由 Rust 语言编写的操作系统,这也是我们坚定不移的选择Rust的重要原因之一。

3.2 为何选择 Tauri
选择了 Rust ,我们顺其自然的选择了 Rust 多端开发框架 Tauri。Tauri 是一个基于 Rust 构建的跨平台应用框架。
主要有以下几个优势:
-
独立的前端工程:Tauri 支持任何前端框架,所以你不需要改变你的技术栈;
-
**最小化体积:**使用操作系统本地的网页渲染器,Tauri 应用的体积可以达到最小 600KB;
-
**跨平台:**同一套代码可以编译运行到几乎所有的OS系统中;
-
**高安全性:**Tauri 团队的首要目标,推动 Tauri 的首要任务和最大的创新。
-
**进程间通讯:**用 JavaScript 编写前端,用 Rust 编写应用程序逻辑,并使用 Swift 和 Kotlin 在系统中深入集成;
-
**由 Rust 驱动:**以性能和安全性为中心,Rust 是下一代应用程序的语言。
使用Tauri框架,让我们不仅能获得Rust的安全性 和高效性,还能享受Web开发的熟悉感和灵活性。

四、方案介绍
通过刚才的技术方案,我们打造出了FunProxy 一站式代理工具。FunProxy 是一款用 Rust 开发的纯自研的一站式抓包 & 代理工具,全平台支持,天生更安全,天生更流畅!!!
主打一个方便、快捷和无边界。

4.1 优势及亮点
FunProxy 主要的优势及亮点如下:
-
全功能抓包能力
-
全平台独立应用支持
-
云端 hosts
-
云端 Rules
-
协同抓包
-
极致性能

4.1.1 全功能抓包能力
协议
支持HTTP/1.x和HTTP/2协议、WebSocket、TLS 1.1-1.3的加密协议
工具
支持重写、断点、数据对比、模拟超时、全局搜索、筛选、对比
文件
支持导入导出会话、支持Fun格式的文件,HAR格式文件、Charles格式文件

4.1.2 全平台独立应用支持
我们支持几乎所有的主流操作系统,例如Windows/macOS/Linux/Android/iOS/还有网页版本,并且多操作系统可以相互配合,FunProxy 让代理打破边界。

4.1.3 云端 hosts
hosts维护是一个大的痛点,因为各个系统的hosts会经常变化,导致有些同学的hosts因为没有及时同步变更,而出现问题。所以我们提供了云端hosts功能。云端hosts 可以让项目中的hosts由一人设置,人人共享。保证hosts能够实时同步到FunProxy的各个用户。
云端hosts支持静态 和远程两种hosts。
**静态 hosts:**系统中的hosts文件格式一样,一一对应。
**远程hosts:**可以提供一个链接,我们会远程去拉取这个hosts,在FunProxy运行的时候再去加载这个Hosts。
顺便说一句:FunProxy 所有的hosts都是虚拟hosts,并不会修改系统hosts,保证系统hosts的干净

4.1.4 云端规则
同云端 hosts 一样,规则也可以配置到云端让所有人共享。

所有的规则都有一些公共的配置。比如++规则名称++ 、++匹配模式++ 和++匹配链接++。
我们的匹配模式支持++通配符++ 和++正则表达式++。几乎可以适配所有的链接。
同时我们也提供了检测工具,帮助大家检测链接是否被匹配上。

目前规则主要有三个分类:分别是远程映射,本地映射,和解密。
远程映射在远程映射这个功能上,我们可以配置远程主机的相关信息,这样就可以将需要转发的链接转发到对应的服务器上面。
本地映射在本地映射模式中,支持直接修改本地映射的内容,同时我们提供了强大的代码编辑器功能,帮助大家在编写本地映射内容的时候, 能够有比较好的代码提示。
解密功能这个功能对于很多加密传输网站测试尤为重要。因为每个网站的加解密方式可能不一样,所有我们提供了运行时函数。用户可以自己编写JavaScript或者Python运行函数,动态的根据传入的参数进行对应的加解密计算。
动态函数解析让 FunProxy 几乎能解所有的密文,这样我们在查看接口的时候就非常的方便,不用再去解密工具中查看。
同时也请大家放心,我们的解密工具有着非常高的安全性,保证只有项目管理员授权的人才能查看,大家也不用担心自己的解密方法被别人盗取。

4.1.5 协同抓包
通过 FunProxy 左下角提供的分享功能,可以让当前抓包的所有数据实时同步给所有拥有这个链接的人。所有的操作都能同步到所有端,无需下载,让大家更加方便的进行数据包的共享。

4.1.6 极致性能
我们对比了市场上主流的抓包软件,无论从安装包大小、启动时间、安装空间、使用内存来说,FunProxy 都有非常大的优势。

4.2 技术架构
FunProxy是基于Tauri框架进行跨平台的开发,我们封装了fun-core 和fun-mitm两个核心库。
fun-core主要是所有端的公共能力,比如配置文件,存储等。
fun-mitm主要负责所有端的man in the middle 也就是中间人的功能,提供抓包的核心能力。
tauri-plugin-funproxy因为有些端的功能可能需要调用系统特定的能力,在移动端我们封装了tauri-plugin-funproxy来调用移动端的特定能力,比如VPN。
其他在各个桌面端我们封装了各个系统的核心crates来调用桌面端的特定能力。其他能力则是通过Rust直接调用。
为了提供云端能力,我们使用Go语言搭建了一个服务。包括:项目管理、用户管理、升级管理、健康检查、云端Host、云端规则、云端工具等等功能。服务只是一个增值功能,所有的FunProxy都可以离线运行。
同时为了保证各端APP核心能力的正常,使用了playwright搭建了自动化测试能力。重点测试的功能有:登录、代理、抓包、升级、权限、兼容性、性能等。
最后通过vitepress搭建FunProxy的文档功能。方便用户使用和查看。包括:介绍、使用说明、常见问题、更新日志、API、贡献指南、问题反馈等。

五、核心实现
5.1 MITM
这是FunProxy的MITM(Man-in-the-Middle,中间人)方案。所谓的中间人,就是在服务端和客户端之间加入一个节点,这个节点负责接收客户端发送的内容,通过自己的规则然后再转发到服务端。我们这里用HTTP和HTTPS举例:

HTTP中间人方案:
客户端通过发送HTTP内容到FunProxy,FunProxy拿到HTTP内容后,在转发到服务器中。服务器处理完成后再将信息发送给FunProxy,FunProxy在转发给客户端。这就完成了一个请求的中间人接收和转发方案。
由于HTTP都是明文传输,所以比较简单。HTTPS就比较复杂了,涉及到SSL证书的认证,具体的大家可以看图上的流程。
了解完了中间人的过程。下面我们根据刚才分析的过程,可以设计出Fun-MITM的模块需要包含哪些模块。
**CA模块:**负责根证书的生成和加载,以及各个网站证书的认证
**Client:**这个Client就是模拟真实用户发送请求的那个客户端
**Server:**这个Server就是拦截用户请求的那个服务端
**http_handler:**更改请求和响应的内容
**socket_handker:**处理socket请求

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
在中间人方案中如何实现虚拟Hosts能力?我们在定义client的时候,可以定义一个resolver。这个resolver在每次hosts解析的时候都会调用。
首先他会去云端获取当前hosts的配置的IP是哪个,如果有的话,直接返回。如果没有配置,那么调用系统的loopup_ip函数来直接解析hosts。最后将解析好的hosts返回。
在定义HttpConnector的时候,使用new_with_resolver加载这个dns-resolver。这就完成了一个简单的自定义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 自动安装证书
在使用FunProxy中可以自动安装并设置证书为信任。具体的是在macOS中调用security add-trusted-cert。在Windows中主要调用certutil方法具体的命令行和rust代码如图片所示:

5.4 流量拦截
接下来给大家分享一下我们是怎么拦截流量的。首先是PC系统,比如Windows/macOS/Linux这些系统上,APP授权后可以获取很高的权限,所以我们直接调用系统提供的接口能力。

比如在macOS上面就是通过networksetup这个命令行工具实现对系统代理的设置,在Windows上面可以通过ProxyServer设置。


而在移动端APP上面,一般APP的权限特别低,APP没有直接设置系统代理的能力,所以我们采用的是通过VPN的方式来实现流量的拦截与转发。
我们通过VpnService来创建VPN服务。然后调用VpnService的各种能力。
这里我们用了一个安卓Vpn的setHttpProxy方法,就可以将所有的流量主动打到我们的fun-mitm 服务器上面。fun-mitm 在通过自己的规则对所有的流量进行处理,包括DNS解析或者请求响应的修改等等。

5.5 调用系统能力
我们以macOS应用沉浸式头部为例给大家做个分享。在macOS应用设计中,其实更加通用性的设计是沉浸式的头部,而Tauri生成的应用,默认都会有一个头部,如屏幕上黄色所示,这个头部其实没多少作用。接下来我将用这个例子,给大家分享一下在 Rust 中如何通过调用系统的原生的能力,来去掉这个头部。

-
我们通过 cargo new macos --lib 创建一个lib库。在lib库中新建一个window.swift文件,主要使用window.titlebarAppearsTransparent方法来设置透明度。
-
接着在lib中通过swift_rs中的swift宏来调用这个函数,并将这个函数设置为pub给其他方法调用。
-
给这个 lib 添加 build.rs 来编译这个库。可以指定macOS的版本等其他内容。最后我们在自己的项目中就可以直接引入这个lib库。直接调用里面的set_titlebar_style方法。从而通过swift_rs调用macOS中的原生能力。

5.6 协同抓包
协同抓包能够让一端的抓包数据,通过链接分享的方式,实时同步给其他所有拥有这个链接的人。
主要核心是我们的fun-core这个库。通过fun-core这个库启动的时候我们会启动两个服务,一个是api,负责接口服务。
另一个就是会启动websocket服务。用户打开分享过来的链接,会自动通浏览器的websocket方法来直接对fun-core的websocket进行通讯。当有新的抓包数据产生的时候,fun-core会通过websocket推送给所有当前连接的用户。从而达到一端抓包,多端同步的功能。

六、规划总结
6.1 规划
接下来我们重点投入的是workflow这个功能,支持对请求响应的全生命周期管理。用户可以直观的选择想要的工具,并构建工作流对请求响应进行处理。工作流不仅支持请求响应的处理,还支持逻辑的判断,比如将符合某种条件下的请求高亮显示出来。等等,可玩性更高。

6.2 总结
通过上面我们对FunProxy的介绍,相信大家对FunProxy有了比较不错的了解。FunProxy使用 Rust 构建的跨平台全链路测试&抓包代理工具。通过FunProxy,我们只需要简单的通过选择,比如选择项目,选择环境,选择规则,就能将全链路测试过程中复杂的配置流程,变得异常的简单。同时通过云端功能让所有人能够共享配置,极大的提升全链路测试的环境配置和抓包效率,让全链路测试变得更加丝滑流畅。