# 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)。 )