17 KiB
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。
当前状态
可用(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 都会:
- 校验 URL 上的
?token= mysqli_connect(host, login, password, …)mysqli_set_charset(..., 'UTF8')- 可选
mysqli_select_db actn=C返回服务器信息,或actn=Q执行q[]里的 SQL- 脚本结束,远程 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 后,在仓库根目录:
go mod tidy
go test ./...
build.bat
产物:dist\mysql-tunnel.exe。build.bat 会先跑完全部测试,再以 CGO_ENABLED=0 生成精简二进制。
等价命令:
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 根目录或由反向代理转到该文件。编辑文件顶部:
$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)。窗口可最小化到任务栏;点关闭会藏到托盘,监听可继续。托盘图标双击还原,右键退出。
也可以复制模板后用编辑器改:
copy config.example.yaml config.yaml
不要把 config.yaml 提交进 Git(已在 .gitignore)。密钥只放在 yaml 里,不会写进 exe。
4. 验证隧道
在配置窗口点 测试连接,或在仓库根目录(或把 config.yaml 拷到 exe 同级目录):
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,在窗口里点 启动。可最小化;关窗口进托盘,进程还在。
不要窗口、前台监听(关控制台即退出):
dist\mysql-tunnel.exe --console
dist\mysql-tunnel.exe --config config.yaml
Windows 服务(开机自启、后台运行): 先填好 config.yaml,再以管理员运行:
install-service.bat
或:
dist\mysql-tunnel.exe --install --config config.yaml
安装时会把 config.yaml 复制到 exe 同目录(dist\config.yaml),服务始终读这份配置,不依赖当前工作目录。改配置后执行:
dist\mysql-tunnel.exe --restart
其它:
dist\mysql-tunnel.exe --stop
dist\mysql-tunnel.exe --start
dist\mysql-tunnel.exe --uninstall
卸载也可用 uninstall-service.bat(需管理员)。请先 --test 全绿再安装服务。
配置说明
完整示例见 config.example.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 权限限制为本机账户可读。
命令行
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)→ 点「启动」保持运行(可最小化/托盘)→ 影刀再连本机端口。
本机命令行客户端也可:
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 一键测试 + 编译
自动化测试
go test ./...
包内测试(internal/.../*_test.go):
config:YAML、监听绑定规则、URL 脱敏logger:token / 密码脱敏tunnel:HTTP 客户端、Navicat magic 头嗅探navicat:GetBlock / 头解析、actn=C/Q、mockntunnel_mysql.phpmysqlserver:握手、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+ 后重开终端再跑 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。
开发阶段(已完成)
- 协议调研 — 见
docs/PROTOCOL.md - Navicat 客户端
actn=C/actn=Q+ 解析 - 本机 MySQL 服务(握手、认证、
COM_QUERY、PING、QUIT) - 接到隧道客户端 + ResultSet / DML /
COM_INIT_DB - Windows GUI / 托盘 / 服务安装
- 构建脚本与端到端测试(
go test+tests/)
已知后续方向:预处理语句本地模拟、跨 HTTP 事务(协议本身不支持)、多语句拆分策略。
实现约定:只说 Navicat 隧道这一套协议——token 放 query、POST application/x-www-form-urlencoded、解析 16 字节 EchoHeader + GetBlock,不发明 JSON API。
License
MIT — 见 LICENSE。 )