pyftpdlib搭建的FTP服务器无法从公网访问的问题排查
问题场景
使用pyftpdlib搭建FTP服务器并运行在Docker容器中,本地访问完全正常,但通过公网IP连接时触发ConnectionRefusedError,且服务器端无任何日志记录。
服务器代码
from pyftpdlib import servers, handlers address = ("0.0.0.0", 21) server = servers.FTPServer(address, handlers.FTPHandler) server.banner = "Welcome to My FTP Server" handler = server.handler handler.authorizer.add_user("username", "password", ".", perm="elr") handler.authorizer.add_anonymous(".", perm="elr") handler.masquerade_address = '97.117.28.178' # from `curl ifconfig.me` handler.passive_ports = range(60000, 60100) server.serve_forever()
Docker启动命令
docker run --rm -it -p 20:20 -p 21:21 -p 60000-60100:60000-60100 python:slim bash
客户端代码
import os from ftplib import FTP class FTPClient: def __init__(self, host): self.host = host def _connect(self): self.ftp = FTP(self.host) self.ftp.set_pasv(True) self.ftp.login('username', 'password') def _disconnect(self): ''' Disconnect from the FTP server ''' self.ftp.quit() def connect(self): self.ftp = FTP(self.host) self.ftp.login('username', 'password') @staticmethod def manageConnection(func): def wrapper(self, *args, **kwargs): self._connect() result = func(self, *args, **kwargs) self._disconnect() return result return wrapper @manageConnection def pull(self, filename: str, path: str = None, local: str = '.') -> bool: if path is not None: self.ftp.cwd(path) if filename in self.ftp.nlst(): local_path = os.path.join(local, filename) with open(local_path, 'wb') as file: self.ftp.retrbinary('RETR ' + filename, file.write) return True return False @manageConnection def view(self, path: str = None) -> bool: if path is not None: self.ftp.cwd(path) for file in self.ftp.nlst(): print(file) return False
本地访问验证(正常)
f = FTPClient('127.0.0.1') f.connect() # works - I see logs on server f.pull('utils.py') # works - downloads files
公网访问报错
f = FTPClient('97.117.28.178') f.connect() # doesn't work - no log entries on server, it doesn't even reach it
Traceback (most recent call last):
File "", line 1, in
File "", line 13, in wrapper
File "", line 5, in _connect
File "/usr/local/lib/python3.11/ftplib.py", line 121, in init
self.connect(host)
File "/usr/local/lib/python3.11/ftplib.py", line 158, in connect
self.sock = socket.create_connection((self.host, self.port), self.timeout,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/socket.py", line 851, in create_connection
raise exceptions[0]
File "/usr/local/lib/python3.11/socket.py", line 836, in create_connection
sock.connect(sa)
ConnectionRefusedError: [Errno 111] Connection refused
## 问题原因 请求未到达服务器是核心问题,具体原因分为以下几点: - **端口未放行**:宿主机防火墙、云服务商安全组未开放FTP所需的21(控制端口)、20(主动模式数据端口)、60000-60100(被动模式端口),公网请求被直接拦截。 - **NAT穿透问题**:如果是家庭网络,公网IP是运营商NAT后的地址,未在路由器配置端口转发,导致请求无法穿透到宿主机。 - **端口映射失效**:Docker端口映射命令未生效,宿主机未监听对应端口,请求无法转发到容器内的FTP服务。 ## 解决方法 ### 1. 放行所有必要端口 - **云服务器**:登录云控制台,在安全组添加入站规则,开放`21/TCP`、`20/TCP`、`60000-60100/TCP`,允许所有IP(0.0.0.0/0)访问。 - **本地宿主机**: - Ubuntu/Debian(ufw): ```bash sudo ufw allow 20/tcp sudo ufw allow 21/tcp sudo ufw allow 60000:60100/tcp sudo ufw reload ``` - CentOS/RHEL(firewalld): ```bash sudo firewall-cmd --add-port=20/tcp --permanent sudo firewall-cmd --add-port=21/tcp --permanent sudo firewall-cmd --add-port=60000-60100/tcp --permanent sudo firewall-cmd --reload ``` ### 2. 验证Docker端口映射 在宿主机执行命令,检查21端口是否被Docker监听: ```bash netstat -tulpn | grep :21
若输出包含docker-proxy则映射正常,否则重新启动容器确保端口映射命令正确。
3. 家庭网络配置端口转发
在路由器后台配置端口转发规则,将路由器的21、20、60000-60100端口转发到宿主机的对应端口;若运营商封禁21端口,可更换非标准端口(如2121)。
4. 确认被动模式配置
确保masquerade_address设置为宿主机的公网IP,且被动端口范围60000-60100已在所有环节放行,避免后续数据传输失败。
内容的提问来源于stack exchange,提问作者MetaStack

