commit 9cee40db866da58da882b851d7c1b12f15faa36a
Author: GVSADS
Date: Thu Jan 29 22:53:58 2026 +0800
Initial commit of PyFrp project
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..cda77d5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Galaxy Vastar Software Studio
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..1bd58bc
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,2 @@
+from .client import PortForwardClient
+from .server import PortForwardServer
diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..aa5d030
Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ
diff --git a/__pycache__/client.cpython-312.pyc b/__pycache__/client.cpython-312.pyc
new file mode 100644
index 0000000..28a695d
Binary files /dev/null and b/__pycache__/client.cpython-312.pyc differ
diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc
new file mode 100644
index 0000000..caaad1e
Binary files /dev/null and b/__pycache__/server.cpython-312.pyc differ
diff --git a/backup/__init__.py b/backup/__init__.py
new file mode 100644
index 0000000..bf6328b
--- /dev/null
+++ b/backup/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Brief Introduction
+# The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size,
+# while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features.
+
+from . import client
+from . import server
diff --git a/backup/client.py b/backup/client.py
new file mode 100644
index 0000000..c00535a
--- /dev/null
+++ b/backup/client.py
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+# Brief Introduction
+# The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size,
+# while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features.
+
+import socket
+import threading
+import json
+import traceback
+import time
+from Crypto.Cipher import AES
+from Crypto.Util.Padding import pad, unpad
+import base64
+import sys
+import os
+
+class AesEncryptor:
+ def __init__(self, Key):
+ self.Key = Key.ljust(32)[:32].encode('utf-8')
+ self.Mode = AES.MODE_CBC
+ def Encrypt(self, Data):
+ Iv = b'0123456789abcdef'
+ Cipher = AES.new(self.Key, self.Mode, Iv)
+ return base64.b64encode(Iv + Cipher.encrypt(pad(Data.encode('utf-8'), AES.block_size))).decode('utf-8')
+ def Decrypt(self, Data):
+ Data = base64.b64decode(Data)
+ Iv = Data[:16]
+ Cipher = AES.new(self.Key, self.Mode, Iv)
+ return unpad(Cipher.decrypt(Data[16:]), AES.block_size).decode('utf-8')
+
+class PortMapping:
+ def __init__(self, ForwardHost, ForwardPort, TargetPort, Mode):
+ self.ForwardHost = ForwardHost
+ self.ForwardPort = ForwardPort
+ self.TargetPort = TargetPort
+ self.Mode = Mode.lower()
+ self.IsActive = False
+ def ToDict(self):
+ return {
+ 'forward_host': self.ForwardHost,
+ 'forward_port': self.ForwardPort,
+ 'target_port': self.TargetPort,
+ 'mode': self.Mode
+ }
+
+class PortForwardClient:
+ def __init__(self, ServerHost="127.0.0.1", ServerPort=5000, Key="07A36AEF1907843"):
+ self.ServerHost = ServerHost
+ self.ServerPort = ServerPort
+ self.Key = Key
+ self.Socket = None
+ self.IsRunning = False
+ self.Mappings = []
+ self.MappingsLock = threading.Lock()
+ self.Encryptor = AesEncryptor(Key)
+ def Log(self, Message):
+ Timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
+ print(f"[{Timestamp}] [Client] {Message}")
+ def HandleError(self, Message):
+ self.Log(f"Error: {Message}")
+ self.Log(traceback.format_exc())
+ def Connect(self):
+ try:
+ self.Socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.Socket.connect((self.ServerHost, self.ServerPort))
+ self.Log(f"Connected to server {self.ServerHost}:{self.ServerPort}")
+ return True
+ except Exception as E:
+ self.HandleError(f"Connection failed: {str(E)}")
+ self.Socket = None
+ return False
+ def AddMapping(self, ForwardHost, ForwardPort, TargetPort, Mode):
+ with self.MappingsLock:
+ for M in self.Mappings:
+ if M.TargetPort == TargetPort:
+ self.Log(f"Mapping for target port {TargetPort} already exists")
+ return False
+ Mapping = PortMapping(ForwardHost, ForwardPort, TargetPort, Mode)
+ self.Mappings.append(Mapping)
+ # 仅当已连接时才立即注册映射
+ if self.Socket is not None:
+ return self.RegisterMapping(Mapping)
+ return True # 连接后会自动注册
+ def RegisterMapping(self, Mapping):
+ if self.Socket is None:
+ self.Log("Cannot register mapping - not connected to server")
+ return False
+ try:
+ Command = {
+ 'type': 'register',
+ 'timestamp': str(time.time()),
+ **Mapping.ToDict()
+ }
+ EncryptedData = self.Encryptor.Encrypt(json.dumps(Command))
+ self.Socket.sendall((EncryptedData + '\n').encode('utf-8'))
+ ResponseData = self.Socket.recv(4096).decode('utf-8').strip()
+ Response = json.loads(self.Encryptor.Decrypt(ResponseData))
+ if Response.get('status') == 'success':
+ self.Log(f"Successfully registered mapping: {Mapping.TargetPort} -> {Mapping.ForwardHost}:{Mapping.ForwardPort} ({Mapping.Mode})")
+ Mapping.IsActive = True
+ return True
+ else:
+ self.Log(f"Failed to register mapping: {Response.get('message', 'Unknown error')}")
+ return False
+ except Exception as E:
+ self.HandleError(f"Error registering mapping: {str(E)}")
+ return False
+ def RemoveMapping(self, TargetPort):
+ with self.MappingsLock:
+ for I, M in enumerate(self.Mappings):
+ if M.TargetPort == TargetPort:
+ if self.UnregisterMapping(M):
+ del self.Mappings[I]
+ return True
+ return False
+ self.Log(f"No mapping found for target port {TargetPort}")
+ return False
+ def UnregisterMapping(self, Mapping):
+ try:
+ Command = {
+ 'type': 'unregister',
+ 'timestamp': str(time.time()),
+ 'target_port': Mapping.TargetPort
+ }
+ EncryptedData = self.Encryptor.Encrypt(json.dumps(Command))
+ self.Socket.sendall((EncryptedData + '\n').encode('utf-8'))
+ ResponseData = self.Socket.recv(4096).decode('utf-8').strip()
+ Response = json.loads(self.Encryptor.Decrypt(ResponseData))
+ if Response.get('status') == 'success':
+ self.Log(f"Successfully unregistered mapping: {Mapping.TargetPort}")
+ Mapping.IsActive = False
+ return True
+ else:
+ self.Log(f"Failed to unregister mapping: {Response.get('message', 'Unknown error')}")
+ return False
+ except Exception as E:
+ self.HandleError(f"Error unregistering mapping: {str(E)}")
+ return False
+ def Start(self):
+ if not self.Connect():
+ return
+ self.IsRunning = True
+ self.Log("Client started")
+ try:
+ # 连接成功后注册所有映射
+ with self.MappingsLock:
+ for Mapping in self.Mappings:
+ self.RegisterMapping(Mapping)
+ while self.IsRunning:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ self.Log("Client interrupted")
+ except Exception as E:
+ self.HandleError(f"Client error: {str(E)}")
+ finally:
+ self.Stop()
+ def Stop(self):
+ self.IsRunning = False
+ with self.MappingsLock:
+ for Mapping in self.Mappings:
+ if Mapping.IsActive:
+ self.UnregisterMapping(Mapping)
+ self.Mappings.clear()
+ if self.Socket:
+ try:
+ self.Socket.close()
+ except:
+ pass
+ self.Log("Client stopped")
+
+
+if __name__ == "__main__":
+ Config = {
+ "server_host": "127.0.0.1",
+ "server_port": 5000,
+ "key": "07A36AEF1907843",
+ "mappings": [
+ {
+ "forward_host": "127.0.0.1",
+ "forward_port": 5902,
+ "target_port": 5500,
+ "mode": "tcp"
+ }
+ ]
+ }
+ if len(sys.argv) > 1:
+ try:
+ with open(sys.argv[1], 'r') as F:
+ Config.update(json.load(F))
+ except Exception as E:
+ print(f"Error loading config file: {str(E)}")
+ os._exit(0)
+ Client = PortForwardClient(
+ ServerHost=Config["server_host"],
+ ServerPort=int(Config["server_port"]),
+ Key=Config["key"]
+ )
+ for Mapping in Config["mappings"]:
+ Client.AddMapping(
+ ForwardHost=Mapping["forward_host"],
+ ForwardPort=int(Mapping["forward_port"]),
+ TargetPort=int(Mapping["target_port"]),
+ Mode=Mapping["mode"]
+ )
+ try:
+ Client.Start()
+ except KeyboardInterrupt:
+ Client.Stop()
\ No newline at end of file
diff --git a/backup/readme.md b/backup/readme.md
new file mode 100644
index 0000000..2d4ac85
--- /dev/null
+++ b/backup/readme.md
@@ -0,0 +1,181 @@
+
+
+
PyFrp
+ Frp (Fast Reverse Proxy) implemented in Python
+
+
+
+
+
+---
+
+
+
+
+
+
+---
+> **This project is only in its early stages, and the documentation is currently incomplete. Thank you for using this project. If possible, please give it a star ♥**
+
+# PyFrp
+
+## 🗡 Brief Introduction
+The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size, while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features.
+
+### Comparison with the Original Frp
+| Feature| PyFrp | Frp|
+|:---------------:|:-----:|:----:|
+| Quick Setup| ✅| ✅|
+| Size| ✅| ❌|
+| SSL Support| ❌| ✅|
+| Embeddable| ✅| ✅❌ |
+| Documentation| ❌| ✅|
+
+Choose based on your needs:
+- If you prioritize **quick setup, small size, simplicity, and embeddability** (e.g., integrating into your Python project), PyFrp is a great choice.
+- For **production deployments**, Frp remains the better option for now.
+
+In the future, we aim to enhance PyFrp's functionality, add more configuration options, and potentially support additional protocols (e.g., HTTP/HTTPS).
+
+We’re just getting started and would greatly appreciate your support—**a ⭐ Star** would mean a lot to us!
+
+If needed, we could also develop a **C-based implementation** of Frp.
+
+> Note: **HTTPS is not yet supported**. We may add this later, and contributions are welcome!
+
+---
+
+## 🚀 Quick Start
+
+The only third-party library you need is `pycryptodome`. Install it with:
+```bash
+pip install pycryptodome
+```
+We use its encryption algorithms to secure your data. While any version of `pycryptodome` should work, we recommend **v3.22.0** if you encounter issues:
+```bash
+pip install pycryptodome==3.22.0
+```
+For a **non-encrypted version**, simply remove all encryption-related code.
+
+### Configuration
+#### Server (`server_config.json`):
+```python
+{
+ "internal_data_port": 5000, // PyFrp server data port
+ "allowed_port_range": "5001-5500", // Allowed port range
+ "max_ports_per_client": 5, // Max ports per client
+ "key": "07A36AEF1907843" // Encryption key
+}
+```
+
+#### Client (`client_config.json`):
+```python
+{
+ "server_host": "127.0.0.1", // PyFrp server address
+ "server_port": 5000, // PyFrp server port
+ "key": "07A36AEF1907843", // Encryption key
+ "mappings": [ // Port mappings
+ {
+ "forward_host": "127.0.0.1", // Local host
+ "forward_port": 5902, // Local port
+ "target_port": 5500, // Target port
+ "mode": "tcp" // Protocol (TCP/UDP)
+ }
+ // Add more mappings as needed
+ ]
+}
+```
+
+Run with custom configs:
+```bash
+python server.py server_config.json
+python client.py client_config.json
+```
+
+---
+
+## 📞 Contact Us
+ - 📧 Email: wyt18222152539wyt@163.com
+ - 🌐 Website: [Galaxy Vastar Software Studio](https://www.gvsds.com)
+ - 📱 WeChat: GVSADS
+
+---
+
+## 🗡 简单介绍
+原本的 Frp https://github.com/Fatedier/frp/ 体积略显臃肿,而 PyFrp 则是一个基于 Python 实现的简单版本,体积更小,功能也更简单。
+
+我们 和 原版 Frp 相比
+| 功能 | PyFrp | Frp |
+|:-:|:-:|:-:|
+| 快速配置 | ✅ | ✅ |
+| 体积 | ✅ | ❌ |
+| SSL 功能 | ❌ | ✅ |
+| 嵌入式 | ✅ | ✅❌ |
+| 文档 | ❌ | ✅ |
+
+请您根据您的需要选择使用哪个,
+- 如果您需要快速配置,体积小,功能简单,嵌入式,集成到您自己的 Python 项目中,那么 PyFrp 是一个不错的选择。
+- 如果您需要部署在生产环境中,暂时不要选择 PyFrp,Frp 是一个更好的选择。
+
+日后,我们以 Frp 为目标,完善其功能,添加更多的配置项,同时也会考虑添加更多的协议支持,如 http、https 等。
+
+我们其实也才起步,仍然需要您的支持,如果可以,还请您点一个 Star ⭐,这将是对我们最大的支持。
+
+如果您有需要,我们可以设计一版 C 实现的 Frp。
+
+> 注意,我们暂时还不支持 https 协议。稍后如果有时间,我们可能会考虑支持,如果您已经帮我们支持,随时欢迎您提交。
+
+## 🚀 快速开始
+
+你唯一需要下载的第三方库是 `pycryptodome`,使用以下命令安装:
+```bash
+pip install pycryptodome
+```
+我们使用它的加密算法来保护你的数据。我们可知的是任意版本的 `pycryptodome` 都可以使用。但如果出现加密算法的报错,我们建议你使用最新版本的 `pycryptodome`,或者使用我们所使用的 3.22.0 版本,即:
+```bash
+pip install pycryptodome==3.22.0
+```
+如果你期望使用一个不带加密的版本,你可以将所有涉及加密的部分全部删掉。
+
+
+
+然后,你可以修改源代码中的个人配置
+
+server端配置修改:
+```python
+{
+ "internal_data_port": 5000, // PyFrp 服务器端数据端口
+ "allowed_port_range": "5001-5500", // 允许的端口范围
+ "max_ports_per_client": 5, // 每个客户端最大端口数
+ "key": "07A36AEF1907843" // 加密密钥
+}
+```
+
+client
+```python
+{
+ "server_host": "127.0.0.1", // PyFrp 服务器端主机地址
+ "server_port": 5000, // PyFrp 服务器端端口
+ "key": "07A36AEF1907843", // 加密密钥
+ "mappings": [ // 端口映射配置
+ {
+ "forward_host": "127.0.0.1", // 本地主机地址
+ "forward_port": 5902, // 本地端口
+ "target_port": 5500, // 目标端口
+ "mode": "tcp" // 传输模式
+ },
+ // 你可以在这里输入更多的端口映射配置
+ ]
+}
+```
+
+允许你直接修改代码中尾部的 Config 变量来实现自定义配置。或者指定配置文件位置。
+```bash
+python server.py server_config.json
+python client.py client_config.json
+```
+
+## 📞 联系我们
+ - 📧 Email: wyt18222152539wyt@163.com
+ - 🌐 官网: [银河万通软件开发工作室](https://www.gvsds.com)
+ - 📱 微信: GVSADS
diff --git a/backup/server.py b/backup/server.py
new file mode 100644
index 0000000..194f478
--- /dev/null
+++ b/backup/server.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+# Brief Introduction
+# The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size,
+# while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features.
+
+import socket
+import threading
+import json
+import traceback
+import time
+from Crypto.Cipher import AES
+from Crypto.Util.Padding import pad, unpad
+import base64
+import re
+import sys
+import os
+
+class AesEncryptor:
+ def __init__(self, Key):
+ self.Key = Key.ljust(32)[:32].encode('utf-8')
+ self.Mode = AES.MODE_CBC
+ def Encrypt(self, Data):
+ Iv = b'0123456789abcdef'
+ Cipher = AES.new(self.Key, self.Mode, Iv)
+ return base64.b64encode(Iv + Cipher.encrypt(pad(Data.encode('utf-8'), AES.block_size))).decode('utf-8')
+ def Decrypt(self, Data):
+ Data = base64.b64decode(Data)
+ Iv = Data[:16]
+ Cipher = AES.new(self.Key, self.Mode, Iv)
+ return unpad(Cipher.decrypt(Data[16:]), AES.block_size).decode('utf-8')
+
+class PortRange:
+ def __init__(self, RangeStr):
+ Match = re.match(r'^(\d+)-(\d+)$', RangeStr)
+ if not Match:
+ raise ValueError("Invalid port range format")
+ self.Start = int(Match.group(1))
+ self.End = int(Match.group(2))
+ if self.Start < 1 or self.End > 65535 or self.Start > self.End:
+ raise ValueError("Invalid port range values")
+ def IsInRange(self, Port):
+ return self.Start <= Port <= self.End
+
+class ClientHandler:
+ def __init__(self, Server, ClientSocket, Address):
+ self.Server = Server
+ self.ClientSocket = ClientSocket
+ self.Address = Address
+ self.IsRunning = True
+ self.Mappings = {}
+ self.Encryptor = AesEncryptor(Server.Key)
+ self.Log("New client connected")
+ def Log(self, Message):
+ Timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
+ print(f"[{Timestamp}] [Client {self.Address}] {Message}")
+ def HandleError(self, Message):
+ self.Log(f"Error: {Message}")
+ self.Log(traceback.format_exc())
+ def Close(self):
+ self.IsRunning = False
+ for Mapping in self.Mappings.values():
+ Mapping.Close()
+ self.Mappings.clear()
+ try:
+ self.ClientSocket.close()
+ except:
+ pass
+ self.Log("Client disconnected")
+ def CreateTcpMapping(self, ForwardHost, ForwardPort, TargetPort):
+ try:
+ ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ ServerSocket.bind(('0.0.0.0', TargetPort))
+ ServerSocket.listen(5)
+ Mapping = TcpMapping(self, ServerSocket, ForwardHost, ForwardPort, TargetPort)
+ self.Mappings[TargetPort] = Mapping
+ threading.Thread(target=Mapping.Start, daemon=True).start()
+ return True
+ except Exception as E:
+ self.HandleError(f"Failed to create TCP mapping: {str(E)}")
+ return False
+ def RemoveMapping(self, TargetPort):
+ if TargetPort in self.Mappings:
+ self.Mappings[TargetPort].Close()
+ del self.Mappings[TargetPort]
+ return True
+ return False
+ def ProcessCommand(self, Command):
+ try:
+ CmdType = Command.get('type')
+ if CmdType == 'register':
+ if len(self.Mappings) >= self.Server.MaxPortsPerClient:
+ return {'status': 'error', 'message': f'Max {self.Server.MaxPortsPerClient} ports per client'}
+ TargetPort = Command.get('target_port')
+ if not self.Server.AllowedPorts.IsInRange(TargetPort):
+ return {'status': 'error', 'message': f'Target port {TargetPort} not in allowed range'}
+ if TargetPort in self.Server.GetAllUsedPorts():
+ return {'status': 'error', 'message': f'Target port {TargetPort} already in use'}
+ Mode = Command.get('mode', 'tcp').lower()
+ ForwardHost = Command.get('forward_host', '127.0.0.1')
+ ForwardPort = Command.get('forward_port')
+ if not ForwardPort:
+ return {'status': 'error', 'message': 'Forward port is required'}
+ Success = False
+ if Mode == 'tcp':
+ Success = self.CreateTcpMapping(ForwardHost, ForwardPort, TargetPort)
+ else:
+ return {'status': 'error', 'message': f'Unsupported mode {Mode}'}
+ if Success:
+ return {'status': 'success', 'message': f'Mapping created: {TargetPort} -> {ForwardHost}:{ForwardPort} ({Mode})'}
+ else:
+ return {'status': 'error', 'message': 'Failed to create mapping'}
+ elif CmdType == 'unregister':
+ TargetPort = Command.get('target_port')
+ if self.RemoveMapping(TargetPort):
+ return {'status': 'success', 'message': f'Mapping removed: {TargetPort}'}
+ else:
+ return {'status': 'error', 'message': f'No mapping found for {TargetPort}'}
+ else:
+ return {'status': 'error', 'message': f'Unknown command type {CmdType}'}
+ except Exception as E:
+ self.HandleError(f"Error processing command: {str(E)}")
+ return {'status': 'error', 'message': str(E)}
+ def Run(self):
+ try:
+ self.ClientSocket.settimeout(30)
+ while self.IsRunning:
+ Data = b''
+ while True:
+ try:
+ Chunk = self.ClientSocket.recv(1024)
+ if not Chunk:
+ self.Log("Connection closed by client")
+ self.Close()
+ return
+ Data += Chunk
+ if b'\n' in Data:
+ break
+ except socket.timeout:
+ continue
+ except Exception as E:
+ self.HandleError(f"Receive error: {str(E)}")
+ self.Close()
+ return
+ try:
+ EncryptedData = Data.decode('utf-8').strip()
+ DecryptedData = self.Encryptor.Decrypt(EncryptedData)
+ Command = json.loads(DecryptedData)
+ Timestamp = Command.get('timestamp')
+ if not Timestamp or abs(time.time() - float(Timestamp)) > 30:
+ Response = {'status': 'error', 'message': 'Invalid or expired timestamp'}
+ else:
+ Response = self.ProcessCommand(Command)
+ Response['timestamp'] = str(time.time())
+ EncryptedResponse = self.Encryptor.Encrypt(json.dumps(Response))
+ self.ClientSocket.sendall((EncryptedResponse + '\n').encode('utf-8'))
+ except Exception as E:
+ self.HandleError(f"Data processing error: {str(E)}")
+ Response = {'status': 'error', 'message': 'Invalid data format'}
+ EncryptedResponse = self.Encryptor.Encrypt(json.dumps(Response))
+ self.ClientSocket.sendall((EncryptedResponse + '\n').encode('utf-8'))
+ except Exception as E:
+ self.HandleError(f"Client handler error: {str(E)}")
+ finally:
+ self.Close()
+
+class TcpMapping:
+ def __init__(self, ClientHandler, ServerSocket, ForwardHost, ForwardPort, TargetPort):
+ self.ClientHandler = ClientHandler
+ self.ServerSocket = ServerSocket
+ self.ForwardHost = ForwardHost
+ self.ForwardPort = ForwardPort
+ self.TargetPort = TargetPort
+ self.IsRunning = True
+ self.ClientHandler.Log(f"TCP mapping created: {TargetPort} -> {ForwardHost}:{ForwardPort}")
+ def Log(self, Message):
+ self.ClientHandler.Log(f"[TCP {self.TargetPort}] {Message}")
+ def HandleError(self, Message):
+ self.Log(f"Error: {Message}")
+ self.Log(traceback.format_exc())
+ def Close(self):
+ self.IsRunning = False
+ try:
+ self.ServerSocket.close()
+ except:
+ pass
+ self.Log("Mapping closed")
+ def Relay(self, SrcSocket, DstSocket, Direction):
+ try:
+ while self.IsRunning:
+ Data = SrcSocket.recv(4096)
+ if not Data:
+ break
+ DstSocket.sendall(Data)
+ self.Log(f"Relayed {len(Data)} bytes {Direction}")
+ except Exception as E:
+ self.HandleError(f"Relay error {Direction}: {str(E)}")
+ finally:
+ try:
+ SrcSocket.close()
+ except:
+ pass
+ try:
+ DstSocket.close()
+ except:
+ pass
+ def HandleConnection(self, ClientSocket, Address):
+ self.Log(f"New connection from {Address}")
+ try:
+ ForwardSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ ForwardSocket.connect((self.ForwardHost, self.ForwardPort))
+ self.Log(f"Connected to forward target {self.ForwardHost}:{self.ForwardPort}")
+ threading.Thread(target=self.Relay, args=(ClientSocket, ForwardSocket, f"to {self.ForwardHost}:{self.ForwardPort}"), daemon=True).start()
+ threading.Thread(target=self.Relay, args=(ForwardSocket, ClientSocket, f"from {self.ForwardHost}:{self.ForwardPort}"), daemon=True).start()
+ except Exception as E:
+ self.HandleError(f"Failed to connect to forward target: {str(E)}")
+ try:
+ ClientSocket.close()
+ except:
+ pass
+ def Start(self):
+ try:
+ while self.IsRunning:
+ self.ServerSocket.settimeout(1)
+ try:
+ ClientSocket, Address = self.ServerSocket.accept()
+ ClientSocket.settimeout(None)
+ threading.Thread(target=self.HandleConnection, args=(ClientSocket, Address), daemon=True).start()
+ except socket.timeout:
+ continue
+ except Exception as E:
+ if self.IsRunning:
+ self.HandleError(f"Accept error: {str(E)}")
+ break
+ except Exception as E:
+ self.HandleError(f"Mapping error: {str(E)}")
+ finally:
+ self.Close()
+
+class PortForwardServer:
+ def __init__(self, InternalPort=5000, AllowedPortsRange="5001-5500", MaxPortsPerClient=5, Key="07A36AEF1907843"):
+ self.InternalPort = InternalPort
+ self.AllowedPorts = PortRange(AllowedPortsRange)
+ self.MaxPortsPerClient = MaxPortsPerClient
+ self.Key = Key
+ self.ServerSocket = None
+ self.IsRunning = False
+ self.Clients = []
+ self.ClientsLock = threading.Lock()
+ def Log(self, Message):
+ Timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
+ print(f"[{Timestamp}] [Server] {Message}")
+ def HandleError(self, Message):
+ self.Log(f"Error: {Message}")
+ self.Log(traceback.format_exc())
+ def GetAllUsedPorts(self):
+ UsedPorts = set()
+ with self.ClientsLock:
+ for Client in self.Clients:
+ UsedPorts.update(Client.Mappings.keys())
+ return UsedPorts
+ def RemoveClient(self, Client):
+ with self.ClientsLock:
+ if Client in self.Clients:
+ self.Clients.remove(Client)
+ self.Log(f"Client removed. Total clients: {len(self.Clients)}")
+ def Start(self):
+ try:
+ self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.ServerSocket.bind(('0.0.0.0', self.InternalPort))
+ self.ServerSocket.listen(5)
+ self.IsRunning = True
+ self.Log(f"Server started on port {self.InternalPort}")
+ self.Log(f"Allowed ports: {self.AllowedPorts.Start}-{self.AllowedPorts.End}")
+ self.Log(f"Max ports per client: {self.MaxPortsPerClient}")
+ while self.IsRunning:
+ ClientSocket, Address = self.ServerSocket.accept()
+ self.Log(f"New connection from {Address}")
+ Client = ClientHandler(self, ClientSocket, Address)
+ with self.ClientsLock:
+ self.Clients.append(Client)
+ self.Log(f"Client added. Total clients: {len(self.Clients)}")
+ threading.Thread(target=lambda: [Client.Run(), self.RemoveClient(Client)], daemon=True).start()
+ except Exception as E:
+ self.HandleError(f"Server error: {str(E)}")
+ self.Stop()
+ def Stop(self):
+ self.IsRunning = True
+ if self.ServerSocket:
+ try:
+ self.ServerSocket.close()
+ except:
+ pass
+ with self.ClientsLock:
+ for Client in self.Clients:
+ Client.Close()
+ self.Clients.clear()
+ self.Log("Server stopped")
+
+if __name__ == "__main__":
+ Config = {
+ "internal_data_port": 5000,
+ "allowed_port_range": "5001-5500",
+ "max_ports_per_client": 5,
+ "key": "07A36AEF1907843"
+ }
+ if len(sys.argv) > 1:
+ try:
+ with open(sys.argv[1], 'r') as F:
+ Config.update(json.load(F))
+ except Exception as E:
+ print(f"Error loading config file: {str(E)}")
+ os._exit(0)
+ Server = PortForwardServer(
+ InternalPort=int(Config["internal_data_port"]),
+ AllowedPortsRange=Config["allowed_port_range"],
+ MaxPortsPerClient=int(Config["max_ports_per_client"]),
+ Key=Config["key"]
+ )
+ try:
+ Server.Start()
+ except KeyboardInterrupt:
+ Server.Stop()
diff --git a/client.py b/client.py
new file mode 100644
index 0000000..61f557b
--- /dev/null
+++ b/client.py
@@ -0,0 +1,295 @@
+import socket
+import threading
+import json
+import traceback
+import sys
+import time
+
+class PortForwardClient:
+ def __init__(self, ServerDomain="127.0.0.1", ServerPort=5000, Forwards=None, Key="07A36AEF1907843"):
+ self.ServerDomain = ServerDomain
+ self.ServerPort = ServerPort
+ self.Forwards = Forwards or []
+ self.Key = Key
+ self.ServerSocket = None
+ self.Running = True
+ self.ForwardMap = {}
+ self.ConnectionMap = {}
+ self.Lock = threading.Lock()
+ self.Buffer = b''
+ self.MessageSeparator = b'|||'
+
+ def Start(self):
+ try:
+ self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.ServerSocket.connect((self.ServerDomain, self.ServerPort))
+ print(f"Connected to server {self.ServerDomain}:{self.ServerPort}")
+ self.Authenticate()
+ threading.Thread(target=self.ReceiveFromServer, daemon=True).start()
+ self.SetupForwards()
+ while self.Running:
+ time.sleep(5)
+ except Exception as e:
+ print(f"Client start error: {e}")
+ traceback.print_exc()
+ self.Stop()
+
+ def Stop(self):
+ self.Running = False
+ with self.Lock:
+ for forwardId, forwardData in self.ForwardMap.items():
+ try:
+ self.SendToServer({'type': 'close_forward', 'forward_id': forwardId})
+ except:
+ pass
+ for connId, conn in forwardData['connections'].items():
+ try:
+ conn.close()
+ except:
+ pass
+ self.ForwardMap.clear()
+ self.ConnectionMap.clear()
+ if self.ServerSocket:
+ try:
+ self.ServerSocket.close()
+ except:
+ pass
+ print("Client stopped")
+
+ def Authenticate(self):
+ self.SendToServer({'type': 'auth', 'key': self.Key})
+ response = self.ServerSocket.recv(4096)
+ if not response:
+ raise ConnectionError("Server closed connection during authentication")
+ self.Buffer += response
+ self.ProcessBuffer()
+
+ def SetupForwards(self):
+ for forward in self.Forwards:
+ forwardDomain = forward.get('forward_domain', '127.0.0.1')
+ forwardPort = forward.get('forward_port')
+ targetPort = forward.get('target_port')
+ mode = forward.get('mode', 'tcp').upper()
+ if not all([forwardPort, targetPort]):
+ print("Invalid forward configuration, skipping")
+ continue
+ self.SendToServer({
+ 'type': 'forward_request',
+ 'forward_domain': forwardDomain,
+ 'forward_port': forwardPort,
+ 'target_port': targetPort,
+ 'mode': mode
+ })
+
+ def ReceiveFromServer(self):
+ while self.Running and self.ServerSocket:
+ try:
+ self.ServerSocket.settimeout(1)
+ data = self.ServerSocket.recv(4096)
+ if not data:
+ print("Server disconnected")
+ self.Running = False
+ break
+ self.Buffer += data
+ self.ProcessBuffer()
+ except socket.timeout:
+ continue
+ except Exception as e:
+ print(f"Server communication error: {e}")
+ traceback.print_exc()
+ self.Running = False
+ break
+
+ def ProcessBuffer(self):
+ while self.MessageSeparator in self.Buffer:
+ msgEnd = self.Buffer.index(self.MessageSeparator)
+ messageData = self.Buffer[:msgEnd]
+ self.Buffer = self.Buffer[msgEnd + len(self.MessageSeparator):]
+ try:
+ message = json.loads(messageData.decode('utf-8'))
+ self.ProcessServerMessage(message)
+ except json.JSONDecodeError:
+ print("Received invalid JSON from server")
+ except Exception as e:
+ print(f"Error processing server data: {e}")
+ traceback.print_exc()
+
+ def ProcessServerMessage(self, message):
+ if message.get('type') == 'forward_response':
+ self.HandleForwardResponse(message)
+ elif message.get('type') == 'new_connection':
+ self.HandleNewConnection(message)
+ elif message.get('type') == 'data':
+ self.HandleData(message)
+ elif message.get('type') == 'close_connection':
+ self.HandleCloseConnection(message)
+ elif message.get('type') == 'error':
+ print(f"Server error: {message.get('message')}")
+
+ def HandleForwardResponse(self, message):
+ if message.get('success'):
+ forwardId = message.get('forward_id')
+ targetPort = message.get('target_port')
+ forwardConfig = next((f for f in self.Forwards if f.get('target_port') == targetPort), None)
+ if forwardConfig and forwardId:
+ with self.Lock:
+ self.ForwardMap[forwardId] = {
+ 'config': forwardConfig,
+ 'connections': {}
+ }
+ print(f"Forward established: {forwardId}")
+ else:
+ print(f"Received forward response for unknown target port {targetPort}")
+ else:
+ print(f"Forward request failed: {message.get('message')}")
+
+ def HandleNewConnection(self, message):
+ forwardId = message.get('forward_id')
+ connId = message.get('conn_id')
+ if not all([forwardId, connId]):
+ return
+ with self.Lock:
+ if forwardId not in self.ForwardMap:
+ print(f"Received connection for unknown forward {forwardId}")
+ return
+ forwardData = self.ForwardMap[forwardId]
+ config = forwardData['config']
+ try:
+ if config.get('mode', 'tcp').upper() == 'TCP':
+ conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ conn.connect((config['forward_domain'], config['forward_port']))
+ with self.Lock:
+ forwardData['connections'][connId] = conn
+ self.ConnectionMap[connId] = forwardId
+ threading.Thread(target=self.ForwardToServer, args=(forwardId, connId, conn), daemon=True).start()
+ print(f"Established connection {connId} for forward {forwardId}")
+ else:
+ print(f"Unsupported mode for forward {forwardId}")
+ except Exception as e:
+ print(f"Error establishing connection for {forwardId}: {e}")
+ traceback.print_exc()
+ self.SendToServer({
+ 'type': 'close_connection',
+ 'forward_id': forwardId,
+ 'conn_id': connId
+ })
+
+ def ForwardToServer(self, forwardId, connId, conn):
+ try:
+ while self.Running:
+ conn.settimeout(1)
+ try:
+ with self.Lock:
+ if connId not in self.ConnectionMap or self.ConnectionMap[connId] != forwardId:
+ break
+ data = conn.recv(4096)
+ if not data:
+ break
+ self.SendToServer({
+ 'type': 'data',
+ 'forward_id': forwardId,
+ 'conn_id': connId,
+ 'data': data.hex()
+ })
+ except socket.timeout:
+ continue
+ except Exception as e:
+ print(f"Forward to server error: {e}")
+ traceback.print_exc()
+ break
+ finally:
+ try:
+ conn.close()
+ except:
+ pass
+ with self.Lock:
+ if forwardId in self.ForwardMap and connId in self.ForwardMap[forwardId]['connections']:
+ del self.ForwardMap[forwardId]['connections'][connId]
+ if connId in self.ConnectionMap:
+ del self.ConnectionMap[connId]
+ self.SendToServer({
+ 'type': 'close_connection',
+ 'forward_id': forwardId,
+ 'conn_id': connId
+ })
+ print(f"Closed connection {connId} for forward {forwardId}")
+
+ def HandleData(self, message):
+ forwardId = message.get('forward_id')
+ connId = message.get('conn_id')
+ dataHex = message.get('data')
+ if not all([forwardId, connId, dataHex]):
+ return
+ try:
+ data = bytes.fromhex(dataHex)
+ with self.Lock:
+ if forwardId not in self.ForwardMap or connId not in self.ForwardMap[forwardId]['connections']:
+ print(f"Received data for unknown connection {connId}")
+ return
+ conn = self.ForwardMap[forwardId]['connections'][connId]
+ conn.sendall(data)
+ except Exception as e:
+ print(f"Data handling error: {e}")
+ traceback.print_exc()
+ self.SendToServer({
+ 'type': 'close_connection',
+ 'forward_id': forwardId,
+ 'conn_id': connId
+ })
+
+ def HandleCloseConnection(self, message):
+ forwardId = message.get('forward_id')
+ connId = message.get('conn_id')
+ with self.Lock:
+ if forwardId in self.ForwardMap and connId in self.ForwardMap[forwardId]['connections']:
+ try:
+ self.ForwardMap[forwardId]['connections'][connId].close()
+ except:
+ pass
+ del self.ForwardMap[forwardId]['connections'][connId]
+ if connId in self.ConnectionMap:
+ del self.ConnectionMap[connId]
+ print(f"Connection {connId} for forward {forwardId} closed by server")
+
+ def SendToServer(self, message):
+ if not self.ServerSocket or not self.Running:
+ return
+ try:
+ data = json.dumps(message).encode('utf-8') + self.MessageSeparator
+ self.ServerSocket.sendall(data)
+ except Exception as e:
+ print(f"Error sending to server: {e}")
+ traceback.print_exc()
+ self.Running = False
+
+def main():
+ config = {
+ "ServerDomain": "127.0.0.1",
+ "ServerPort": 5000,
+ "Key": "07A36AEF1907843",
+ "Forwards": [
+ {
+ "forward_domain": "127.0.0.1",
+ "forward_port": 36667,
+ "target_port": 5002,
+ "mode": "TCP"
+ }
+ ]
+ }
+ if len(sys.argv) > 1:
+ try:
+ with open(sys.argv[1], 'r') as f:
+ config.update(json.load(f))
+ except Exception as e:
+ print(f"Error loading config file: {e}")
+ traceback.print_exc()
+ client = PortForwardClient(
+ ServerDomain=config["ServerDomain"],
+ ServerPort=int(config["ServerPort"]),
+ Forwards=config["Forwards"],
+ Key=config["Key"]
+ )
+ client.Start()
+
+if __name__ == "__main__":
+ main()
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..ba6d0c1
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,388 @@
+
+
+
PyFrp
+ Frp (Fast Reverse Proxy) implemented in Python
+
+
+
+
+
+---
+
+
+
+
+
+
+---
+> **This project is only in its early stages, and the documentation is currently being improved. Thank you for using this project. If possible, please give it a star ♥**
+
+# PyFrp
+
+## 🗡 Brief Introduction
+The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size, while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features.
+
+### Comparison with the Original Frp
+| Feature| PyFrp | Frp|
+|:---------------:|:-----:|:----:|
+| Quick Setup| ✅| ✅|
+| Size| ✅| ❌|
+| SSL Support| ❌| ✅|
+| Embeddable| ✅| ✅❌ |
+| Documentation| ✅| ✅|
+| TCP Support| ✅| ✅|
+| UDP Support| ❌| ✅|
+| HTTP/HTTPS Support| ❌| ✅|
+
+Choose based on your needs:
+- If you prioritize **quick setup, small size, simplicity, and embeddability** (e.g., integrating into your Python project), PyFrp is a great choice.
+- For **production deployments**, Frp remains the better option for now.
+
+In the future, we aim to enhance PyFrp's functionality, add more configuration options, and potentially support additional protocols (e.g., HTTP/HTTPS).
+
+We’re just getting started and would greatly appreciate your support—**a ⭐ Star** would mean a lot to us!
+
+> Note: **HTTPS is not yet supported**. We may add this later, and contributions are welcome!
+
+---
+
+## 🚀 Quick Start
+
+### Prerequisites
+
+The only third-party library you need is `pycryptodome`. Install it with:
+```bash
+pip install pycryptodome
+```
+While any version of `pycryptodome` should work, we recommend **v3.22.0** if you encounter issues:
+```bash
+pip install pycryptodome==3.22.0
+```
+
+### Configuration
+
+#### Server (`server_config.json`):
+```json
+{
+ "InternalDataPort": 5000, // PyFrp server data port
+ "AllowedPortRange": "5001-5500", // Allowed port range
+ "MaxPortsPerClient": 5, // Max ports per client
+ "Key": "07A36AEF1907843" // Authentication key
+}
+```
+
+#### Client (`client_config.json`):
+```json
+{
+ "ServerDomain": "127.0.0.1", // PyFrp server address
+ "ServerPort": 5000, // PyFrp server port
+ "Key": "07A36AEF1907843", // Authentication key
+ "Forwards": [ // Port mappings
+ {
+ "forward_domain": "127.0.0.1", // Local host
+ "forward_port": 36667, // Local port
+ "target_port": 5002, // Target port
+ "mode": "TCP" // Protocol (TCP only)
+ }
+ // Add more mappings as needed
+ ]
+}
+```
+
+### Running PyFrp
+
+#### Start the server:
+```bash
+python server.py server_config.json
+```
+
+#### Start the client:
+```bash
+python client.py client_config.json
+```
+
+You can also modify the default configuration directly in the source code.
+
+---
+
+## 📖 Usage Example
+
+### Example: Exposing a Local HTTP Server
+
+1. **Start a local HTTP server**:
+ ```bash
+ python -m http.server 36667
+ ```
+
+2. **Configure PyFrp client** (`client_config.json`):
+ ```json
+ {
+ "ServerDomain": "127.0.0.1",
+ "ServerPort": 5000,
+ "Key": "07A36AEF1907843",
+ "Forwards": [
+ {
+ "forward_domain": "127.0.0.1",
+ "forward_port": 36667,
+ "target_port": 5002,
+ "mode": "TCP"
+ }
+ ]
+ }
+ ```
+
+3. **Start PyFrp server and client**:
+ ```bash
+ # In terminal 1
+ python server.py
+
+ # In terminal 2
+ python client.py
+ ```
+
+4. **Access the local server through the proxy**:
+ ```bash
+ curl http://localhost:5002
+ ```
+
+ You should see the HTTP response from your local server, indicating that the port forwarding is working correctly.
+
+---
+
+## ⚠️ Limitations
+
+- **Only TCP protocol is supported** (UDP, HTTP, HTTPS are not supported)
+- **No SSL encryption** for data transmission
+- **Simple authentication mechanism** (fixed key)
+- **No automatic reconnection** after network interruption
+- **Limited error handling** and logging
+- **Performance limitations** due to JSON and hex encoding for data transfer
+
+---
+
+## 🛠️ How It Works
+
+### Server-side (server.py)
+1. Listens for client connections on the internal data port
+2. Handles client authentication
+3. Creates and manages port forwarding services
+4. Forwards data between external connections and clients
+
+### Client-side (client.py)
+1. Connects to the server and authenticates
+2. Sends port forwarding configuration
+3. Handles connections to local services
+4. Forwards data between the server and local services
+
+---
+
+## 🤝 Contributing
+
+Contributions are welcome! Here's how you can help:
+
+1. **Report bugs** by opening an issue
+2. **Suggest features** by opening an issue
+3. **Submit pull requests** with bug fixes or enhancements
+
+### Development Guidelines
+- Follow the existing code style
+- Add comments for complex code
+- Test your changes thoroughly
+- Update documentation as needed
+
+---
+
+## 📞 Contact Us
+ - 📧 Email: wyt18222152539wyt@163.com
+ - 🌐 Website: [Galaxy Vastar Software Studio](https://www.gvsds.com)
+ - 📱 WeChat: GVSADS
+
+---
+
+## 📄 License
+
+This project is licensed under the MIT License. See the LICENSE file for details.
+
+---
+
+## 🗡 简单介绍
+原本的 Frp https://github.com/Fatedier/frp/ 体积略显臃肿,而 PyFrp 则是一个基于 Python 实现的简单版本,体积更小,功能也更简单。
+
+我们 和 原版 Frp 相比
+| 功能 | PyFrp | Frp |
+|:-:|:-:|:-:|
+| 快速配置 | ✅ | ✅ |
+| 体积 | ✅ | ❌ |
+| SSL 功能 | ❌ | ✅ |
+| 嵌入式 | ✅ | ✅❌ |
+| 文档 | ✅ | ✅ |
+| TCP 支持 | ✅ | ✅ |
+| UDP 支持 | ❌ | ✅ |
+| HTTP/HTTPS 支持 | ❌ | ✅ |
+
+请您根据您的需要选择使用哪个,
+- 如果您需要快速配置,体积小,功能简单,嵌入式,集成到您自己的 Python 项目中,那么 PyFrp 是一个不错的选择。
+- 如果您需要部署在生产环境中,暂时不要选择 PyFrp,Frp 是一个更好的选择。
+
+日后,我们以 Frp 为目标,完善其功能,添加更多的配置项,同时也会考虑添加更多的协议支持,如 http、https 等。
+
+我们其实也才起步,仍然需要您的支持,如果可以,还请您点一个 Star ⭐,这将是对我们最大的支持。
+
+> 注意,我们暂时还不支持 https 协议。稍后如果有时间,我们可能会考虑支持,如果您已经帮我们支持,随时欢迎您提交。
+
+## 🚀 快速开始
+
+### 先决条件
+
+你唯一需要下载的第三方库是 `pycryptodome`,使用以下命令安装:
+```bash
+pip install pycryptodome
+```
+我们推荐使用 **v3.22.0** 版本:
+```bash
+pip install pycryptodome==3.22.0
+```
+
+### 配置
+
+#### 服务器端配置 (`server_config.json`):
+```json
+{
+ "InternalDataPort": 5000, // PyFrp 服务器端数据端口
+ "AllowedPortRange": "5001-5500", // 允许的端口范围
+ "MaxPortsPerClient": 5, // 每个客户端最大端口数
+ "Key": "07A36AEF1907843" // 认证密钥
+}
+```
+
+#### 客户端配置 (`client_config.json`):
+```json
+{
+ "ServerDomain": "127.0.0.1", // PyFrp 服务器端主机地址
+ "ServerPort": 5000, // PyFrp 服务器端端口
+ "Key": "07A36AEF1907843", // 认证密钥
+ "Forwards": [ // 端口映射配置
+ {
+ "forward_domain": "127.0.0.1", // 本地主机地址
+ "forward_port": 36667, // 本地端口
+ "target_port": 5002, // 目标端口
+ "mode": "TCP" // 传输模式(仅支持TCP)
+ }
+ // 你可以在这里输入更多的端口映射配置
+ ]
+}
+```
+
+### 运行 PyFrp
+
+#### 启动服务器:
+```bash
+python server.py server_config.json
+```
+
+#### 启动客户端:
+```bash
+python client.py client_config.json
+```
+
+你也可以直接修改源代码中的默认配置。
+
+---
+
+## � 使用示例
+
+### 示例:暴露本地 HTTP 服务器
+
+1. **启动本地 HTTP 服务器**:
+ ```bash
+ python -m http.server 36667
+ ```
+
+2. **配置 PyFrp 客户端** (`client_config.json`):
+ ```json
+ {
+ "ServerDomain": "127.0.0.1",
+ "ServerPort": 5000,
+ "Key": "07A36AEF1907843",
+ "Forwards": [
+ {
+ "forward_domain": "127.0.0.1",
+ "forward_port": 36667,
+ "target_port": 5002,
+ "mode": "TCP"
+ }
+ ]
+ }
+ ```
+
+3. **启动 PyFrp 服务器和客户端**:
+ ```bash
+ # 在终端 1 中
+ python server.py
+
+ # 在终端 2 中
+ python client.py
+ ```
+
+4. **通过代理访问本地服务器**:
+ ```bash
+ curl http://localhost:5002
+ ```
+
+ 你应该会看到来自本地服务器的 HTTP 响应,表明端口转发工作正常。
+
+---
+
+## ⚠️ 限制
+
+- **仅支持 TCP 协议**(不支持 UDP、HTTP、HTTPS)
+- **数据传输无 SSL 加密**
+- **简单的认证机制**(固定密钥)
+- **网络中断后无自动重连**
+- **有限的错误处理**和日志记录
+- **性能限制**(由于使用 JSON 和 hex 编码传输数据)
+
+---
+
+## 🛠️ 工作原理
+
+### 服务器端 (server.py)
+1. 在内部数据端口上监听客户端连接
+2. 处理客户端认证
+3. 创建和管理端口转发服务
+4. 在外部连接和客户端之间转发数据
+
+### 客户端 (client.py)
+1. 连接到服务器并进行认证
+2. 发送端口转发配置
+3. 处理到本地服务的连接
+4. 在服务器和本地服务之间转发数据
+
+---
+
+## 🤝 贡献
+
+欢迎贡献!你可以通过以下方式帮助我们:
+
+1. **报告错误**:通过打开 issue
+2. **建议功能**:通过打开 issue
+3. **提交代码**:通过 pull request 提交 bug 修复或增强功能
+
+### 开发指南
+- 遵循现有的代码风格
+- 为复杂代码添加注释
+- 彻底测试你的更改
+- 根据需要更新文档
+
+---
+
+## �📞 联系我们
+ - 📧 Email: wyt18222152539wyt@163.com
+ - 🌐 官网: [银河万通软件开发工作室](https://www.gvsds.com)
+ - 📱 微信: GVSADS
+
+---
+
+## 📄 许可证
+
+本项目采用 MIT 许可证。详情请参阅 LICENSE 文件。
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..1e8624d
--- /dev/null
+++ b/server.py
@@ -0,0 +1,343 @@
+import socket
+import threading
+import json
+import traceback
+import sys
+import re
+from collections import defaultdict
+
+class PortForwardServer:
+ def __init__(self, InternalDataPort=5000, AllowedPortRange="5001-5500", MaxPortsPerClient=5, Key="07A36AEF1907843"):
+ self.InternalDataPort = InternalDataPort
+ self.AllowedPortRange = AllowedPortRange
+ self.MaxPortsPerClient = MaxPortsPerClient
+ self.Key = Key
+ self.ParsePortRange()
+ self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.Clients = {}
+ self.ClientLocks = defaultdict(threading.Lock)
+ self.ForwardMap = {}
+ self.ForwardLocks = defaultdict(threading.Lock)
+ self.Running = True
+ self.MessageSeparator = b'|||'
+
+ def ParsePortRange(self):
+ match = re.match(r'^(\d+)-(\d+)$', self.AllowedPortRange)
+ if not match:
+ raise ValueError("Invalid port range format")
+ self.MinPort = int(match.group(1))
+ self.MaxPort = int(match.group(2))
+ if self.MinPort >= self.MaxPort or self.MinPort < 1 or self.MaxPort > 65535:
+ raise ValueError("Invalid port range values")
+
+ def IsPortAllowed(self, port):
+ return self.MinPort <= port <= self.MaxPort
+
+ def Start(self):
+ try:
+ self.ServerSocket.bind(('0.0.0.0', self.InternalDataPort))
+ self.ServerSocket.listen(5)
+ print(f"Server started on port {self.InternalDataPort}")
+ acceptThread = threading.Thread(target=self.AcceptClients, daemon=True)
+ acceptThread.start()
+ while self.Running:
+ cmd = input("Enter 'exit' to stop server: ")
+ if cmd.lower() == 'exit':
+ self.Running = False
+ break
+ self.Stop()
+ except Exception as e:
+ print(f"Server start error: {e}")
+ traceback.print_exc()
+
+ def Stop(self):
+ self.Running = False
+ self.ServerSocket.close()
+ for clientId, clientData in self.Clients.items():
+ with self.ClientLocks[clientId]:
+ if clientData['socket']:
+ try:
+ clientData['socket'].close()
+ except:
+ pass
+ with self.ForwardLocks[clientId]:
+ for forwardId, forwardData in clientData['forwards'].items():
+ try:
+ forwardData['server'].close()
+ except:
+ pass
+ print("Server stopped")
+
+ def AcceptClients(self):
+ while self.Running:
+ try:
+ clientSocket, addr = self.ServerSocket.accept()
+ print(f"New client connection from {addr}")
+ clientThread = threading.Thread(target=self.HandleClient, args=(clientSocket, addr), daemon=True)
+ clientThread.start()
+ except Exception as e:
+ if self.Running:
+ print(f"Accept error: {e}")
+ traceback.print_exc()
+
+ def HandleClient(self, clientSocket, addr):
+ clientId = f"{addr[0]}:{addr[1]}"
+ self.Clients[clientId] = {'socket': clientSocket, 'forwards': {}, 'addr': addr, 'buffer': b''}
+ try:
+ while self.Running:
+ clientSocket.settimeout(30)
+ try:
+ data = clientSocket.recv(4096)
+ if not data:
+ print(f"Client {clientId} disconnected")
+ break
+ self.Clients[clientId]['buffer'] += data
+ self.ProcessBuffer(clientId)
+ except socket.timeout:
+ continue
+ except Exception as e:
+ print(f"Client communication error: {e}")
+ traceback.print_exc()
+ break
+ finally:
+ with self.ClientLocks[clientId]:
+ if clientId in self.Clients:
+ del self.Clients[clientId]
+ with self.ForwardLocks[clientId]:
+ for forwardId in list(self.ForwardMap.keys()):
+ if forwardId.startswith(clientId):
+ del self.ForwardMap[forwardId]
+ try:
+ clientSocket.close()
+ except:
+ pass
+ print(f"Client {clientId} handler cleaned up")
+
+ def ProcessBuffer(self, clientId):
+ clientData = self.Clients.get(clientId)
+ if not clientData:
+ return
+ while self.MessageSeparator in clientData['buffer']:
+ msgEnd = clientData['buffer'].index(self.MessageSeparator)
+ messageData = clientData['buffer'][:msgEnd]
+ clientData['buffer'] = clientData['buffer'][msgEnd + len(self.MessageSeparator):]
+ try:
+ message = json.loads(messageData.decode('utf-8'))
+ self.ProcessClientMessage(clientId, message)
+ except json.JSONDecodeError:
+ print(f"Invalid JSON from client {clientId}")
+ self.SendToClient(clientId, {'type': 'error', 'message': 'Invalid JSON'})
+ except Exception as e:
+ print(f"Error processing message: {e}")
+ traceback.print_exc()
+
+ def ProcessClientMessage(self, clientId, message):
+ if message.get('type') == 'auth':
+ self.HandleAuth(clientId, message)
+ elif message.get('type') == 'forward_request':
+ self.HandleForwardRequest(clientId, message)
+ elif message.get('type') == 'data':
+ self.HandleData(clientId, message)
+ elif message.get('type') == 'close_forward':
+ self.HandleCloseForward(clientId, message)
+ else:
+ self.SendToClient(clientId, {'type': 'error', 'message': 'Unknown message type'})
+
+ def HandleAuth(self, clientId, message):
+ clientData = self.Clients.get(clientId)
+ if not clientData:
+ return
+ if message.get('key') == self.Key:
+ clientData['authenticated'] = True
+ self.SendToClient(clientId, {'type': 'auth_response', 'success': True})
+ print(f"Client {clientId} authenticated successfully")
+ else:
+ self.SendToClient(clientId, {'type': 'auth_response', 'success': False, 'message': 'Invalid key'})
+ clientData['socket'].close()
+ print(f"Client {clientId} failed authentication")
+
+ def HandleForwardRequest(self, clientId, message):
+ clientData = self.Clients.get(clientId)
+ if not clientData or not clientData.get('authenticated', False):
+ self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Not authenticated'})
+ return
+ with self.ClientLocks[clientId]:
+ if len(clientData['forwards']) >= self.MaxPortsPerClient:
+ self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Max ports per client reached'})
+ return
+ targetPort = message.get('target_port')
+ mode = message.get('mode', 'tcp').upper()
+ if not self.IsPortAllowed(targetPort):
+ self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Target port not allowed'})
+ return
+ forwardId = f"{clientId}:{targetPort}"
+ if forwardId in self.ForwardMap:
+ self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Port already in use'})
+ return
+ try:
+ if mode == 'TCP':
+ forwardServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ forwardServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ forwardServer.bind(('0.0.0.0', targetPort))
+ forwardServer.listen(5)
+ with self.ClientLocks[clientId]:
+ clientData['forwards'][forwardId] = {'server': forwardServer, 'mode': mode, 'connections': {}}
+ with self.ForwardLocks[clientId]:
+ self.ForwardMap[forwardId] = clientId
+ threading.Thread(target=self.AcceptForwardConnections, args=(clientId, forwardId, forwardServer), daemon=True).start()
+ self.SendToClient(clientId, {'type': 'forward_response', 'success': True, 'target_port': targetPort, 'forward_id': forwardId})
+ print(f"Forward created: {forwardId}")
+ else:
+ self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Unsupported mode'})
+ except Exception as e:
+ print(f"Forward creation error: {e}")
+ traceback.print_exc()
+ self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': str(e)})
+
+ def AcceptForwardConnections(self, clientId, forwardId, forwardServer):
+ try:
+ while self.Running and forwardId in self.ForwardMap and self.ForwardMap[forwardId] == clientId:
+ forwardServer.settimeout(1)
+ try:
+ conn, addr = forwardServer.accept()
+ connId = f"{addr[0]}:{addr[1]}"
+ print(f"New connection to forward {forwardId} from {connId}")
+ with self.ClientLocks[clientId]:
+ clientData = self.Clients.get(clientId)
+ if not clientData or forwardId not in clientData['forwards']:
+ conn.close()
+ continue
+ clientData['forwards'][forwardId]['connections'][connId] = conn
+ self.SendToClient(clientId, {
+ 'type': 'new_connection',
+ 'forward_id': forwardId,
+ 'conn_id': connId
+ })
+ threading.Thread(target=self.ForwardToClient, args=(clientId, forwardId, connId, conn), daemon=True).start()
+ except socket.timeout:
+ continue
+ except Exception as e:
+ print(f"Forward accept error: {e}")
+ traceback.print_exc()
+ finally:
+ try:
+ forwardServer.close()
+ except:
+ pass
+ print(f"Forward listener {forwardId} stopped")
+
+ def ForwardToClient(self, clientId, forwardId, connId, conn):
+ try:
+ while self.Running:
+ conn.settimeout(1)
+ try:
+ data = conn.recv(4096)
+ if not data:
+ break
+ self.SendToClient(clientId, {
+ 'type': 'data',
+ 'forward_id': forwardId,
+ 'conn_id': connId,
+ 'data': data.hex()
+ })
+ except socket.timeout:
+ if not self.Running or clientId not in self.Clients:
+ break
+ continue
+ except Exception as e:
+ print(f"Forward to client error: {e}")
+ traceback.print_exc()
+ break
+ finally:
+ try:
+ conn.close()
+ except:
+ pass
+ with self.ClientLocks[clientId]:
+ clientData = self.Clients.get(clientId)
+ if clientData and forwardId in clientData['forwards']:
+ if connId in clientData['forwards'][forwardId]['connections']:
+ del clientData['forwards'][forwardId]['connections'][connId]
+ self.SendToClient(clientId, {
+ 'type': 'close_connection',
+ 'forward_id': forwardId,
+ 'conn_id': connId
+ })
+ print(f"Connection {connId} to forward {forwardId} closed")
+
+ def HandleData(self, clientId, message):
+ forwardId = message.get('forward_id')
+ connId = message.get('conn_id')
+ dataHex = message.get('data')
+ if not all([forwardId, connId, dataHex]):
+ return
+ try:
+ data = bytes.fromhex(dataHex)
+ with self.ClientLocks[clientId]:
+ clientData = self.Clients.get(clientId)
+ if not clientData or forwardId not in clientData['forwards']:
+ return
+ forwardData = clientData['forwards'][forwardId]
+ if connId not in forwardData['connections']:
+ return
+ conn = forwardData['connections'][connId]
+ conn.sendall(data)
+ except Exception as e:
+ print(f"Data handling error: {e}")
+ traceback.print_exc()
+
+ def HandleCloseForward(self, clientId, message):
+ forwardId = message.get('forward_id')
+ if not forwardId:
+ return
+ with self.ClientLocks[clientId]:
+ clientData = self.Clients.get(clientId)
+ if clientData and forwardId in clientData['forwards']:
+ try:
+ clientData['forwards'][forwardId]['server'].close()
+ except:
+ pass
+ del clientData['forwards'][forwardId]
+ with self.ForwardLocks[clientId]:
+ if forwardId in self.ForwardMap:
+ del self.ForwardMap[forwardId]
+ print(f"Forward {forwardId} closed by client")
+
+ def SendToClient(self, clientId, message):
+ try:
+ with self.ClientLocks[clientId]:
+ clientData = self.Clients.get(clientId)
+ if not clientData or not clientData['socket']:
+ return
+ clientSocket = clientData['socket']
+ data = json.dumps(message).encode('utf-8') + self.MessageSeparator
+ clientSocket.sendall(data)
+ except Exception as e:
+ print(f"Error sending to client {clientId}: {e}")
+ traceback.print_exc()
+
+def main():
+ config = {
+ "InternalDataPort": 5000,
+ "AllowedPortRange": "5001-5500",
+ "MaxPortsPerClient": 5,
+ "Key": "07A36AEF1907843"
+ }
+ if len(sys.argv) > 1:
+ try:
+ with open(sys.argv[1], 'r') as f:
+ config.update(json.load(f))
+ except Exception as e:
+ print(f"Error loading config file: {e}")
+ traceback.print_exc()
+ server = PortForwardServer(
+ InternalDataPort=int(config["InternalDataPort"]),
+ AllowedPortRange=config["AllowedPortRange"],
+ MaxPortsPerClient=int(config["MaxPortsPerClient"]),
+ Key=config["Key"]
+ )
+ server.Start()
+
+if __name__ == "__main__":
+ main()