Repository navigation
Expand file tree
/
Copy pathmain.go
More file actions
executable file
·270 lines (238 loc) · 8.92 KB
/
Copy pathmain.go
File metadata and controls
executable file
·270 lines (238 loc) · 8.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// ftpproxy: 一个协议感知的透明FTP代理
//
// 用途:部署在能自由访问外网的机器(A)上,B端FTP客户端把A当作
// "FTP服务器"直接连接,本代理会:
// 1. 透明转发控制连接(FTP命令/响应)到真实的目标FTP服务器
// 2. 拦截服务端返回的 PASV(227) / EPSV(229) 响应,把里面的
// "真实服务器数据端口地址" 替换成 "A自己的一个临时端口"
// 3. 当客户端连接这个临时端口时,代理再去连真实服务器的
// 数据端口,双向转发字节流
//
// 这样B端软件完全不需要感知SOCKS代理或做任何特殊配置,
// 只要能连上A,就能像连真实FTP服务器一样正常收发数据(仅支持被动模式)。
package main
import (
"bufio"
"flag"
"fmt"
"io"
"log"
"net"
"regexp"
"strconv"
"strings"
"sync/atomic"
"time"
)
var (
listenAddr = flag.String("listen", ":2121", "本地监听地址(控制端口),B端软件连接这个地址")
targetAddr = flag.String("target", "", "真实FTP服务器地址,格式 host:port,例如 1.2.3.4:21")
publicIP = flag.String("public-ip", "", "对外宣告的地址(写入PASV/EPSV响应)。为空则自动使用B端连接过来的本机地址")
dataPortLow = flag.Int("data-port-min", 0, "数据端口范围下限,0表示使用系统随机端口")
dataPortHigh = flag.Int("data-port-max", 0, "数据端口范围上限,0表示使用系统随机端口")
dataTimeout = flag.Duration("data-timeout", 30*time.Second, "等待客户端连接数据端口的超时时间")
verbose = flag.Bool("verbose", false, "打印完整的FTP命令/响应日志(密码会被打码),用于诊断登录/数据连接问题")
)
// maskCommand 对USER/PASS命令的参数做打码处理,避免明文密码写入日志
func maskCommand(line string) string {
trimmed := strings.TrimRight(line, "\r\n")
upper := strings.ToUpper(trimmed)
if strings.HasPrefix(upper, "PASS ") {
return "PASS ****"
}
if strings.HasPrefix(upper, "USER ") {
return trimmed // 用户名一般不算敏感信息,保留方便核对
}
return trimmed
}
var (
pasvRe = regexp.MustCompile(`\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)`)
epsvRe = regexp.MustCompile(`\(\|\|\|(\d+)\|\)`)
)
// 用于在给定端口范围内轮询分配,避免每次都从头试起
var portCursor int32
func main() {
flag.Parse()
if *targetAddr == "" {
log.Fatal("必须通过 -target 指定真实FTP服务器地址,例如 -target=1.2.3.4:21")
}
if (*dataPortLow == 0) != (*dataPortHigh == 0) {
log.Fatal("-data-port-min 和 -data-port-max 必须同时指定或同时不指定")
}
if *dataPortLow > 0 && *dataPortLow > *dataPortHigh {
log.Fatal("-data-port-min 不能大于 -data-port-max")
}
ln, err := net.Listen("tcp", *listenAddr)
if err != nil {
log.Fatalf("监听 %s 失败: %v", *listenAddr, err)
}
log.Printf("FTP透明代理已启动: 监听 %s -> 转发至 %s", *listenAddr, *targetAddr)
for {
client, err := ln.Accept()
if err != nil {
log.Printf("accept错误: %v", err)
continue
}
go handleControlConn(client)
}
}
// handleControlConn 处理一条客户端到代理的控制连接
func handleControlConn(client net.Conn) {
defer client.Close()
server, err := net.Dial("tcp", *targetAddr)
if err != nil {
log.Printf("连接目标服务器 %s 失败: %v", *targetAddr, err)
return
}
defer server.Close()
clientAddr := client.RemoteAddr().String()
log.Printf("[%s] 新连接", clientAddr)
// 确定对外宣告的IP: 优先用户指定,否则用客户端连接到A时使用的本地地址
announceIP := *publicIP
if announceIP == "" {
if host, _, err := net.SplitHostPort(client.LocalAddr().String()); err == nil {
announceIP = host
}
}
// 客户端 -> 服务端:
// - 默认(非verbose): 用 io.Copy 做纯字节透传,不关心命令是否成行,
// 不会因为等待换行符而产生任何阻塞风险,是最稳的转发方式。
// - 开启 -verbose: 改为按行读取,以便打印命令日志(密码打码),
// 代价是必须读到完整一行才转发,对畸形/非常规客户端存在
// 理论上的阻塞风险,仅建议诊断时临时开启。
go func() {
defer func() {
if tc, ok := server.(*net.TCPConn); ok {
tc.CloseWrite()
}
}()
if *verbose {
cReader := bufio.NewReader(client)
for {
line, err := cReader.ReadString('\n')
if len(line) > 0 {
log.Printf("[%s] C->S: %s", clientAddr, maskCommand(line))
if _, werr := server.Write([]byte(line)); werr != nil {
return
}
}
if err != nil {
return
}
}
}
io.Copy(server, client)
}()
// 服务端 -> 客户端: 逐行读取,拦截227/229响应并重写
reader := bufio.NewReader(server)
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
out := line
if m := pasvRe.FindStringSubmatch(line); m != nil && strings.Contains(line, "227") {
if rewritten, ok := handlePassiveResponse(line, m, announceIP, clientAddr); ok {
out = rewritten
}
} else if m := epsvRe.FindStringSubmatch(line); m != nil && strings.Contains(line, "229") {
if rewritten, ok := handleExtendedPassiveResponse(line, m, announceIP, clientAddr); ok {
out = rewritten
}
}
if *verbose {
log.Printf("[%s] S->C: %s", clientAddr, strings.TrimRight(out, "\r\n"))
}
if _, werr := client.Write([]byte(out)); werr != nil {
break
}
}
if err != nil {
break
}
}
log.Printf("[%s] 连接结束", clientAddr)
}
// handlePassiveResponse 处理形如
// 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
// 的响应,返回重写后的整行文本
func handlePassiveResponse(line string, m []string, announceIP, clientAddr string) (string, bool) {
realIP := fmt.Sprintf("%s.%s.%s.%s", m[1], m[2], m[3], m[4])
p1, _ := strconv.Atoi(m[5])
p2, _ := strconv.Atoi(m[6])
realPort := p1*256 + p2
realDataAddr := net.JoinHostPort(realIP, strconv.Itoa(realPort))
localPort, err := startDataRelay(realDataAddr, clientAddr)
if err != nil {
log.Printf("[%s] 打开数据中转端口失败: %v", clientAddr, err)
return line, false
}
ipParts := strings.ReplaceAll(announceIP, ".", ",")
newLine := fmt.Sprintf("227 Entering Passive Mode (%s,%d,%d).\r\n",
ipParts, localPort/256, localPort%256)
return newLine, true
}
// handleExtendedPassiveResponse 处理形如
// 229 Entering Extended Passive Mode (|||port|)
// 的响应。EPSV不携带IP,隐含"和控制连接同一目标服务器"
func handleExtendedPassiveResponse(line string, m []string, announceIP, clientAddr string) (string, bool) {
realPort, _ := strconv.Atoi(m[1])
targetHost, _, _ := net.SplitHostPort(*targetAddr)
realDataAddr := net.JoinHostPort(targetHost, strconv.Itoa(realPort))
localPort, err := startDataRelay(realDataAddr, clientAddr)
if err != nil {
log.Printf("[%s] 打开数据中转端口失败: %v", clientAddr, err)
return line, false
}
newLine := fmt.Sprintf("229 Entering Extended Passive Mode (|||%d|)\r\n", localPort)
return newLine, true
}
// startDataRelay 在A上开一个临时监听端口,等待客户端(B)连接进来,
// 然后连接真实服务器的数据地址,做双向字节转发。
// 返回分配到的本地端口号。
func startDataRelay(realDataAddr, clientAddr string) (int, error) {
ln, err := listenDataPort()
if err != nil {
return 0, err
}
port := ln.Addr().(*net.TCPAddr).Port
go func() {
defer ln.Close()
ln.(*net.TCPListener).SetDeadline(time.Now().Add(*dataTimeout))
conn1, err := ln.Accept()
if err != nil {
log.Printf("[%s] 等待数据连接超时或失败: %v", clientAddr, err)
return
}
defer conn1.Close()
conn2, err := net.Dial("tcp", realDataAddr)
if err != nil {
log.Printf("[%s] 连接真实数据端口 %s 失败: %v", clientAddr, realDataAddr, err)
return
}
defer conn2.Close()
log.Printf("[%s] 数据通道建立: 本地:%d <-> %s", clientAddr, port, realDataAddr)
done := make(chan struct{}, 2)
go func() { io.Copy(conn2, conn1); done <- struct{}{} }()
go func() { io.Copy(conn1, conn2); done <- struct{}{} }()
<-done
}()
return port, nil
}
// listenDataPort 根据配置的端口范围分配一个监听端口;
// 未配置范围时使用系统随机端口(:0)
func listenDataPort() (net.Listener, error) {
if *dataPortLow == 0 {
return net.Listen("tcp", ":0")
}
rangeSize := *dataPortHigh - *dataPortLow + 1
start := int(atomic.AddInt32(&portCursor, 1)) % rangeSize
var lastErr error
for i := 0; i < rangeSize; i++ {
port := *dataPortLow + (start+i)%rangeSize
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err == nil {
return ln, nil
}
lastErr = err
}
return nil, fmt.Errorf("端口范围 %d-%d 内没有可用端口: %v", *dataPortLow, *dataPortHigh, lastErr)
}