Initial commit of PyFrp project

This commit is contained in:
GVSADS
2026-01-29 22:53:58 +08:00
commit 9cee40db86
12 changed files with 1769 additions and 0 deletions

21
LICENSE Normal file
View File

@@ -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.

2
__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .client import PortForwardClient
from .server import PortForwardServer

Binary file not shown.

Binary file not shown.

Binary file not shown.

7
backup/__init__.py Normal file
View File

@@ -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

208
backup/client.py Normal file
View File

@@ -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()

181
backup/readme.md Normal file
View File

@@ -0,0 +1,181 @@
<p align="center">
<img src="https://avatars.githubusercontent.com/u/193612261?v=4" width="270px" />
<p align="center">PyFrp</p>
<p align="center">Frp (Fast Reverse Proxy) implemented in Python</p>
</p>
![GitHub Followers](https://img.shields.io/badge/dynamic/json?color=green&label=GitHub%20Followers&query=%24.data.totalSubs&url=https%3A%2F%2Fapi.spencerwoo.com%2Fsubstats%2F%3Fsource%3Dgithub%26queryKey%3DGVSADS)
![Total Repos](https://img.shields.io/badge/dynamic/json?color=orange&label=Total%20Repos&query=%24.total_count&url=https%3A%2F%2Fapi.github.com%2Fsearch%2Frepositories%3Fq%3Duser%3AGVSADS)
---
![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white)
![Operating System](https://img.shields.io/badge/Operating%20System-000000?style=for-the-badge&logo=linux&logoColor=white)
![Server](https://img.shields.io/badge/Server-000000?style=for-the-badge&logo=serverless&logoColor=white)
![Networking](https://img.shields.io/badge/Networking-000000?style=for-the-badge&logo=cisco&logoColor=white)
---
> **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).
Were 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 是一个不错的选择。
- 如果您需要部署在生产环境中,暂时不要选择 PyFrpFrp 是一个更好的选择。
日后,我们以 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

324
backup/server.py Normal file
View File

@@ -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()

295
client.py Normal file
View File

@@ -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()

388
readme.md Normal file
View File

@@ -0,0 +1,388 @@
<p align="center">
<img src="https://avatars.githubusercontent.com/u/193612261?v=4" width="270px" />
<p align="center">PyFrp</p>
<p align="center">Frp (Fast Reverse Proxy) implemented in Python</p>
</p>
![GitHub Followers](https://img.shields.io/badge/dynamic/json?color=green&label=GitHub%20Followers&query=%24.data.totalSubs&url=https%3A%2F%2Fapi.spencerwoo.com%2Fsubstats%2F%3Fsource%3Dgithub%26queryKey%3DGVSADS)
![Total Repos](https://img.shields.io/badge/dynamic/json?color=orange&label=Total%20Repos&query=%24.total_count&url=https%3A%2F%2Fapi.github.com%2Fsearch%2Frepositories%3Fq%3Duser%3AGVSADS)
---
![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white)
![Operating System](https://img.shields.io/badge/Operating%20System-000000?style=for-the-badge&logo=linux&logoColor=white)
![Server](https://img.shields.io/badge/Server-000000?style=for-the-badge&logo=serverless&logoColor=white)
![Networking](https://img.shields.io/badge/Networking-000000?style=for-the-badge&logo=cisco&logoColor=white)
---
> **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).
Were 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 是一个不错的选择。
- 如果您需要部署在生产环境中,暂时不要选择 PyFrpFrp 是一个更好的选择。
日后,我们以 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
```
你也可以直接修改源代码中的默认配置。
---
## <20> 使用示例
### 示例:暴露本地 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 修复或增强功能
### 开发指南
- 遵循现有的代码风格
- 为复杂代码添加注释
- 彻底测试你的更改
- 根据需要更新文档
---
## <20>📞 联系我们
- 📧 Email: wyt18222152539wyt@163.com
- 🌐 官网: [银河万通软件开发工作室](https://www.gvsds.com)
- 📱 微信: GVSADS
---
## 📄 许可证
本项目采用 MIT 许可证。详情请参阅 LICENSE 文件。

343
server.py Normal file
View File

@@ -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()