rea-reverse

Protobuf 在线解码:没有 .proto 也能看字段,等同 protoc --decode_raw

把 protobuf、gRPC 或 gRPC-Web 的二进制 body 以十六进制或 base64 粘贴进来,或直接打开原始文件,就能看到字段号、wire type 和值。文本视图的输出和 protoc --decode_raw 完全一样,树形视图列出每个字段的其他可能解读。

你的数据不会离开浏览器. 解码在本地一个没有网络权限的 Web Worker 中完成,不上传任何内容。 如何自己验证

在单独页面打开工具(无广告、无统计)

调试自己的应用时抓到一个请求或响应,body 是二进制的:application/x-protobuf、application/grpc 或 application/grpc-web+proto。DevTools 里只能看到一串乱码,描述它的 .proto 又在另一个仓库里,或者在你手头没有的那个构建版本里。装了 protoc 的话,把字节存成文件跑一下 protoc --decode_raw 就能看结构。这个页面把同样的事搬进浏览器:贴上字节,直接看字段。

使用步骤

  1. 拿到字节:在 DevTools 里把请求或响应 body 复制成十六进制或 base64,或者另存为文件。想先试一下,可以点“载入示例”。
  2. 粘贴到上方的输入框(十六进制和 base64 会自动识别),点“解码”;也可以打开二进制文件。gRPC 的 body 请保持勾选“检测 gRPC / gRPC-Web 分帧”。
  3. 先看概要(输入类型、字节数、分帧),再看与 protoc --decode_raw 一致的文本视图;切换到树形视图,可以逐个字段换成其他解读。

真实输出示例

我们让 protoc 36.2 把 Google 的 timestamp.proto 的描述写成二进制 FileDescriptorSet(258 字节),把它的十六进制粘贴到本页,点“解码”,再把页面显示的结果复制下来。排版做了压缩(标签和值放在同一行,省去按钮),所有值都原样保留。文本视图与 protoc --decode_raw 逐字节一致。

样本: google/protobuf/timestamp.proto(BSD-3-Clause,随 protoc 36.2 提供)的 FileDescriptorSet,2026-11-02 用 protoc --descriptor_set_out 生成

概要
输入        十六进制文本
字节数      258
分帧        无(单条 protobuf 消息)
顶层字段数  1

消息:文本(protoc --decode_raw)
1 {
  1: "google/protobuf/timestamp.proto"
  2: "google.protobuf"
  4 {
    1: "Timestamp"
    2 {
      1: "seconds"
      3: 1
      4: 1
      5: 3
      10: "seconds"
    }
    2 {
      1: "nanos"
      3: 2
      4: 1
      5: 5
      10: "nanos"
    }
  }
  8 {
    1: "com.google.protobuf"
    8: "TimestampProto"
    10: 1
    11: "google.golang.org/protobuf/types/known/timestamppb"
    31: 1
    36: "GPB"
    37: "Google.Protobuf.WellKnownTypes"
  }
  12: "proto3"
}

能做什么

输入。 把 body 以十六进制(空格、冒号、逗号以及 0x、\x 前缀都会被忽略)或 base64(标准或 URL 安全字母表,有没有补位的 = 都行)粘贴进来,也可以直接打开原始字节文件,最大 16 MiB。输入格式自动识别:只含十六进制字符的文本按十六进制读,其余按 base64 读。如果你的 base64 恰好全由 0-9、a-f 组成,手动选 Base64 即可。

gRPC 分帧。 gRPC 和 gRPC-Web 的 body 并不是裸的 protobuf:每条消息前面有 1 字节标志位和 4 字节大端长度,流式响应里会连着好几帧。字节能被完整切成这样的帧时,每一帧单独展示:

  • 标志 0x00:普通消息,直接解码;
  • 标志 0x01:压缩过的消息。开头像 gzip 或 zlib 的(grpc-encoding: gzip 或 deflate)用浏览器自带的 DecompressionStream 解压后再解码;snappy、zstd 等其他编码只做提示,不去猜;
  • 标志 0x80:gRPC-Web 的 trailers 帧,按文本显示(grpc-status、grpc-message)。

gRPC-Web-text(application/grpc-web-text)的 body 就是这些帧的 base64,有时是好几段各自带补位的 base64 首尾相接,原样粘贴即可。如果想把字节当成单条消息解,取消勾选 检测 gRPC / gRPC-Web 分帧。整个 body 是一个 gzip 流时,会先解压。

文本视图。 默认视图和 protoc --decode_raw 的输出逐字节相同:每个字段一行 字段号: 值,varint 按无符号十进制,fixed32 和 fixed64 按补零的十六进制,嵌套消息用花括号,其余按 C 风格转义的字符串输出。可以复制或下载成 .txt,两次抓包的结果拿任何文本 diff 工具就能比。

树形视图。 wire format 只记录值是怎么编码的,不记录它是什么意思,所以树形视图把讲得通的解读都列出来:

  • varint:无符号、有符号 int64(最高位为 1 时)、zigzag 的 sint,值为 0 或 1 时还有 bool;
  • fixed32 和 fixed64:无符号、有符号,以及 float 或 double;
  • length-delimited:嵌套消息、UTF-8 字符串、packed varint、packed fixed32 或 fixed64,以及十六进制字节。每个字段都有按钮切换解读,默认显示哪一种和 protoc 的优先顺序一致(先当消息);
  • 已废弃的 group(wire type 3 和 4)和 protoc 一样按嵌套字段显示。

每个字段还标出字节偏移,方便在十六进制编辑器里对照。

字段类型是怎么判断的

protoc 对每个非空的 length-delimited 值都先试着当嵌套消息解析:能完整解析成字段就按消息输出,否则按字符串输出。所以两个字节的字符串 "hi" 会被打印成 1 { 13: 105 }:0x68 恰好是合法的 tag(字段 13,varint),0x69 是合法的值。文本视图保留 protoc 的这个选择,树形视图里可以把这个字段切到 字符串。

解码器照着 protoc 的两套解析逻辑实现。顶层按 C++ ParseContext 的规则读(tag 和长度最多 5 字节,长度小于 2 GiB,group 最多嵌套 100 层);嵌套的值按 TextFormat::Printer 的方式重新解析,预算从 10 开始、每深一层减 1,所以嵌套超过 10 层的消息在 protoc 和这里都会显示成字符串。

我们在 macOS 上用 libprotoc 36.2 做了对照。为本页写了 10 个测试消息(全部标量类型、packed 与非 packed 的 repeated 字段、sint32 和 sint64、含中文和 emoji 的字符串、map、oneof、空消息、最大到 536,870,911 的字段号、proto2 group、一棵 12 层的树),用 protoc --encode 编码后,文本输出与 protoc --decode_raw 逐字节一致。另外 1,000 个随机生成并随机篡改过的输入(包括 protoc 两套解析逻辑表现不同的超长 varint),凡是 protoc 能解的,输出文本都相同;protoc 拒绝的,这里也都报错。

字节不是合法 protobuf 时

protoc 会直接报 Failed to parse input.,什么都不输出。这个页面会把出错位置之前的字段解出来显示,并给出出错的字节偏移和原因:值被截断、本该是 tag 的位置出现 0x00(常见于填充字节或末尾的校验和)、字段号为 0、wire type 6 或 7、没有起始的 end-group tag、超过 10 字节的 varint。如果一个字段都解不出来,先确认粘贴的是 body 而不是整段 HTTP 报文,再试试打开或关闭分帧检测。

数据如何保持私密

  • 解码器运行在一个单独的框架(iframe)里,来源是 tools.rea-reverse.com。下面说的处理都在这个框架里、在你的设备上完成。
  • 这个页面在正文之后展示一个广告。广告脚本运行在 rea-reverse.com 上,而框架属于另一个源(origin),浏览器不允许广告脚本或本页的任何其他脚本读取你粘贴的内容、打开的文件或结果。
  • 框架的内容安全策略拦截它的脚本能发起的所有请求(connect-src 'none',也不加载任何第三方资源)。框架不能自行打开窗口或让本页跳转;只有你点击报告里的链接时才会打开页面。它传给本页的只有自身高度和下面这些统计事件。
  • 解码在你设备上的 Web Worker 里完成,粘贴或打开的内容都不会上传。
  • 字段值一律按纯文本显示,payload 里的任何内容都不会被当成 HTML 解析或执行。
  • 统计事件只有粗粒度信息:成功、malformed 或错误码,字节的来源(hex、base64 或 file),是否识别出 gRPC 分帧,大小区间和耗时区间。绝不包含字节内容、字段号或字段值。框架把事件交给本页,由本页发送。完整列表见隐私政策。
  • 不想要任何广告和统计?在单独页面打开解码器。

如何自己验证

  1. 打开浏览器开发者工具的 Network(网络) 面板,加载本页。发往 tools.rea-reverse.com 的请求就是框架:它的 HTML、样式、字体和脚本,以及 Worker 脚本 /_astro/protobuf-worker-*.js。发往 Google 域名的请求是本页的广告。
  2. 清空日志,然后解码一段数据。在过滤框里输入 domain:tools.rea-reverse.com:最多只会再出现一次 Worker 脚本 /_astro/protobuf-worker-*.js(为下一次运行准备的新 Worker),没有任何请求带着你的数据。广告请求可能还会继续,但它们来自本页,不带框架里的任何内容。
  3. 点开来自 tools.rea-reverse.com 的 protobuf-decoder/ 文档,查看 Headers:它的 Content-Security-Policy 里有 connect-src 'none' 和 frame-ancestors https://rea-reverse.com。在 Chrome 里,Application(应用)> Frames 也能看到框架的源和策略。Worker 运行在 default-src 'none'; script-src 'self'; connect-src 'none' 这条策略下,浏览器不允许它发出任何网络请求。
  4. 检查隔离:在 Console(控制台) 里,上下文选 top,运行 document.getElementById('tool-frame').contentDocument,结果是 null;frames[0].document 会抛出 SecurityError。广告脚本拿到的也是这个结果。
  5. 或者先打开页面再断网(关掉 Wi-Fi,或在 Network 面板勾选 Offline),然后解码,照样能用。

局限

  • 没有字段名和类型。 没有 schema,字段 2 就只是字段 2,string 和 bytes 也分不出来。暂不支持粘贴 .proto 按名字解码:常用的 protobuf.js 用 new Function 动态生成解码代码,而本页的内容安全策略禁止这样做。
  • 有歧义的值依然有歧义。 短字符串可能被解析成消息,一串小整数的 packed 列表也可能被当成字符串。树形视图把可能的解读都列出来,哪个对得上你的接口,由你来判断。
  • 压缩只支持浏览器能解的 gzip 和 deflate 帧。snappy 和 zstd 帧会标出来,但不解码。
  • 大小。 最多 16 MiB 字节。树形视图只显示前 200,000 个字段,文本视图和下载的文件始终完整。

字节从哪里来

要先抓到自己 Web 应用的流量,可以在 DevTools 里录制并导出 HAR;HAR / SAZ 接口归纳会按接口归纳请求并列出内容类型,protobuf 接口一眼就能找到,HAR 文件分析指南讲了怎么读抓包文件、怎么安全地分享。只抓取和解码你自己拥有或已获授权测试的应用与服务的流量。

对应的命令行

样本的生成方式(Homebrew 安装的 protoc 36.2,常用的 .proto 文件放在 /opt/homebrew/include 下):

$ protoc -I/opt/homebrew/include --descriptor_set_out=timestamp.desc google/protobuf/timestamp.proto
$ wc -c timestamp.desc
     258 timestamp.desc

不带 schema 的原始解码,输出与上面页面的文本视图完全相同:

$ protoc --decode_raw < timestamp.desc
1 {
  1: "google/protobuf/timestamp.proto"
  2: "google.protobuf"
  4 {
    1: "Timestamp"
…
  12: "proto3"
}

带上 schema(descriptor.proto)后,同样的字节有了字段名:字段 4 是 message_type,字段 8 是 options;字段内部 3 是 number,5 是 type(3 = TYPE_INT64,5 = TYPE_INT32):

$ protoc -I/opt/homebrew/include --decode=google.protobuf.FileDescriptorSet google/protobuf/descriptor.proto < timestamp.desc
file {
  name: "google/protobuf/timestamp.proto"
  package: "google.protobuf"
  message_type {
    name: "Timestamp"
    field {
      name: "seconds"
      number: 1
      label: LABEL_OPTIONAL
      type: TYPE_INT64
      json_name: "seconds"
    }
…
  syntax: "proto3"
}

常见问题

没有 .proto 文件,怎样解码 protobuf?

把字节以十六进制或 base64 粘贴进来,点“解码”。你会看到字段编号、wire type 和值,与 protoc --decode_raw 的输出相同;字段名和确切类型只存在于 schema 里,上面描述文件的例子就说明了这一点。

和 protoc --decode_raw 一样吗?

文本视图与 libprotoc 36.2 的 protoc --decode_raw 逐字节一致,包括它“优先当成嵌套消息而不是字符串”的判断和 10 层嵌套上限。树形视图在此之外列出每个字段的其他可能解读。

怎样解码 gRPC-Web 响应?

按原样粘贴 body:二进制 gRPC-Web 用十六进制或文件,application/grpc-web-text 用 base64。勾选“检测 gRPC / gRPC-Web 分帧”后,每一帧分别解码,trailers 帧以文本显示。

为什么一个字符串被显示成了嵌套消息?

wire format 对字符串、bytes、嵌套消息和 packed 列表用的是同一种长度前缀类型,短字符串也可能恰好能解析成合法消息。和 protoc 一样,文本视图优先当作消息;在树形视图里可以把这个字段切换成字符串。

能用我的 .proto 文件解码吗?

不能。浏览器里常用的 protobuf.js 会用 new Function 生成解码函数,本页的内容安全策略不允许这样做。请在本地运行 protoc --decode=YourMessage your.proto < body.bin。

数据会上传吗?

不会。解码在你设备上的 Web Worker 里运行,这个 Worker 无法联网,你粘贴或打开的字节不会离开浏览器。

更新于

相关教程