一个协议感知的透明 FTP 代理,单文件 Go 程序,无第三方依赖。
场景:机器 A 能自由访问外网并可以部署任意服务;机器 B 只能通过 A 提供的 SOCKS 代理上网,且 B 上的 FTP 客户端软件不支持配置 SOCKS 代理(只能直接填目标服务器地址)。
FTP 协议的被动模式(PASV/EPSV)会在控制连接之外,临时协商一个独立的数据端口进行文件传输。普通的静态端口转发工具(如 gost)只能转发固定端口,无法感知这个动态协商出来的地址,导致控制连接能通、数据连接必然失败(列目录、上传、下载全部失败或卡死)。
ftpproxy 部署在 A 上,作为 B 和真实 FTP 服务器之间的"翻译层":
- 透明转发控制连接的所有命令和响应;
- 拦截服务端返回的
227(PASV)/229(EPSV)响应,把里面"真实服务器的数据端口地址"改写成"A 上一个临时监听端口"; - 当 B 连接这个临时端口时,代理再去连真实服务器的数据端口,双向转发字节流。
B 端软件全程感觉不到任何差异——把 FTP 服务器地址填成 A 的 IP,就像直连一台真实 FTP 服务器一样正常收发数据。
- 仅支持被动模式(PASV/EPSV),不支持主动模式(PORT)。主动模式需要服务端反向连接客户端,这在"B 处于受限网络之后"的场景下结构性不可行。绝大多数现代 FTP 客户端默认就是被动模式,一般无需额外配置;但如果你的客户端固定使用主动模式且无法切换,此工具无法解决。
- 仅处理明文 FTP 控制连接。如果目标服务器要求
AUTH TLS(显式 FTPS),控制连接会在握手后转为加密字节流,代理无法再以文本行方式解析、拦截227/229响应,被动模式地址重写会失效。 - 只做单一目标转发:一个
ftpproxy实例对应一个固定的上游 FTP 服务器(通过-target指定),不做多站点路由。
go mod init ftpproxy # 仓库中如尚无 go.mod,先执行一次
go build -o ftpproxy main.goWindows 下交叉编译:
GOOS=windows GOARCH=amd64 go build -o ftpproxy.exe main.go在 A 上启动,把 -target 指向真实的 FTP 服务器:
./ftpproxy -listen=:2121 -target=真实FTP服务器IP:21B 端 FTP 客户端的服务器地址填 A 的 IP,端口填 2121(或你自定义的监听端口),用户名密码照常填写,无需任何代理相关配置。
如果 B 端软件的自定义端口设置不生效(部分老旧/简化实现的软件存在硬编码默认端口 21 的 bug——参见下方 FAQ),建议直接把
-listen设为:21,并确保 A 上没有其他服务占用该端口。
| 参数 | 默认值 | 说明 |
|---|---|---|
-listen |
:2121 |
本地监听地址(控制端口),B 端软件连接这个地址 |
-target |
无(必填) | 真实 FTP 服务器地址,格式 host:port,例如 1.2.3.4:21 |
-public-ip |
空 | 对外宣告的地址,写入改写后的 PASV/EPSV 响应。留空则自动使用 B 连接 A 时所用的本机地址,一般无需手动指定;仅在 A 有多网卡/NAT 导致自动识别地址不对时才需要覆盖 |
-data-port-min |
0 |
数据端口范围下限,0 表示使用系统随机端口 |
-data-port-max |
0 |
数据端口范围上限,0 表示使用系统随机端口 |
-data-timeout |
30s |
等待 B 端连接数据端口的超时时间,超时未连接则放弃该次数据传输 |
-verbose |
false |
打印完整的 FTP 命令/响应日志(密码自动打码),用于诊断登录、PASV 协商等问题;仅建议排查问题时临时开启(原因见下方"实现说明") |
-data-port-min/-data-port-max 需同时指定或同时不指定,且下限不能大于上限。如果 A 前面还有防火墙/安全组,除控制端口外,也需要放行这段数据端口范围。
日常使用建议不加 -verbose,用最稳的纯字节透传模式常驻运行。可配合任务计划程序、NSSM 或其他服务包装工具设置开机自启和异常重启;如果需要针对 Windows 环境的进程守护脚本(aardio/AHK 实现),可以另行编写。
Q: 客户端软件里设置了自定义端口,但连接始终打到了默认的 21 端口,日志显示登录失败?
这是部分 FTP 客户端软件自身的 bug——UI 上的自定义端口设置只在"测试连接"功能里生效,实际上传/下载逻辑里端口号被硬编码成了 21,未读取用户配置(已在 FastStone Capture 的 FTP 上传功能中实际验证到此行为)。排查方法:临时关闭 A 上占用 21 端口的其他服务,把 ftpproxy 的 -listen 也设为 :21 测试是否恢复正常;如果恢复正常,说明是客户端软件的端口配置缺陷,无法通过代理端修复,只能固定用 21 端口部署,或更换支持正确读取自定义端口的客户端。
Q: 加 -verbose 有什么代价,为什么不干脆一直开着?
非 verbose 模式下,客户端→服务端方向使用 io.Copy 做纯字节透传,来多少数据转发多少,没有任何等待逻辑。verbose 模式为了能打印出完整的命令文本,改为按行读取(必须收到完整的 \r\n 结尾才转发),对绝大多数标准 FTP 客户端没有影响,但对个别拆包发送命令、行尾迟迟不到达的非常规客户端存在理论上的阻塞风险。因此建议默认关闭,只在诊断登录或 PASV 问题时临时开启,看完日志就关掉。
Q: 目标服务器需要 FTPS(AUTH TLS)能用吗?
不能。控制连接一旦升级为 TLS,后续都是加密字节流,代理无法再用正则匹配文本形式的 227/229 响应,被动模式地址改写会失效。这种情况下需要考虑其他方案(如客户端直接支持 SOCKS5 代理配置,或更换支持 FTPS 感知的网关)。
Q: 支持主动模式(PORT)吗?
不支持,也没有计划支持。主动模式要求 FTP 服务端主动连接客户端所在网络,这与"客户端 B 处于受限网络之后,只能发起出站连接"的前提矛盾,任何基于出站代理的方案都无法解决这个方向性问题。
代码只有一个文件 main.go,核心逻辑集中在:
handleControlConn:每条控制连接一个 goroutine,双向转发命令/响应,是拦截改写的入口。handlePassiveResponse/handleExtendedPassiveResponse:分别处理227/229响应的解析与改写。startDataRelay:为每一次 PASV/EPSV 协商临时开一个监听端口,等待客户端连接后再转发到真实数据端口,用完即关闭。
代理只理解 FTP 控制连接里与被动模式相关的极少数响应格式,其余命令/响应一律原样透传,不做任何缓存或篡改。