Metadata-Version: 2.4
Name: adbproxy
Version: 1.0.0
Summary: ADB smart-socket and device transport proxies
Author: Forgo7ten
License: MIT
Project-URL: Homepage, https://github.com/Forgo7ten/adbproxy
Project-URL: Repository, https://github.com/Forgo7ten/adbproxy
Project-URL: Issues, https://github.com/Forgo7ten/adbproxy/issues
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: zstandard>=0.25.0
Provides-Extra: lz4
Requires-Dist: lz4>=4.3.0; extra == "lz4"
Provides-Extra: brotli
Requires-Dist: brotli>=1.1.0; extra == "brotli"
Provides-Extra: decompress
Requires-Dist: adbproxy[brotli,lz4]; extra == "decompress"
Dynamic: license-file

# adbproxy

在 ADB client 与 server / 设备之间插入透明代理，解析并记录 ADB 协议流量，同时保持原始字节原样转发。

适用于调试 adb 命令、观察 shell / sync / install 等协议交互、抓取 push/pull 文件内容。

## 功能

- **server-proxy**：MITM 本机 `adb client ↔ adb server` 的 smart-socket 链路（默认监听 `5037`，真实 server 挪到 `5038`）
- **device-proxy**：模拟网络 adbd，接受 transport 协议连接，再经系统 adb server 转发到真实设备
- 解析并记录 shell、sync、install、stream 等协议事件
- 可选落盘 sync / install 文件内容，以及 `--debug` 原始 hex
- 观察 / 解码 / 抓包失败时 fail-open，不中断透明转发

## 架构

```mermaid
flowchart LR
  Client[adb client]
  SP[server-proxy :5037]
  Server[adb server :5038]
  Dev[真实设备]

  Client -->|smart socket| SP
  SP -->|smart socket| Server
  Server --> Dev

  Client2[adb client]
  ServerFront[adb server :5037]
  DP[device-proxy :5566]
  ServerBack[adb server :5037]
  Dev2[真实设备]

  Client2 -->|smart socket| ServerFront
  ServerFront -->|transport| DP
  DP -->|smart socket| ServerBack
  ServerBack --> Dev2
```

## 环境要求

- Python ≥ 3.10
- [uv](https://docs.astral.sh/uv/)
- 系统 `adb`（Android platform-tools）

## 安装

```bash
git clone <repo-url>
cd adbproxy   # 或你的本地目录名
uv sync
```

也可从 PyPI：

```bash
pip install adbproxy
# 可选：sync 解压依赖
pip install "adbproxy[decompress]"

# 临时运行
uvx adbproxy server --help
uvx adbproxy device --help
```

`uv sync` 会安装依赖，并根据 `pyproject.toml` 的 `[project.scripts]` 注册命令行入口：

| 命令 | 对应模块 | 作用 |
|------|----------|------|
| `adbproxy` | `adbproxy.cli.main:main` | **推荐**统一入口：`server` / `device` 子命令 |
| `adb-server-proxy` | `adbproxy.cli.server:main` | 兼容旧入口（等同 `adbproxy server`） |
| `adb-device-proxy` | `adbproxy.cli.device:main` | 兼容旧入口（等同 `adbproxy device`） |

推荐启动方式：

```bash
# 统一入口（推荐）
uv run adbproxy server --help
uv run adbproxy device --help
uv run python -m adbproxy server --help
uv run python -m adbproxy device --help

# 兼容旧 console script / 模块路径
uv run adb-server-proxy --help
uv run adb-device-proxy --help
uv run python -m adbproxy.cli.server --help
uv run python -m adbproxy.cli.device --help
```

下文示例统一使用 `adbproxy server` / `adbproxy device`。

## 使用

### 1. server-proxy（MITM 本机 5037）

启动前确认本机 `5038` 未被占用。代理会接管默认 `5037`，把真实 adb server 挪到 `5038`；退出时按所有权恢复。

```bash
uv run adbproxy server
# 等价：
#   uv run python -m adbproxy server
#   uv run adb-server-proxy

# 可选参数：
#   --debug                  将每段 recv 的原始 hex 落盘
#   --no-capture             不落盘 sync/install 文件，仅打印摘要
#   --log-format json        事件日志写成 JSONL（默认 text）
#   --listen PORT            代理监听端口，即 adb client 连的那个（默认 5037）
#   --adb-server-port PORT   转发到的真实 adb server 端口（默认 5038）
#   --reuse-adb-server       复用已在运行的后端 server，不接管也不启停任何 server
```

两个端口成对使用：`--listen` 是要从真 adb server 手里接管的端口，`--adb-server-port`
是真 server 被挪去的地方。改这两个值可以同时跑多个实例，或在默认端口被别的工具占用时改道：

```bash
# 接管 6037，把真实 server 放到 6038
uv run adbproxy server --listen 6037 --adb-server-port 6038
adb -P 6037 devices -l
```

#### 复用模式（不接管任何 server）

`--reuse-adb-server` 让代理只做转发：不 kill 监听端口上的实例、不起新的后端、退出时也
没有要恢复的东西。适合本机还有别的 adb 客户端（IDE 之类）时挂一层观察——它们继续用
默认 5037，你用代理端口：

```bash
# 监听 6037，转发到已在运行的 5037
uv run adbproxy server --listen 6037 --adb-server-port 5037 --reuse-adb-server
adb -P 6037 shell echo hello    # 经过代理
adb devices -l                  # 不经过代理，照常工作
```

两种模式的区别：

| | 默认（接管） | `--reuse-adb-server` |
|---|---|---|
| 监听端口上的真 server | 先 kill，退出时恢复 | 不碰（该端口必须空闲，否则 bind 失败） |
| 后端 server | 由代理启动并持有所有权 | 必须已在运行；代理不启不停 |
| 后端意外死掉 | 下一条连接自动重建 | 不重建（没有所有权） |
| 设备可见性 | 后端要重新独占 USB | 设备始终在原 server 上 |

另开终端正常使用 adb（默认走 5037，即经过代理）：

```bash
adb devices -l
adb shell echo hello
adb push local.apk /data/local/tmp/
adb install local.apk
```

### 2. device-proxy（模拟网络 adbd）

默认监听 `5566`，后端复用系统 `5037` adb server，把请求转发到指定真实设备。

```bash
uv run adbproxy device \
  --listen 5566 \
  --target serial:<设备序列号>

# 等价：
# uv run python -m adbproxy device \
#   --listen 5566 \
#   --target serial:<设备序列号>
# uv run adb-device-proxy \
#   --listen 5566 \
#   --target serial:<设备序列号>
```

`--target` 支持：

| 值 | 含义 |
|----|------|
| `usb` | 自动选择首个 USB 且状态为 `device` 的设备（默认） |
| `serial:<s>` | 指定序列号 |
| `net:<ip>:<port>` | 先 `adb connect` 到该网络设备 |

常用参数：

```text
--listen PORT           监听端口，默认 5566
--adb-server-port PORT  后端 adb server 端口，默认 5037
--debug                 原始 hex 落盘
--no-capture            禁用 sync/install 文件落盘
--banner-type TYPE      CNXN banner 前缀，默认 device
--log-format {text,json} 事件日志格式，默认 text
```

另开终端连接并操作：

```bash
adb connect 127.0.0.1:5566
adb -s 127.0.0.1:5566 get-state
adb -s 127.0.0.1:5566 shell echo hello
adb -s 127.0.0.1:5566 push local.apk /data/local/tmp/
adb disconnect 127.0.0.1:5566
```

## 输出目录

| 路径 | 内容 |
|------|------|
| `out/logs/*.log` | 事件日志（默认 text 格式）；加 `--debug` 时另有 debug hex 日志 |
| `out/logs/*.jsonl` | `--log-format json` 时的事件日志，每行一条 JSON |
| `out/logs/*.1` | 日志超过大小上限后转存的上一代文件（只保留一代） |
| `out/files/` | sync push/pull、install 流捕获（未加 `--no-capture` 时） |

单个日志文件超过 64 MiB 会转存为 `.1` 并重开，磁盘占用因此稳定在 2× 上限以内；
`--debug` 对每块 recv 落一整段 hexdump，长跑时增长很快，这个上限尤其有用。
用 `ADB_PROXY_MAX_LOG_BYTES` 调整。

JSONL 每行含 `ts` / `conn` / `level` / `msg` 四个字段（进程级消息没有 `conn`）：

```bash
jq -r 'select(.level=="event") | "\(.conn)\t\(.msg)"' out/logs/*.jsonl
```

`out/` 的位置：从源码树运行时落在项目根；通过 `pip install` / `uvx` 安装后落在当前工作目录。
两种情况都可用 `ADB_PROXY_OUT_DIR` 覆盖。

```bash
export ADB_PROXY_OUT_DIR=/tmp/adbproxy-out
# 写入 /tmp/adbproxy-out/logs 与 /tmp/adbproxy-out/files
```

## 测试

### 准备

真机动态矩阵（push / pull / install）依赖测试 APK。仓库不附带该文件，运行前请自行放置：

```bash
# 任意可安装的 APK 即可
cp /path/to/your.apk tests/app.apk
```

`tests/app.apk` 已在 `tests/.gitignore` 中忽略，不会被提交。

无设备的单元 / 静态测试不需要该文件。

### 环境变量

| 变量 | 说明 |
|------|------|
| `ADB_PROXY_REAL_DEVICE=1` | 启用真机动态用例；未设置时相关用例自动 skip |
| `ADB_PROXY_EXCLUSIVE_ADB=1` | 额外启用接管默认端口的用例；要求本机没有其它 adb 客户端 |
| `ADB_SERIAL` | 指定真机序列号；省略时取首个 USB 且状态为 `device` 的设备 |
| `ADB_PROXY_OUT_DIR` | 覆盖运行产物根目录（日志 / 抓包），默认见「输出目录」一节 |
| `ADB_PROXY_MAX_LOG_BYTES` | 单个日志文件的大小上限，超过即轮转，默认 64 MiB |

### 全部测试

```bash
# 无设备：单元 + 静态；动态用例自动 skip
uv run python -B -m unittest discover -s tests -v

# 含真实设备动态矩阵（需先放置 tests/app.apk）
export ADB_PROXY_REAL_DEVICE=1
export ADB_SERIAL=<可选，默认取首个 USB device>
uv run python -B -m unittest discover -s tests -v
```

### 按文件运行

| 文件 | 覆盖范围 |
|------|----------|
| `tests/test_protocol_codecs.py` | smart-socket wire / classifier / conversation、payload、transport codec |
| `tests/test_server_connection.py` | server-proxy 转发与旁路观察、fail-open 不变量 |
| `tests/test_device_transport.py` | AdbdSession 决策、StreamTable、后端握手、front 传输行为 |
| `tests/test_observers.py` | StreamBuffer 路由、sync 分帧/压缩/落盘、install 抓包命名 |
| `tests/test_app_lifecycle.py` | 输出目录、cleanup 顺序、adb server lease、目标解析 |
| `tests/test_imports.py` | 包布局与导入 |
| `tests/test_cli_main.py` | 统一 CLI 分发 |
| `tests/test_dynamic_commands.py` | 真机动态矩阵（需 `tests/app.apk`） |

server-proxy 的动态矩阵跑在「监听空闲端口 + 复用现有 5037 作后端」的模式下，因此本机开着
Android Studio 之类的 adb 客户端也不影响它。接管默认端口那条路径是单独的用例，需要
`ADB_PROXY_EXCLUSIVE_ADB=1` 才会跑。

```bash
uv run python -B -m unittest tests.test_protocol_codecs -v

# 真机动态矩阵（server-proxy + device-proxy；需 tests/app.apk）
export ADB_PROXY_REAL_DEVICE=1
export ADB_SERIAL=<可选>
uv run python -B -m unittest tests.test_dynamic_commands -v
```

### 代码检查

```bash
uv sync --group dev

uv run ruff check .          # lint
uv run mypy                  # 类型检查（协议层开启 check_untyped_defs）
uv run coverage run -m unittest discover -s tests && uv run coverage report
```

CI（`.github/workflows/ci.yml`）在 Python 3.10–3.13 上跑同一组命令，覆盖率低于 72% 会失败。

## 说明

- 代理是透明的：解析失败不会改写或阻断转发字节。观察器出错只降级、不影响转发，
  被吞掉的异常在 `--debug` 下会连 traceback 写进 debug 日志，否则记一条 debug 级提示。
- 退出码：正常收尾为 `0`；若退出时没能把 adb server 恢复原状（例如 `adb start-server`
  失败），返回 `1` 并在日志里给出需要手动执行的命令。

### adb kill-server 与后端自愈

`host:kill` 和别的请求一样原样转发，代理不改写这条语义：客户端拿到的是真实 server 的
应答，`adb kill-server` 的输出与退出码都和没有代理时一致（AOSP 服务端的处理是 `SendOkay`
后 `exit(0)`，因此客户端读到 OKAY 再看到连接关闭）。

后端就此退出。只要它的所有权在代理手上（默认模式下由代理启动的那个），下一条需要它的
连接会把它重建出来——`adb start-server`、`adb devices`、任何命令都算，这也正是 adb 自己
的语义：kill 之后任何需要 server 的命令都会重新唤起它。重建遵循和 lease 一样的规矩：
只在 ledger 能证明该端口上的 server 是本进程起的时候才动手，端口上换成了别人的实例就不碰。

自愈不只针对 kill-server：后端崩溃、被 `pkill`、被 OOM 杀掉都走同一条路径（下一条连接
连不上后端 → 重建 → 重试一次）。

`--reuse-adb-server` 下后端是别人的 server，代理不会替它重建；转发 kill 时日志会提醒
一句，之后需要自己 `adb start-server`。

顺带一提，`adb start-server` 没有对应的代理逻辑：客户端在发出 `host:start-server`
**之前**就 return 了（`adb_client.cpp`），线上真正到达 server 的只有 `host:version`。

### 已知限制

| 限制 | 说明 |
|------|------|
| 不支持 `A_AUTH` | device-proxy 收到 AUTH 包会终止该连接。前端需已授权、或用无需配对的连接方式 |
| 不支持 `A_STLS` | 同上，收到 STLS 即终止；不支持 TLS 化的 adb 连接 |
| v1 停等流控 | device-proxy 的 CNXN banner 会硬剥 `delayed_ack`，后端真机广告了也不透传 |
| 单文件捕获上限 | sync/install 落盘单文件超过 256 MiB 即停止写入并记日志 |
