如何用Nginx实现动态TCP正向代理?解决PHP MySQL请求IP隐藏需求
首先明确:Nginx并非只能实现静态TCP隧道,纯核心的ngx_stream_core_module确实不支持解析URL参数(毕竟它是面向原始TCP流的,不处理HTTP协议层内容),但通过扩展或巧劲,我们完全可以实现你要的动态代理需求。
结合你隐藏PHP MySQL请求IP的场景,我整理了几种可行方案:
方案1:用OpenResty的Lua扩展实现动态TCP代理
如果你能使用OpenResty(Nginx+Lua生态),这是最灵活的方案。Lua脚本可以在TCP连接建立阶段,解析客户端发送的初始数据(比如自定义握手信息),动态指定转发目标。
比如针对你的MySQL场景,可以让PHP先发送一行包含目标信息的自定义前缀,再发送原生MySQL协议数据:
stream { server { listen 8080; content_by_lua_block { -- 读取客户端发送的第一行自定义目标信息 local line, err = ngx.socket.tcp():receiveuntil("\r\n")() if not line then ngx.log(ngx.ERR, "Failed to read destination: ", err) return ngx.exit(444) end -- 解析格式:DEST: example.com:3306 local dest = ngx.re.match(line, "^DEST: (.+)$")[1] if not dest then ngx.log(ngx.ERR, "Invalid destination format") return ngx.exit(444) end -- 建立到目标的连接并转发流量 local sock = ngx.socket.tcp() local ok, err = sock:connect(dest) if not ok then ngx.log(ngx.ERR, "Failed to connect to ", dest, ": ", err) return ngx.exit(444) end -- 双向转发数据 ngx.thread.spawn(function() while true do local data, err = sock:receive(8192) if not data then break end ngx.print(data) ngx.flush() end end) while true do local data, err = ngx.req.socket():receive(8192) if not data then break end sock:send(data) end } } }
对应的PHP代码需要先发送目标信息,再建立MySQL连接:
// 先连接代理并发送目标 $proxySock = fsockopen('proxy_host', 8080); fwrite($proxySock, "DEST: example.com:3306\r\n"); // 将socket传递给mysqli(PHP 7.1+支持) $mysqli = mysqli_init(); $mysqli->set_opt(MYSQLI_OPT_SOCKET, $proxySock); $mysqli->real_connect('', 'username', 'password', 'dbname');
方案2:静态批量配置(适合目标数量有限的场景)
如果你的目标MySQL服务器数量不多,完全可以为每个目标单独配置一个监听端口,简单直接:
stream { server { listen 8081; proxy_pass db1.example.com:3306; } server { listen 8082; proxy_pass db2.example.com:3306; } # 更多目标... }
PHP里只需根据需求连接对应端口:mysqli_connect("proxy_host:8081", $user, $pass, $db);。这个方案无需额外扩展,但灵活性差,目标多了维护成本高。
方案3:使用第三方动态上游模块
如果你不想用Lua,可以尝试第三方模块ngx_stream_upstream_dynamic_module,它允许你通过HTTP接口动态添加/修改上游目标,无需重启Nginx。不过需要你自行编译Nginx并添加该模块,适合需要频繁变更目标的场景。
总结
回到你的问题:Nginx绝对可以实现动态TCP代理,只是核心stream模块做不到,需要借助OpenResty Lua或第三方模块。结合你的PHP MySQL场景,方案1(Lua+自定义握手)是最贴合动态需求的,方案2则适合简单场景。
内容的提问来源于stack exchange,提问作者Viktor Joras

