ftp代理服务器(被动模式)

摘要:

内网FTP。少量端口开启转发。FTP被动模式。

[2019-6-16更新] 在学校内网搞一个内网socks代理。然后电脑端使用proxifier设置host规则即可。关注csdn博客uiop_uiop_uiop。不过手机要想访问的话除非手机的客户端有socks代理选项,不然还是需要下面的脚本支持。

[2020更新] 更优雅的办法,就是直接异地组局域网连进来。方法可以私信我。


引言: 

学校里面有个ftp,常需要在里面下载老师的课件以及上交作业。学校有一个统一的vpn程序,但是测试发现它只转发了少量指定端口,稍稍的扫描了一下,目前发现就转发了:53、80、81、443、1433、8080端口。而抓包分析ftp的被动模式协议之后,发现ftp的被动模式(Passive Mode)的数据传输会在客户端发送FTP命令"PASV" 之后在服务器端主动开放一个端口,服务器再告诉客户端这个端口所在主机的ip地址和端口号(返回的ftp指令样例:227 Entering Passive Mode (192,168,152,132,29,220)\r\n,端口号为后面两个数字的组合:29 * 256 + 220 = 7644),然后客户端再开启一个socket连接去连接服务器的这个端口并进行监听,然后命令socket会再发送一条指令(比如LIST, RETR, STOR),数据就会通过这个新开启的socket连接进行传输。而这个PASV端口每次的数据传输都是由ftp服务器随机指定的(不过这个pasv端口范围好像可以在服务端配置在一个范围里,然后在vpn里面把这个范围的端口的数据也进行转发就可以了。不过端口范围也不能设置的太小,否则在瞬时访问量较大的时候,会出错。不过这个动作蛮大的,也就没打算去和老师说hhh)。

大概思路:

0、首先要有一台内网自己的主机。一个旧的安卓手机就可以。(考虑ip的变化)

1、在vpn转发的仅有的端口当中选择两个,一个作为命令端口,一个作为数据端口(其实数据端口可以做个集合,这样可以提高程序效率),注意bind的时候有问题需要注意,这个稍后再讨论具体细节。

2、使用socketserver模块创建服务端,转发端口的数据。检测response是不是Passive Mode,然后再开启两个线程,一个recv,一个send。分别准备上传下载。

3、因为只有很少的端口可用,目前简单起见我就使用了一个端口作为数据端口,这样也就仅限于自己个人使用。(更好的设计应该是添加全局任务等待队列,再分配其他几个可用的端口。)所以这里涉及到了端口复用的问题。在这里踩了一些坑:

1、sk.close()之后,占用的端口并未释放出来,还能进行sk.send和sk.recv。只有通过sk.shutdown(2)才能关闭掉上传和下载。(0 不能再读,1不能再写,2 读写都禁止)。因为windows判断传输是否完成的标志是端口是否关闭。因为有的文件是空的他就会传长度为0的数据回来。。

2、在shutdown之后想要再次bind原来的端口,windows上直接:

      self.sdc = socket.socket()
      self.sdc.bind(('0.0.0.0',DATA_PORT))
      self.sdc.listen(5)
      self.request, addr = self.sdc.accept()

     并没有什么问题。但是在linux上测试就发现shutdown之后再次bind会报错,Already in use。

    必须要在bind之前加上self.sdc.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

    这是端口复用的关键。

所以之后开发项目的话,得提前把英文原版文档都看一遍才可以。

程序不适合生产环境,只有一个端口作为数据传输。我也就打算个人使用,每次用的时候sshdroid连接上去,使用qpython跑这个脚本。

/data/data/org.qpython.qpy3/files/bin/qpython.sh "/sdcard/qpython/scripts3/FTP_Proxy_Server.py"

也能看log,自己用的方便。之后有时间在回来多加上几个端口以及全局任务队列,面向生产环境在更新一下。ftp是一个非常方便的东西,但是必须要选好加密模式,防止密码和文件被明文在网络上传输。学校单位的ftp在外网连接vpn仍然无法访问的原因正式由于被动模式的原理导致。这是此程序面向的实际问题。补充一下,如果执行之后非正常退出程序可能会导致重新运行python脚本的时候报错说 端口已占用。这时候

netstat -anp | grep [PORT_NUMBER]
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      5200/python
…………


kill 5200

思路比较simple,直接贴代码了。python3

代码:

#coding=utf-8
import socket
import socketserver
import threading
import time

FTP_ADDR = '222.204.216.22'
FTP_CMD_PORT = 21
PROXY_CMD_PORT = 443
DATA_PORT = 1433
BUFSIZE = 4096

class FtpCmdProxy(socketserver.BaseRequestHandler):
	def setup(self):
		self.skcmd = socket.socket()

	def handle(self):#要加timeout等退出条件
		print("[I] - Connection recvd from client.")
		self.skcmd.connect((FTP_ADDR,FTP_CMD_PORT))
		self.request.send(self.skcmd.recv(BUFSIZE))#Serv-U is ready
		try:
			while True:
				cmd = self.request.recv(BUFSIZE)#block until command recvd, and since the command is less then 1.46KB, just recv once
				if len(cmd) == 0:
					raise Exception
				print("[I] - Cmd Recvd : %s"%cmd)
				self.skcmd.send(cmd)
				response = self.skcmd.recv(BUFSIZE)
				print("[I] - Response from FTP svr : %s"%response)
				if 'Entering Passive Mode'.encode('utf-8') in response:
					response = self.handlePASV(response)
					print("[I] - PASV Changed to %s"%response)
				self.request.send(response)
		except:
			print("[W] - cmd client closed")

	def handlePASV(self, response):
		L = response.decode('utf-8').split("(")[-1].split(")")[0].split(",")
		self.pasv_port = int(L[-2])*256 + int(L[-1])
		t1 = threading.Thread(target = self.threadCreateDataProxyListening, args = ())
		t1.start()
		response = '227 Entering Passive Mode (%s,%d,%d)\r\n'%(self.get_host_ip().replace(".",","), DATA_PORT / 256, DATA_PORT % 256)
		return response.encode('utf-8')

	def threadCreateDataProxyListening(self):
		mFtpDataProxy = FtpDataProxy(self.pasv_port, self.skcmd, self.request)
		mFtpDataProxy.handle()
		


	def get_host_ip(self):#注意多网卡的情况,会有问题。一般为默认当前主要上网网卡
		try:
			s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
			s.connect(('8.8.8.8', 80))
			ip = s.getsockname()[0]
		finally:
			s.close()
		return ip


class FtpDataProxy(FtpCmdProxy):
	def __init__(self, pasv_port, sk_cmd_svr, sk_cmd_clt):
		self.pasv_port = pasv_port
		self.sk_cmd_svr = sk_cmd_svr
		self.sk_cmd_clt = sk_cmd_clt
		self.skdata = socket.socket()
		self.sdc = socket.socket()
		self.sdc.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#端口复用关键
		self.skdata.connect((FTP_ADDR,self.pasv_port))
		while True:#make sure to bind successfully
			try:
				self.sdc.bind(('0.0.0.0',DATA_PORT))
			except:
				print("[E] - Bind failed, Maybe Already in use.")
				time.sleep(1)
				continue
			break
		self.sdc.listen(5)
		print("[I] - Proxy binded local port %s."%DATA_PORT)
		
		class TimeOut:
			def __init__(self):
				self.__running = True
			def terminate(self):
				self.__running = False
			def run(self, seconds = 60):
				time.sleep(seconds)
				if self.__running:
					self.request.shutdown(2)
					self.request.close()
					self.skdata.shutdown(2)
					self.skdata.close()
					print("[E] - PASV timed out, connection closed.")
				else:
					print("[I] - TimeOut check closed.")

		o_timeout = TimeOut()
		t_timeout = threading.Thread(target = o_timeout.run, args = (60,))
		t_timeout.start()
		self.request, addr = self.sdc.accept()
		print("[I] - Data Connection Recvd!")
		o_timeout.terminate()

	def handle(self):
		print("[I] - Handleing Connection of PASV.")
		
		self.tdown = threading.Thread(target = self.downloadHandle, args = ())
		self.tdown.start()
		self.tup = threading.Thread(target = self.uploadHandle, args = ())
		self.tup.start()

		while self.tdown.isAlive() and self.tdown.isAlive():
			time.sleep(0.1)

		print("[I] - PASV finished.")
		response = self.sk_cmd_svr.recv(BUFSIZE)
		print(response)
		self.sk_cmd_clt.send(response)
		try:
			self.skdata.shutdown(2)#SHUT_RDWR 接收发送通道都关闭。
			self.skdata.close()
			self.request.shutdown(2)
			self.request.close()
		except:
			print("[W] - shutdown socket failed, maybe already shut.")
	
	def downloadHandle(self):
		try:
			nxt = self.skdata.recv(BUFSIZE)
			print("[I] - Len : %s"%len(nxt))
			self.request.send(nxt)
			while len(nxt) != 0:
				nxt = self.skdata.recv(BUFSIZE)
				if len(nxt) == 0:
					break
				self.request.send(nxt)
		except:
			print("[W] - Download Aborted")

	def uploadHandle(self):
		try:
			nxt = self.request.recv(BUFSIZE)
			print("[I] - Len : %s"%len(nxt))
			self.skdata.send(nxt)
			while len(nxt) != 0:
				nxt = self.request.recv(BUFSIZE)
				self.skdata.send(nxt)
			print("#####################")
			#主动关闭端口,这是文件传输完毕的代表
			self.skdata.shutdown(2)#SHUT_RDWR 接收发送通道都关闭。
			self.skdata.close()
			self.request.shutdown(2)
			self.request.close()
		except:
			print("[W] - Upload Aborted.")

svr = socketserver.ThreadingTCPServer(('0.0.0.0',PROXY_CMD_PORT),FtpCmdProxy)
svr.serve_forever()

 

Guess you like

Origin blog.csdn.net/uiop_uiop_uiop/article/details/86665218