Ngrok 内网穿透协议分析

383 阅读3分钟

Ngrok4j

背景

ngrok是一个go语言编写的内网穿透代理软件,之前的开源的版本是 2.1.7,也已经很多年没有更新了,作者也已经放弃这个版本的维护了,因为这个版本存在一个很严重的bug,内存泄漏。导致很多使用者望而止步,一旦用来下载大文件,内存将会一直占用,不会释放。本来想基于go重写一版的,奈何还没熟悉,只能先用吃饭的Java来熟悉下协议流程。因此创建了ngrok4j的工程,基于netty框架来实现,像很多例如Dubbo、RocketMQ、Hadoop都是使用了netty,正好学习这种异步的网络处理框架。

协议分析

ngrok的协议还是比较简单,一个数据包包括数据长度和内容两部分,注意的是长度占了8字节且是小端,关于大小端的解释

Big-Endian和Little-Endian的定义如下:

  1. Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

低地址 -----------------> 高地址

0x12  |  0x34  |  0x56  |  0x78

  1. Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

低地址 ------------------> 高地址

0x78  |  0x56  |  0x34  |  0x12

包内容

数据内容是用网络交互常见的json,json的结构也是极其的简单,等会再解释几种的数据类型。

{
    "Type":"数据的类型",
    "Payload":"数据的内容"
}

比如一个最简单的心跳包

{"Type":"Pong","Payload":{}}

一共是28个长度

那么前面8位将会是

数据长度 | 数据内容

1c 00 00 00 00 00 00 00 | 7b 22 54 79 70 65 22 3a 22 50 6f 6e 67 22 2c 22 50 61 79 6c 6f 61 64 22 3a 7b 7d 7d

0x1c = 28

数据类型

  • 心跳包(Ping/Pong):用于客户端和服务端的确认,每过段时间必须发一次,确保连接不被服务端断掉。
{"Type":"Pong","Payload":{}}
{"Type":"Ping","Payload":{}}
  • 认证包(Auth/AuthResponse):用于客户端请求建立连接的clientId
{
    "Type":"Auth",
    "Payload":{
        "Version":"2",
        "MmVersion":"1.7",
        "User":"",
        "Password":"",
        "Arch":"amd64",
        "ClientId":"",
        "OS":"Windows 10"
    }
}{
    "Type":"AuthResp",
    "Payload":{
        "Version":"2",
        "MmVersion":"1.7",
        "ClientId":"75cffec60f18026fc5d9f0e5ca0dc76b",
        "Error":""
    }
}
  • 隧道请求(ReqTunnel/NewTunnel):用于隧道的建立
{
    "Type":"ReqTunnel",
    "Payload":{
        "ReqId":"bb813b73",
        "Protocol":"http",
        "Hostname":null,
        "Subdomain":"local-nginx",
        "HttpAuth":null,
        "RemotePort":0
    }
}{
    "Type":"NewTunnel",
    "Payload":{
        "ReqId":"bb813b73",
        "Url":"http://local-nginx.vaiwan.com",
        "Protocol":"http",
        "Error":""
    }
}
  • 代理请求(RegProxy/ReqProxy):用于代理的建立
{"Type":"RegProxy","Payload":{"ClientId":"0bb68e355aed1ee8722cc4bf09c2f791"}}
{"Type":"ReqProxy","Payload":{}}
  • 代理开始(StartProxy):服务端将会发送这个标志,表示开始代理真实数据的交互。
{
    "Type":"StartProxy",
    "Payload":{
        "Url":"http://local-nginx.vaiwan.com",
        "ClientAddr":"110.87.21.142:7618"
    }
}

时序分析

ngrok.svg

首先最开始ngrok客户端需要建立一个control的连接即图中的黑色交互线,建立连接后,需要定时发送Ping

  1. 发送auth获取到clientId,
  2. 发送需要建立隧道的请求ReqTunnel
  3. 等待服务器发送建立代理ReqProxy

然后ngrok客户端需要开始代理准备,即图中蓝色线

  1. 建立新的一条和服务端的连接
  2. 建立一条和本地服务端的连接
  3. 等待客户端的请求StartProxy
  4. 开始数据的传输交互即图中的橙色线

代码地址

具体的代码逻辑实现已经提交到 github (备用地址coding)上,可能不够完善。使用了钉钉的免费穿透服务器来测试。下载速度还是可以的,同时下载了大文件,没有出现内存溢出。但是不熟悉netty,中间遇到了堆外内存溢出的情况。虽然不影响请求访问,但是log里还是不停的弹异常信息,不过还是花了几天的时间终于处理掉这个问题。后面会继续分享netty使用中遇到的情况和解决方案。

Untitled.png