first commit

This commit is contained in:
Yangxt
2026-09-21 15:59:34 +08:00
commit d3c2189bf5
+467
View File
@@ -0,0 +1,467 @@
# 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)。
)