有赞移动助手App 本地抓包方案

一、概述

有赞移动助手(下面简称助手App)网关切换功能(有赞移动 App 一键切换网关实践),在我们有赞移动整个开发,测试回归,产品回归验收等扮演着重要的角色,近期我们在 App 集成了本地抓包的功能,更是如虎添翼。在测试抓包,线上问题排查等场景发挥着重要的作用。

二、背景

在目前的抓包的场景中,大部分通过手机连接 PC,进行 IP 代理,用三方抓包工具(Charles,Fiddler,Wireshark等)进行抓包,具体有如下痛点:

  • 抓包需要将移动设备的 Wi-Fi 设置中将代理手动设置成 PC 的 IP,和对应的端口号,过程比较繁琐
  • 移动设备需要安装 Charles 提供的 Root 证书
  • 不能随时随地的用移动设备抓包,必须强依赖于 PC 端

因此我们需要在App增加本地抓包的功能, 通过一种技术手段可以实时监听到通过移动助手网关功能连接到 ZanProxy  服务器的所有网络请求,经过调研,实现这个功能应该具备以下几个条件:

  • 利用 Socket 协议来实现消息推送经过助手 App 网关功能的网络请求到客户端
  • 当我们切换到其他 APP 进行网络请求时,要让助手 App 能在后台保活,甚至是常驻在后台,来达到对网络监听的目的
  • 需要在有赞助手有对应的页面去展示监听到的网络请求,header,response,request等数据

三、有赞移动助手

我们助手App网关功能的原理是 Android 提供的 VpnService ,iOS的NetworkExtension 将 TCP 连接的 IP 数据包通过 tun2socks 转化成 socks5 代理,将数据转发到 ZanProxy 服务器中,实现整个网关功能。

3.1 tun2socks

tun2socks 实现一种机制,它可以让你无需改动任何应用程序而完全同名地机那个数据用 socks 协议封装,转发给一个 socks 代理,然后由代理程序负责于正式服务器之间转发应用数据。使用代理有两种方式,一种是显示配置代理,数据离开你的主机时它的目标地址就是代理服务器。另一种是做透明代理,即在中途把原始数据重定向到一个应用程序,由该代理程序代理转发。tun2socks 在第二种的基础上,完成了 socks 协议的封装,并且实现该机制时使用了强大的tun虚拟网卡而不必再去配置复杂的 iptables 规则,如下图所示

https://img.yzcdn.cn/public_files/2019/05/28/2a928ccd10315d90abcd58816de99293.png

3.2 socks代理

socks 运作原理,就是在TCP数据外包一层 socks 协议头,到达 socks 代理服务器后,脱去 socks 头,然后通过 socks 服务器与真实服务器之间建立的连接将 TCP 数据传给真实服务器,socks 代理并不理解任何应用层协议,它只是负责转发应用层数据而已,这一点使 socks 成为了一个通用的代理协议,这一点和 HTTP 代理服务器是完全不同。

四、技术方案

了解了整个流程以后我们回到最初的痛点,需要利用 Socket 协议来让服务器和App建立长链接,实时监听通过助手App网关的所有 http/https 请求

4.1 Websocket

WebSocket 是HTML5 新增的一种通信协议。WebSocket 协议是一种持久化的双向通信协议,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大的不同有两点:

  • WebSocket是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/UA 都能主动的向对方发送或接收数据,就像 Socket一样,不同的是 WebSocket 是一种建立在Web基础上的一种简单模拟 Socket 的协议。
  • WebSocket需要通过握手连接,类似于TCP,它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。

下面是一个简单的建立握手的时序图:

https://img.yzcdn.cn/public_files/2019/05/28/36f4999be5c52343a261349e7958d4f4.png

4.2 Socket.IO

socket.io 是支持浏览器和服务器之间实时、双向基于事件通信的库,它包括 Node.js 服务端 API 和浏览器的 JavaScript 端的API,是一个完全由 JavaScript 实现、基于 Node.js、支持 WebSocket 协议的用于实时通信、跨平台的开源框架。底层是基于 engine.io,在此基础上增加了Namespace、room、自动重连等特性。

socket.io 设计的目标是支持任何的浏览器,任何 Mobile 设备。支持主流的 PC 浏览器 (IE,Safari,Chrome,Firefox,Opera等),Mobile 浏览器(iphone Safari/ipad Safari/Android WebKit/WebOS WebKit等)。socket.io旨在使实时应用在每个浏览器和移动设备上成为可能,模糊不同的传输机制之间的差异。

但是,WebSocket 协议是 HTML5 新推出的协议,浏览器对它的支持并不完善,由此可以看出,socket.io 不可能仅仅是对 WebSocket 的实现,它还支持其他的通信方式,如上面介绍过的 AJAX 轮询和 Long Polling。根据浏览器的支持程度,自主选择使用哪种方式进行通讯。

4.2.1 Socket.io支持的通信方式:

  • WebSocket
  • Adobe Flash Socket
  • AJAX long-polling
  • AJAX multipart streaming
  • Forever IFrame
  • JSONP polling

4.2.2 可靠性

socket.io即使在下列情况下也能建立联系:

  • 代理和负载平衡器
  • 个人防火墙和杀毒软件

4.2.3自动重连机制

除非有特殊指示,否则断开连接的客户端将尝试重新连接,直到服务器再次可用为止

4.2.4 断开检测

在 engine.io 级别实现心跳极值,允许服务器和客户端都知道对方何时不再响应。通过在服务器和客户端设置计时器,在握手连接期间共享超时值(pinginterval和pingTimeout参数),可以实现该功能。

4.2.5 多路复用

为了在应用程序中创建关注点分离(例如每个模块,或者基于权限),socket.io允许您创建多个名称空间,这些名称空间将作为单独的通信通道,但将共享相同的底层连接。

其他更多的包括,二进制的支持、房间支持等特性,由于想到以后的可扩展性以及客户端的支持程度,以及因此我们选择socket.io作为我们消息推送的技术方案。

4.2.6 源码分析

在建立连接后,每个客户端会被自动加入到一个默认的命名空间。在每个命名空间中,socket 会被默认加入两个名为 None 和 sid 的房间。None的房间用于广播,而 sid 是当前客户端的 session id,用于单播。除默认的房间外,我们可以根据需要将对应 socket 加入自定义房间,roomid 唯一即可。socket.io 基于engine.io,支持 websocket 和 long polling。如果是 long polling,会定时发送 GET, POST请求,当没有数据时,GET 请求在拉取队列消息时会 hang住(超时时间为 pingTimeout ),如果hang住期间服务器一直没有数据产生,则需要等到客户端发送下一个 POST 请求时,此时服务器会往队列中存储 POST 请求中的消息,这样上一个 GET 请求才会返回。如果upgrade到了 Websocket 连接,则探测成功之后会定期ping/pong来保活连接。流程如下图所示:

https://img.yzcdn.cn/public_files/2019/05/29/10fbcaa2603dfb9eb9b8d606397a9c98.png

4.3 Socket.IO应用

在本次需求中移动端 iOS 用的开源库 socket.io-client-swift 是swift语言编写的。Andriod 端用的开源库 socket.io-android-chat 。下面以iOS为例看下如何使用:

 // MARK: Initializers

    /// Type safe way to create a new SocketIOClient. `opts` can be omitted.
    ///
    /// - parameter socketURL: The url of the socket.io server.
    /// - parameter config: The config for this socket.
    public init(socketURL: URL, config: SocketIOClientConfiguration = []) {
        self._config = config
        self.socketURL = socketURL

        super.init()

        setConfigs(_config)
    }

创建初始化 SocketManager 实例,指定 socketUrl ,指定对应的 nameSpace(名称空间)来保证单独的通信通道,将 UUID(设备唯一id)当作 cookie 添加到 header 中,保证能够识别是哪台设备和服务端建立的 socket 通信,调用 connect 方法进行 socket 连接。

socket.on(clientEvent: .connect) { data, ack in  
            print("connect success")
            YZVPNSettingsNetwork.bindDevice(deviceId: device ?? "")
                .subscribe (
                    onNext: {  response in
                        print("websocket, 绑定成功 ")
                },
                    onError: { e in
                        print("websocket, 绑定成功 ")
                }
            )
        }

在连接成功后将设备信息发到我们服务器进行设备的绑定。

socket.on("rows") { (data, ack) in  
            guard let cur = data[0] as? String else { return }
            print(cur)
        }
        socket.on(clientEvent: .disconnect) {data, ack in
            print("socket disconnect")
        }
        socket.on(clientEvent: .error) {data, ack in
            print("socket error")
        }
        socket.on(clientEvent: .pong) {data, ack in
            print("socket pong" + data.description)
        }

接下来就是各种事件的监听,进行相应的处理。我们这边和服务端约定好定义了名叫 rows 的事件,那么在该事件中就可以收到 http/https 请求的所有信息originRequest、requestData、response信息,那么如何将每个请求的信息对应起来呢?我们这里封装了一个名叫 HttpSession 的类如下:

class  HttpSession : HandyJSON {

    var originRequest : String?
    var requestData : String?
    var response : String?
    var id : String?
    required init() {

    }
}

返回的数据里面有 id 来作为每个 http/https 请求的唯一标示,当接收到数据时,用id进行本地映射,将数据塞入对应的 HttpSession 里面,当每个 HttpSession 对象的所有数据都被拿到时,我们再将该条请求信息展示出来。如下图所示:

https://img.yzcdn.cn/public_files/2019/05/27/99ed06c55ce92985d3adc8826e9ff62d.png

以上整个本地抓包功能都已经完结,但是还有一个问题,我们去抓包肯定是打开别的 APP 将助手 App 退出到后台甚至是程序挂起状态,那么如何保证在助手 App 退出到后台模式后还能收到服务端推来的 socket 消息呢?那就需要让程序在后台长时间运行,iOS有以下几种方法:

  • VOIP
  • Background Audio 后台播放音乐
  • Location Services 定位服务
  • Newsstand downloads 后台下载
  • Remote notifications 静默推送
  • 注册一个后台任务

一般而言,音乐应用在后台是避免kill的,如果在后台应用可用时间即将为0时,播放一段音乐,就会使应用变为假前端状态。可以尝试的解决方案如下:应用申请到后台执行任务后,使用 NSTimer 开启一个定时任务,主要负责监控应用剩余的后台可执行时间,当可用的时间少于一个值时,播放一段默声音乐,然后调用 UIApplication对象的 beginBackgroundTaskWithExpirationHandler 方法将之前申请的后台执行任务结束掉,最后再重新申请一个后台执行任务,这样就可以实现后台不限时执行任务了

因此我们这边采用后台音乐和 beginBackgroundTaskWithExpirationHandler 方法保证 APP 能在后台不被 kill 。

五、总结

以上就是整个本地抓包涉及到的所有技术点,总结一下App如何完成本地抓包的流程: 1、当App通过VpnService/NetworkExtension配置 VPN 和定制、扩展核心网络功能
2、通过 tun2Socks,将请求 TCP 数据外包一层 socks 协议头,到达 socks 代理服务器后,脱去 socks 头,然后通过 socks 服务器与真实服务器之间建立的连接将 TCP 数据传给真实服务器,然后进行 ZanProxy 代理访问,完成一次http/https的完整请求
3、在整个请求过程中,我们用 Socket.IO 将服务端与 助手App 建立长连接,将访问的 http/https 数据实时推送给App端
4、助手App监听长连接事件,拿到数据进行处理,展示出来

由于篇幅有限,期间还涉及到一些其他的技术点,有兴趣的同学可以去查阅对应的文献资料,也欢迎大家积极参沟通,写出这篇文章,是抛砖引玉,也是为了其他感兴趣的同学提供一些思路。

参考文献:

https://socket.io/docs/logging-and-debugging/

https://socket.io/

欢迎关注我们的公众号