Files
mysql-tunnel/README.md
T
2026-09-21 15:59:34 +08:00

468 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mysql-tunnel
Windows 本机 MySQL 协议代理,给 **影刀 RPA**(以及其它只认 MySQL 协议的客户端)用。
影刀连的是本机 `127.0.0.1:13306`。本进程把这条连接翻译成 **Navicat HTTP Tunnel** 协议,再 POST 到服务器上的 `ntunnel_mysql.php`,由 PHP 去连远程 MySQL。
```
影刀 RPA / mysql 客户端
│ MySQL 协议(握手、认证、COM_QUERY)
▼
127.0.0.1:13306
│
mysql-tunnel.exe ← 本仓库编译出的 Windows 程序
│ HTTP POST application/x-www-form-urlencoded
│ URL 带 ?token=...
▼
ntunnel_mysql.php ← 部署在能访问远程 MySQL 的 Web 主机上
│ mysqli_connect + mysqli_real_query
▼
远程 MySQL:3306
```
这 **不是** TCP-over-HTTP 端口转发,也 **不是** 业务 REST API。每一条 SQL 都走一次独立的 HTTP 请求。
协议细节见 [docs/PROTOCOL.md](docs/PROTOCOL.md)。
---
## 当前状态
**可用(v0.5.0):** 本机 MySQL Server(默认 `127.0.0.1:13306`)已接通 Navicat HTTP 隧道。Handshake、`mysql_native_password`、`COM_QUERY` / `COM_PING` / `COM_QUIT` / `COM_INIT_DB` 可用。Windows 双击 exe 打开配置窗口(保存、测试、启动/停止、托盘)。可选安装为 Windows 服务。
---
## 工作原理
仓库里的 `ntunnel_mysql.php` 是 Navicat 16 官方隧道脚本(script version **206**),外加 GET `token` 门禁。
每次 HTTP POST,PHP 都会:
1. 校验 URL 上的 `?token=`
2. `mysqli_connect(host, login, password, …)`
3. `mysqli_set_charset(..., 'UTF8')`
4. 可选 `mysqli_select_db`
5. `actn=C` 返回服务器信息,或 `actn=Q` 执行 `q[]` 里的 SQL
6. 脚本结束,**远程 MySQL 连接被丢掉**
因此:
- **无持久会话。** `START TRANSACTION` / `COMMIT` 不能跨两次 HTTP 请求。
- `SET` 会话变量、`last_insert_id()` 只在同一次 POST 的 `q[]` 里有效。
- 影刀侧的 `USE db` 必须由 mysql-tunnel **记住**,并在后续请求里带上 `db=`。
- 大结果集会整包进 PHP 内存再作为 HTTP 响应返回。
`config.yaml` 里的 `charset` **不是** Navicat POST 字段。PHP 固定 UTF8;本客户端在 `actn=Q` 时会把 `SET NAMES utf8mb4`(若配置了非 utf8 字符集)作为第一条 SQL 一并发送。
---
## 你需要准备什么
| 角色 | 是什么 | 谁提供 |
|---|---|---|
| 本机程序 | `mysql-tunnel.exe` | 本仓库编译,或已有的 `dist\mysql-tunnel.exe` |
| 配置文件 | `config.yaml` | 从 `config.example.yaml` 复制后填写 |
| 隧道脚本 | `ntunnel_mysql.php` | 上传到能访问目标 MySQL 的 PHP 主机 |
| 访问令牌 | URL `?token=` 与 PHP `$accessToken` 必须一致 | 你自己设定 |
| 远程 MySQL | PHP 主机能连上的库(常是 `127.0.0.1` / Docker 名 `mysql`) | 运维 / 现有环境 |
| 影刀 | 把数据源指到本机监听地址 | 本机 `mysql-tunnel.exe` 保持运行 |
环境要求:
- **Windows**(目标运行环境)。Linux/macOS 可编译、跑单测,但产品面向影刀本机。
- **Go 1.24+**(仅构建需要;运行 `mysql-tunnel.exe` 不需要装 Go)。
- PHP 主机需启用 **mysqli**,并能访问目标 MySQL。
- 本机到隧道 URL 的 HTTP/HTTPS 网络畅通(公司代理走系统环境变量)。
第三方依赖:`gopkg.in/yaml.v3`(配置)、`github.com/lxn/walk`(Windows 配置窗口,无 CGO)。`github.com/go-sql-driver/mysql` 仅用于兼容性测试,不会链进 `mysql-tunnel.exe`。Windows 服务使用 `golang.org/x/sys/windows/svc`。
---
## 快速开始
### 1. 编译
Go 已加入 `PATH` 后,在仓库根目录:
```bat
go mod tidy
go test ./...
build.bat
```
产物:`dist\mysql-tunnel.exe`。`build.bat` 会先跑完全部测试,再以 `CGO_ENABLED=0` 生成精简二进制。
等价命令:
```bat
go test ./...
go build -trimpath -ldflags="-s -w" -o dist\mysql-tunnel.exe .\cmd\tunnel
```
有 Make 时也可以 `make test` / `make build`(后者交叉编译 Windows amd64)。
### 2. 部署 PHP 隧道
把 `ntunnel_mysql.php` 放到 Web 根目录或由反向代理转到该文件。编辑文件顶部:
```php
$accessToken = '换成足够长的随机串';
$allowTestMenu = false;
```
浏览器访问(应看到 403 文本,说明门禁在工作):
```
https://你的域名/ntunnel_mysql.php
```
带正确 token、无 POST 时,脚本返回二进制错误包(errno 202),而不是 HTML 测试页。
URL 路径可以重写。客户端只要 POST 到配置里的完整 URL(**必须带** `?token=`)。例如:
```
https://example.com/ntunnel_mysql.php?token=YOUR_SECRET
https://ntunnel.example.com/?token=YOUR_SECRET
```
Token **只** 放在 query string,不要放 POST body,也不要用 `Authorization` 头。PHP 只读 `$_GET['token']`。
### 3. 填写配置
双击 `dist\mysql-tunnel.exe` 会打开配置窗口(无参数时默认如此)。把隧道 URL、远程库账号等填进表单,点 **保存**(写入 exe 旁的 `config.yaml`)。窗口可最小化到任务栏;点关闭会藏到托盘,监听可继续。托盘图标双击还原,右键退出。
也可以复制模板后用编辑器改:
```bat
copy config.example.yaml config.yaml
```
**不要把 `config.yaml` 提交进 Git**(已在 `.gitignore`)。密钥只放在 yaml 里,不会写进 exe。
### 4. 验证隧道
在配置窗口点 **测试连接**,或在仓库根目录(或把 `config.yaml` 拷到 exe 同级目录):
```bat
dist\mysql-tunnel.exe --test
```
成功时应类似:
```
mysql-tunnel --test
Tunnel URL: https://example.com/ntunnel_mysql.php?token=***
Tunnel: OK
Remote MySQL: OK
Query SELECT 1: OK
```
失败时先看「常见问题」。日志默认写到 `logs\tunnel.log`(密码和 token 会被打码)。
### 5. 日常启动
**推荐:** 双击 `mysql-tunnel.exe`,在窗口里点 **启动**。可最小化;关窗口进托盘,进程还在。
不要窗口、前台监听(关控制台即退出):
```bat
dist\mysql-tunnel.exe --console
dist\mysql-tunnel.exe --config config.yaml
```
**Windows 服务(开机自启、后台运行):** 先填好 `config.yaml`,再**以管理员**运行:
```bat
install-service.bat
```
或:
```bat
dist\mysql-tunnel.exe --install --config config.yaml
```
安装时会把 `config.yaml` **复制到 exe 同目录**(`dist\config.yaml`),服务始终读这份配置,不依赖当前工作目录。改配置后执行:
```bat
dist\mysql-tunnel.exe --restart
```
其它:
```bat
dist\mysql-tunnel.exe --stop
dist\mysql-tunnel.exe --start
dist\mysql-tunnel.exe --uninstall
```
卸载也可用 `uninstall-service.bat`(需管理员)。请先 `--test` 全绿再安装服务。
---
## 配置说明
完整示例见 `config.example.yaml`。
```yaml
listen:
host: 127.0.0.1
port: 13306
local_auth:
username: root
password: "123456"
tunnel:
url: "https://example.com/ntunnel_mysql.php?token=YOUR_TOKEN"
encode_base64: false
tls_insecure_skip_verify: false
remote:
host: "mysql"
port: 3306
username: "root"
password: "YOUR_PASSWORD"
database: "your_database"
charset: "utf8mb4"
options:
request_timeout: 30
max_connections: 5
log_level: info
log_sql: false
log_file: logs/tunnel.log
```
### `listen` — 本机给影刀听的地址
| 字段 | 默认 | 说明 |
|---|---|---|
| `host` | `127.0.0.1` | 只应绑回环。`0.0.0.0` / `::` 会被拒绝。非回环地址会打启动警告。 |
| `port` | `13306` | 影刀里填的端口。可改成未被占用的端口(如 `3309`)。 |
CLI `--host` / `--port` 会覆盖这两项。
### `local_auth` — 影刀连本机时用的账号
这是 **mysql-tunnel 自己校验** 的用户名/密码,**不必** 和远程 MySQL 相同。远程凭据只写在 `remote.*`。
影刀数据源填这里的用户名和密码(与远程 MySQL 账号可以不同)。
### `tunnel` — HTTP 隧道
| 字段 | 说明 |
|---|---|
| `url` | 完整 URL,**必须含** `?token=...`,且与 PHP `$accessToken` 一致。支持 `http` 和 `https`。 |
| `encode_base64` | `true` 时给每条 SQL 做 Base64 并带 `encodeBase64=1`,用于部分 WAF。默认 `false`。 |
| `tls_insecure_skip_verify` | `true` 时跳过 HTTPS 证书校验。仅排障用,启动会打 **SECURITY** 警告。生产保持 `false`。 |
### `remote` — PHP 主机眼里的 MySQL
**最容易配错的一组。** `host` 是 PHP **所在机器** 访问 MySQL 时用的地址,不是你笔记本上的公网 IP。
| 常见部署 | `remote.host` 应填 |
|---|---|
| PHP 与 MySQL 同机 | `127.0.0.1` 或 `localhost` |
| Docker Compose 服务名 `mysql` | `mysql` |
| 内网另一台库 | 那台机在 PHP 所在网段的内网 IP / 主机名 |
| 字段 | 说明 |
|---|---|
| `port` | 远程 MySQL 端口,默认 `3306`。 |
| `username` / `password` | 远程 MySQL 账号。密码可为空(若库允许)。 |
| `database` | 默认库;空字符串则 PHP 不 `select_db`。 |
| `charset` | 本客户端用 `SET NAMES` 补发。PHP 本身仍先 `UTF8`。`utf8` / `utf8mb3` 不再额外发 SET。 |
### `options`
| 字段 | 默认 | 说明 |
|---|---|---|
| `request_timeout` | `30` | 单次 HTTP 超时(秒),须 `> 0`。大查询可加大。 |
| `max_connections` | `5` | 本机并发连接上限。须 `>= 1`。 |
| `log_level` | `info` | `debug` / `info` / `warn` / `error`。 |
| `log_sql` | `false` | `true` 时记录 SQL 文本(仍会脱敏 token/密码)。排障后再关掉。 |
| `log_file` | `logs/tunnel.log` | 同时写 stdout 和该文件。空路径则只打控制台。 |
Windows 启动时会提醒:`config.yaml` 含密钥,请用 NTFS 权限限制为本机账户可读。
---
## 命令行
```bat
mysql-tunnel.exe
mysql-tunnel.exe --gui
mysql-tunnel.exe --gui --config config.yaml
mysql-tunnel.exe --console
mysql-tunnel.exe --config config.yaml
mysql-tunnel.exe --host 127.0.0.1 --port 13306
mysql-tunnel.exe --test
mysql-tunnel.exe --version
mysql-tunnel.exe -h
```
Windows 下无参数(双击)打开配置窗口。`--config` 且未加 `--gui` 时仍走命令行监听。
| 参数 | 说明 |
|---|---|
| `--gui` | 打开配置窗口(编辑 yaml、测试、启动/停止、最小化/托盘)。 |
| `--console` | 前台监听,不打开窗口。 |
| `--config` | 配置文件路径。CLI 默认当前目录 `config.yaml`;GUI 默认 exe 旁。 |
| `--host` | 覆盖 `listen.host`。 |
| `--port` | 覆盖 `listen.port`。 |
| `--test` | POST `actn=C`,再 POST `actn=Q` + `SELECT 1`。不监听端口。 |
| `--version` | 打印版本并退出。 |
| `--install` / `--uninstall` / `--start` / `--stop` / `--restart` | Windows 服务(需管理员安装)。 |
退出码:`0` 成功,`1` 配置/隧道/查询失败,`2` 参数解析错误。
`--test` 分层含义:
| 输出 | 含义 |
|---|---|
| `Tunnel: OK` | HTTP 通了,响应是 Navicat 二进制头(magic=1111)。 |
| `Remote MySQL: OK` | PHP 连上了 `remote.*`。 |
| `Query SELECT 1: OK` | 查询返回一行一列 `"1"`。 |
| `Tunnel: FAIL` | 网络、403、token、HTML 拦截页、非法二进制。 |
| `Remote MySQL: FAIL` | 隧道层正常,但 MySQL 账号/主机/库不对(如 1045)。 |
脚本 version 不是 202/206 时会提示并继续(magic 仍须正确)。
---
## 给影刀配置
**不要** 把影刀的 MySQL 主机填成 HTTP 隧道 URL。影刀只会说 MySQL 协议。
| 影刀字段 | 填什么 |
|---|---|
| 主机 | `127.0.0.1`(与 `listen.host` 一致) |
| 端口 | `13306`(与 `listen.port` 一致) |
| 用户名 | `local_auth.username` |
| 密码 | `local_auth.password` |
| 数据库 | 可选;不填则用配置里的 `remote.database` |
流程:配置窗口「测试连接」全绿(或 `--test`)→ 点「启动」保持运行(可最小化/托盘)→ 影刀再连本机端口。
本机命令行客户端也可:
```bat
mysql -h 127.0.0.1 -P 13306 -u root -p
SELECT 1;
```
---
## 目录结构
```
cmd/tunnel 入口:GUI / --console / --test / Windows 服务
internal/app 测试隧道、启动本机监听(CLI 与 GUI 共用)
internal/gui Windows 配置窗口 + 托盘
internal/config YAML 加载/保存、校验、URL 脱敏
internal/logger 文件日志,脱敏 token / 密码 / Authorization
internal/tunnel HTTP 客户端(TLS、超时、keep-alive)
internal/navicat actn=C / actn=Q、二进制解析、mock 隧道
internal/mysqlserver 本机 MySQL 握手 / 认证 / COM_QUERY / 结果集
internal/winsvc Windows 服务安装与运行
ntunnel_mysql.php 部署到远端的隧道脚本
config.example.yaml 配置模板
docs/PROTOCOL.md 协议对照 PHP 源码的说明
tests/README.md 单测范围说明
build.bat Windows 一键测试 + 编译
```
---
## 自动化测试
```bat
go test ./...
```
包内测试(`internal/.../*_test.go`):
- `config`:YAML、监听绑定规则、URL 脱敏
- `logger`:token / 密码脱敏
- `tunnel`:HTTP 客户端、Navicat magic 头嗅探
- `navicat`:GetBlock / 头解析、`actn=C/Q`、mock `ntunnel_mysql.php`
- `mysqlserver`:握手、native 密码、COM_QUERY/PING/QUIT、ResultSet、与 mock 隧道联调;另用官方 Go MySQL 驱动做兼容性测试
Mock 覆盖:`SELECT 1`、NULL、BIGINT(文本)、DECIMAL(文本)、utf8mb4 中文、SQL errno 1064、DML OK、错误 token(204)。
`--test` 才打真实隧道;单测默认不访问外网。
---
## 常见问题
| 现象 | 原因与处理 |
|---|---|
| `config error: ...` | YAML 缺必填项或非法。对照 `config.example.yaml`。 |
| HTTP 403 文本 `invalid or missing token` | GET 无 token 或 token 错。检查 URL `?token=` 与 `$accessToken`。 |
| 二进制 errno **204** `invalid access token` | POST 带了错误 token(HTTP 往往仍是 200)。 |
| 二进制 errno **202** `invalid parameters` | 缺 `actn` / `host` / `port` / `login`。 |
| MySQL **1045** | `remote.username` / `password` 被远程拒绝。 |
| `Tunnel: FAIL` 且提示 HTML | URL 指到了登录页、WAF 拦截页或错误的 PHP 路径。用浏览器看该 URL(无 token 应 403 纯文本)。 |
| 连得上隧道但连不上库 | `remote.host` 填了公网 IP。应填 PHP 主机访问 MySQL 的地址。 |
| 中文乱码 | 把 `remote.charset` 设为 `utf8mb4`(客户端会发 `SET NAMES utf8mb4`)。 |
| 超时 | 加大 `options.request_timeout`;大 `SELECT` 会占满 PHP 内存。 |
| 事务丢了 | 正常现象:每次 HTTP 都是新连接。事务必须写在同一次请求里(本机多语句映射是后续阶段)。 |
| `listen.host must not be 0.0.0.0` | 禁止对全网卡暴露本机 MySQL 口。保持 `127.0.0.1`。 |
| Go is not on PATH | 安装 [Go 1.24+](https://go.dev/dl/) 后重开终端再跑 `build.bat`。 |
隧道层 errno(PHP 定义,不是 MySQL):
| errno | 含义 |
|---|---|
| 201 | PHP 版本过低 |
| 202 | 参数无效 |
| 203 | 服务器无 MySQL 扩展 |
| 204 | token 无效 |
| 其它 | 一般为 `mysqli` 错误号(1045、1049、1146、1064…) |
---
## 安全
- 默认只监听 `127.0.0.1`。绑定非回环等于把远程库暴露给能连上该端口的人(仅靠 `local_auth`)。
- 日志不写 MySQL 密码、HTTP token、`Authorization`。出错信息也会脱敏。
- 默认校验证书。`tls_insecure_skip_verify: true` 仅临时排障。
- `ntunnel_mysql.php` 里的 `$accessToken` 视为密钥。**仓库里的默认值必须在部署前改掉。** 脚本泄露或曾公开过旧 token 时立刻轮换,并同步改 `config.yaml`。
- 不要提交 `config.yaml`。示例文件里只放占位符。本地 `config.yaml` / `logs/` / `dist/` 已在 `.gitignore`。
- Token 出现在 URL 中,可能进入 Web 访问日志。用 HTTPS,并限制 PHP 主机访问来源。
---
## 能力与限制
当前 `--test` / Navicat 客户端已支持:连接探测、任意 SQL 转发(`SHOW` / `DESC` / `SELECT` / DML 都是 `actn=Q`)。
已知限制(协议如此,不是漏实现):
- 无服务端会话;预处理语句(`COM_STMT_*`)要到更后阶段在本机模拟。
- `insert_id` / `affected_rows` 在 PHP 里是 32 位,超大 AUTO_INCREMENT 会截断。
- BIGINT / DECIMAL 以 **文本** 传递,客户端不解析成 Go 数值。
- 响应无 SQLSTATE;本机侧多数会用 `HY000`。
---
## 开发阶段(已完成)
1. 协议调研 — 见 `docs/PROTOCOL.md`
2. Navicat 客户端 `actn=C` / `actn=Q` + 解析
3. 本机 MySQL 服务(握手、认证、`COM_QUERY`、PING、QUIT)
4. 接到隧道客户端 + ResultSet / DML / `COM_INIT_DB`
5. Windows GUI / 托盘 / 服务安装
6. 构建脚本与端到端测试(`go test` + `tests/`)
已知后续方向:预处理语句本地模拟、跨 HTTP 事务(协议本身不支持)、多语句拆分策略。
实现约定:只说 Navicat 隧道这一套协议——token 放 query、POST `application/x-www-form-urlencoded`、解析 16 字节 `EchoHeader` + `GetBlock`,不发明 JSON API。
## License
MIT — 见 [LICENSE](LICENSE)。
)