Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ftpproxy

一个协议感知的透明 FTP 代理,单文件 Go 程序,无第三方依赖。

解决什么问题

场景:机器 A 能自由访问外网并可以部署任意服务;机器 B 只能通过 A 提供的 SOCKS 代理上网,且 B 上的 FTP 客户端软件不支持配置 SOCKS 代理(只能直接填目标服务器地址)。

FTP 协议的被动模式(PASV/EPSV)会在控制连接之外,临时协商一个独立的数据端口进行文件传输。普通的静态端口转发工具(如 gost)只能转发固定端口,无法感知这个动态协商出来的地址,导致控制连接能通、数据连接必然失败(列目录、上传、下载全部失败或卡死)。

ftpproxy 部署在 A 上,作为 B 和真实 FTP 服务器之间的"翻译层":

  1. 透明转发控制连接的所有命令和响应;
  2. 拦截服务端返回的 227(PASV)/ 229(EPSV)响应,把里面"真实服务器的数据端口地址"改写成"A 上一个临时监听端口";
  3. 当 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.go

Windows 下交叉编译:

GOOS=windows GOARCH=amd64 go build -o ftpproxy.exe main.go

运行

在 A 上启动,把 -target 指向真实的 FTP 服务器:

./ftpproxy -listen=:2121 -target=真实FTP服务器IP:21

B 端 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 前面还有防火墙/安全组,除控制端口外,也需要放行这段数据端口范围。

部署为 Windows 服务(长期运行)

日常使用建议不加 -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 控制连接里与被动模式相关的极少数响应格式,其余命令/响应一律原样透传,不做任何缓存或篡改。

About

一个Go开发的协议感知的透明 FTP 代理小工具,单文件,无依赖(自动协商数据传输的动态端口,弥补gost等工具只能转发静态端口,无法代理FTP协议的问题)。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages