Spaces:
Paused
Paused
Factor Studios
commited on
Upload 73 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +52 -0
- __init__.py +0 -0
- __pycache__/__init__.cpython-311.pyc +0 -0
- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/main.cpython-311.pyc +0 -0
- __pycache__/main_isp.cpython-311.pyc +0 -0
- app.py +213 -0
- app_status.json +755 -0
- config.json +98 -0
- core/__init__.py +2 -0
- core/__pycache__/__init__.cpython-311.pyc +0 -0
- core/__pycache__/dhcp_server.cpython-311.pyc +0 -0
- core/__pycache__/firewall.cpython-311.pyc +0 -0
- core/__pycache__/ip_parser.cpython-311.pyc +0 -0
- core/__pycache__/logger.cpython-311.pyc +0 -0
- core/__pycache__/nat_engine.cpython-311.pyc +0 -0
- core/__pycache__/openvpn_manager.cpython-311.pyc +0 -0
- core/__pycache__/packet_bridge.cpython-311.pyc +0 -0
- core/__pycache__/session_tracker.cpython-311.pyc +0 -0
- core/__pycache__/socket_translator.cpython-311.pyc +0 -0
- core/__pycache__/tcp_engine.cpython-311.pyc +0 -0
- core/__pycache__/traffic_router.cpython-311.pyc +0 -0
- core/__pycache__/virtual_router.cpython-311.pyc +0 -0
- core/dhcp_server.py +391 -0
- core/firewall.py +523 -0
- core/ip_parser.py +546 -0
- core/logger.py +555 -0
- core/nat_engine.py +638 -0
- core/openvpn_manager.py +508 -0
- core/packet_bridge.py +664 -0
- core/session_tracker.py +602 -0
- core/socket_translator.py +653 -0
- core/tcp_engine.py +716 -0
- core/traffic_router.py +132 -0
- core/virtual_router.py +565 -0
- database/app.db +0 -0
- flask_app.log +0 -0
- main.py +76 -0
- main_isp.py +273 -0
- models/__pycache__/enhanced_user.cpython-311.pyc +0 -0
- models/__pycache__/user.cpython-311.pyc +0 -0
- models/enhanced_user.py +427 -0
- models/user.py +20 -0
- openvpn/ca.crt +20 -0
- openvpn/dh.pem +8 -0
- openvpn/server.conf +21 -0
- openvpn/server.crt +86 -0
- openvpn/server.key +28 -0
- requirements.txt +33 -0
- routes/__pycache__/auth.cpython-311.pyc +0 -0
Dockerfile
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Virtual ISP Stack with OpenVPN Integration
|
| 2 |
+
# Dockerfile for containerized deployment
|
| 3 |
+
|
| 4 |
+
FROM python:3.11-slim
|
| 5 |
+
|
| 6 |
+
# Set working directory
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# Install system dependencies
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
openvpn \
|
| 12 |
+
iptables \
|
| 13 |
+
iproute2 \
|
| 14 |
+
net-tools \
|
| 15 |
+
procps \
|
| 16 |
+
build-essential \
|
| 17 |
+
python3-dev \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
COPY openvpn/server.conf /etc/openvpn/server/server.conf
|
| 21 |
+
COPY openvpn/ca.crt /etc/openvpn/server/ca.crt
|
| 22 |
+
COPY openvpn/server.crt /etc/openvpn/server/server.crt
|
| 23 |
+
COPY openvpn/server.key /etc/openvpn/server/server.key
|
| 24 |
+
COPY openvpn/dh.pem /etc/openvpn/server/dh.pem
|
| 25 |
+
|
| 26 |
+
# Copy requirements and install Python dependencies
|
| 27 |
+
COPY requirements.txt .
|
| 28 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 29 |
+
|
| 30 |
+
# Copy application files
|
| 31 |
+
COPY . .
|
| 32 |
+
|
| 33 |
+
# Create necessary directories
|
| 34 |
+
RUN mkdir -p /tmp/vpn_client_configs \
|
| 35 |
+
&& mkdir -p /var/log/openvpn \
|
| 36 |
+
&& mkdir -p database
|
| 37 |
+
|
| 38 |
+
# Set environment variables
|
| 39 |
+
ENV FLASK_APP=app.py
|
| 40 |
+
ENV FLASK_ENV=production
|
| 41 |
+
ENV PORT=7860
|
| 42 |
+
|
| 43 |
+
# Expose port
|
| 44 |
+
EXPOSE 7860
|
| 45 |
+
|
| 46 |
+
# Health check
|
| 47 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 48 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 49 |
+
|
| 50 |
+
# Run the application
|
| 51 |
+
CMD ["python", "app.py"]
|
| 52 |
+
|
__init__.py
ADDED
|
File without changes
|
__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (173 Bytes). View file
|
|
|
__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (9.36 kB). View file
|
|
|
__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (3.57 kB). View file
|
|
|
__pycache__/main_isp.cpython-311.pyc
ADDED
|
Binary file (10.8 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Virtual ISP Stack with OpenVPN Integration
|
| 4 |
+
HuggingFace Spaces Entry Point
|
| 5 |
+
|
| 6 |
+
This application provides a complete Virtual ISP stack with OpenVPN server integration,
|
| 7 |
+
allowing users to manage VPN connections, generate client configurations, and monitor
|
| 8 |
+
network traffic through a RESTful API.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
+
# Add current directory to Python path
|
| 16 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 17 |
+
|
| 18 |
+
from flask import Flask, send_from_directory, jsonify
|
| 19 |
+
from flask_cors import CORS
|
| 20 |
+
from models.enhanced_user import db
|
| 21 |
+
from routes.auth import auth_bp
|
| 22 |
+
from routes.vpn_client import vpn_client_bp
|
| 23 |
+
from routes.vpn_server import vpn_server_bp
|
| 24 |
+
from routes.isp_api import init_engines, isp_api
|
| 25 |
+
|
| 26 |
+
# Configure logging
|
| 27 |
+
logging.basicConfig(
|
| 28 |
+
level=logging.INFO,
|
| 29 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 30 |
+
)
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
# Create Flask application
|
| 34 |
+
app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), 'static'))
|
| 35 |
+
|
| 36 |
+
# Enable CORS for all routes
|
| 37 |
+
CORS(app, origins="*")
|
| 38 |
+
|
| 39 |
+
# Configuration
|
| 40 |
+
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'vpn-isp-stack-secret-key-change-in-production')
|
| 41 |
+
|
| 42 |
+
# Database configuration
|
| 43 |
+
database_path = os.path.join(os.path.dirname(__file__), 'database', 'app.db')
|
| 44 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{database_path}"
|
| 45 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 46 |
+
|
| 47 |
+
# Initialize database
|
| 48 |
+
db.init_app(app)
|
| 49 |
+
|
| 50 |
+
# Register blueprints
|
| 51 |
+
app.register_blueprint(auth_bp, url_prefix='/api')
|
| 52 |
+
app.register_blueprint(vpn_client_bp, url_prefix='/api')
|
| 53 |
+
app.register_blueprint(vpn_server_bp, url_prefix='/api')
|
| 54 |
+
app.register_blueprint(isp_api, url_prefix='/api')
|
| 55 |
+
|
| 56 |
+
# Engine configuration
|
| 57 |
+
app.config.update({
|
| 58 |
+
"dhcp": {
|
| 59 |
+
"network": "10.0.0.0/24",
|
| 60 |
+
"range_start": "10.0.0.10",
|
| 61 |
+
"range_end": "10.0.0.100",
|
| 62 |
+
"lease_time": 3600,
|
| 63 |
+
"gateway": "10.0.0.1",
|
| 64 |
+
"dns_servers": ["8.8.8.8", "8.8.4.4"]
|
| 65 |
+
},
|
| 66 |
+
"nat": {
|
| 67 |
+
"port_range_start": 10000,
|
| 68 |
+
"port_range_end": 65535,
|
| 69 |
+
"session_timeout": 300
|
| 70 |
+
},
|
| 71 |
+
"firewall": {
|
| 72 |
+
"default_policy": "ACCEPT",
|
| 73 |
+
"log_blocked": True
|
| 74 |
+
},
|
| 75 |
+
"tcp": {
|
| 76 |
+
"initial_window": 65535,
|
| 77 |
+
"max_retries": 3,
|
| 78 |
+
"timeout": 30
|
| 79 |
+
},
|
| 80 |
+
"openvpn": {
|
| 81 |
+
"server_ip": "10.8.0.1",
|
| 82 |
+
"server_port": 1194,
|
| 83 |
+
"network": "10.8.0.0/24"
|
| 84 |
+
},
|
| 85 |
+
"logger": {
|
| 86 |
+
"log_level": "INFO",
|
| 87 |
+
"log_file": "/tmp/virtual_isp.log"
|
| 88 |
+
}
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
# Add VPN server configuration
|
| 92 |
+
app.config.update({
|
| 93 |
+
'VPN_SERVER_IP': os.environ.get('VPN_SERVER_IP', '127.0.0.1'),
|
| 94 |
+
'OPENVPN_PORT': int(os.environ.get('OPENVPN_PORT', 1194)),
|
| 95 |
+
'IKEV2_PORT': int(os.environ.get('IKEV2_PORT', 500)),
|
| 96 |
+
'WIREGUARD_PORT': int(os.environ.get('WIREGUARD_PORT', 51820)),
|
| 97 |
+
'WIREGUARD_SERVER_PUBLIC_KEY': os.environ.get('WIREGUARD_SERVER_PUBLIC_KEY', 'SERVER_PUBLIC_KEY_HERE')
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
# Initialize database tables
|
| 101 |
+
with app.app_context():
|
| 102 |
+
try:
|
| 103 |
+
db.create_all()
|
| 104 |
+
logger.info("Database tables created successfully")
|
| 105 |
+
except Exception as e:
|
| 106 |
+
logger.error(f"Error creating database tables: {e}")
|
| 107 |
+
|
| 108 |
+
# Initialize engines
|
| 109 |
+
try:
|
| 110 |
+
init_engines(app.config)
|
| 111 |
+
logger.info("All engines initialized successfully")
|
| 112 |
+
except Exception as e:
|
| 113 |
+
logger.error(f"Error initializing engines: {e}")
|
| 114 |
+
|
| 115 |
+
@app.route('/')
|
| 116 |
+
def index():
|
| 117 |
+
"""Main index page - redirect to auth if not logged in"""
|
| 118 |
+
return serve_static('auth.html')
|
| 119 |
+
|
| 120 |
+
@app.route('/auth')
|
| 121 |
+
def auth_page():
|
| 122 |
+
"""Authentication page"""
|
| 123 |
+
return serve_static('auth.html')
|
| 124 |
+
|
| 125 |
+
@app.route('/dashboard')
|
| 126 |
+
def dashboard_page():
|
| 127 |
+
"""Dashboard page"""
|
| 128 |
+
return serve_static('index.html')
|
| 129 |
+
|
| 130 |
+
@app.route('/health')
|
| 131 |
+
def health_check():
|
| 132 |
+
"""Health check endpoint for monitoring"""
|
| 133 |
+
return jsonify({
|
| 134 |
+
'status': 'healthy',
|
| 135 |
+
'service': 'Virtual ISP Stack with OpenVPN',
|
| 136 |
+
'version': '1.0.0'
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
@app.route('/api')
|
| 140 |
+
def api_info():
|
| 141 |
+
"""API information endpoint"""
|
| 142 |
+
return jsonify({
|
| 143 |
+
'service': 'Virtual ISP Stack API',
|
| 144 |
+
'version': '1.0.0',
|
| 145 |
+
'endpoints': {
|
| 146 |
+
'openvpn': {
|
| 147 |
+
'status': '/api/openvpn/status',
|
| 148 |
+
'start': '/api/openvpn/start',
|
| 149 |
+
'stop': '/api/openvpn/stop',
|
| 150 |
+
'clients': '/api/openvpn/clients',
|
| 151 |
+
'config': '/api/openvpn/config/<client_name>',
|
| 152 |
+
'stats': '/api/openvpn/stats',
|
| 153 |
+
'configs': '/api/openvpn/configs'
|
| 154 |
+
},
|
| 155 |
+
'dhcp': {
|
| 156 |
+
'leases': '/api/dhcp/leases'
|
| 157 |
+
},
|
| 158 |
+
'nat': {
|
| 159 |
+
'sessions': '/api/nat/sessions',
|
| 160 |
+
'stats': '/api/nat/stats'
|
| 161 |
+
},
|
| 162 |
+
'firewall': {
|
| 163 |
+
'rules': '/api/firewall/rules',
|
| 164 |
+
'logs': '/api/firewall/logs',
|
| 165 |
+
'stats': '/api/firewall/stats'
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
})
|
| 169 |
+
|
| 170 |
+
@app.route('/<path:path>')
|
| 171 |
+
def serve_static(path):
|
| 172 |
+
"""Serve static files"""
|
| 173 |
+
static_folder_path = app.static_folder
|
| 174 |
+
if static_folder_path is None:
|
| 175 |
+
return jsonify({'error': 'Static folder not configured'}), 404
|
| 176 |
+
|
| 177 |
+
if path != "" and os.path.exists(os.path.join(static_folder_path, path)):
|
| 178 |
+
return send_from_directory(static_folder_path, path)
|
| 179 |
+
else:
|
| 180 |
+
index_path = os.path.join(static_folder_path, 'index.html')
|
| 181 |
+
if os.path.exists(index_path):
|
| 182 |
+
return send_from_directory(static_folder_path, 'index.html')
|
| 183 |
+
else:
|
| 184 |
+
return jsonify({
|
| 185 |
+
'message': 'Virtual ISP Stack with OpenVPN Integration',
|
| 186 |
+
'status': 'running',
|
| 187 |
+
'api_docs': '/api'
|
| 188 |
+
})
|
| 189 |
+
|
| 190 |
+
@app.errorhandler(404)
|
| 191 |
+
def not_found(error):
|
| 192 |
+
"""Handle 404 errors"""
|
| 193 |
+
return jsonify({'error': 'Endpoint not found', 'api_docs': '/api'}), 404
|
| 194 |
+
|
| 195 |
+
@app.errorhandler(500)
|
| 196 |
+
def internal_error(error):
|
| 197 |
+
"""Handle 500 errors"""
|
| 198 |
+
return jsonify({'error': 'Internal server error'}), 500
|
| 199 |
+
|
| 200 |
+
if __name__ == '__main__':
|
| 201 |
+
# Get port from environment variable (HuggingFace Spaces uses PORT)
|
| 202 |
+
port = int(os.environ.get('PORT', 7860))
|
| 203 |
+
|
| 204 |
+
logger.info(f"Starting Virtual ISP Stack with OpenVPN on port {port}")
|
| 205 |
+
|
| 206 |
+
# Run the application
|
| 207 |
+
app.run(
|
| 208 |
+
host='0.0.0.0',
|
| 209 |
+
port=port,
|
| 210 |
+
debug=False,
|
| 211 |
+
threaded=True
|
| 212 |
+
)
|
| 213 |
+
|
app_status.json
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Virtual ISP Stack - Network Management Dashboard</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css">
|
| 8 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="app-container">
|
| 12 |
+
<!-- Header -->
|
| 13 |
+
<header class="header">
|
| 14 |
+
<div class="header-content">
|
| 15 |
+
<div class="logo">
|
| 16 |
+
<i class="fas fa-network-wired"></i>
|
| 17 |
+
<h1>Virtual ISP Stack</h1>
|
| 18 |
+
</div>
|
| 19 |
+
<div class="header-status">
|
| 20 |
+
<div class="status-indicator" id="systemStatus">
|
| 21 |
+
<i class="fas fa-circle"></i>
|
| 22 |
+
<span>System Status</span>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="refresh-btn" onclick="refreshData()">
|
| 25 |
+
<i class="fas fa-sync-alt"></i>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</header>
|
| 30 |
+
|
| 31 |
+
<!-- Navigation -->
|
| 32 |
+
<nav class="sidebar">
|
| 33 |
+
<div class="nav-menu">
|
| 34 |
+
<div class="nav-item active" data-section="dashboard">
|
| 35 |
+
<i class="fas fa-tachometer-alt"></i>
|
| 36 |
+
<span>Dashboard</span>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="nav-item" data-section="dhcp">
|
| 39 |
+
<i class="fas fa-server"></i>
|
| 40 |
+
<span>DHCP</span>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="nav-item" data-section="nat">
|
| 43 |
+
<i class="fas fa-exchange-alt"></i>
|
| 44 |
+
<span>NAT</span>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="nav-item" data-section="firewall">
|
| 47 |
+
<i class="fas fa-shield-alt"></i>
|
| 48 |
+
<span>Firewall</span>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="nav-item" data-section="router">
|
| 51 |
+
<i class="fas fa-route"></i>
|
| 52 |
+
<span>Router</span>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="nav-item" data-section="bridge">
|
| 55 |
+
<i class="fas fa-link"></i>
|
| 56 |
+
<span>Bridge</span>
|
| 57 |
+
</div>
|
| 58 |
+
<div class="nav-item" data-section="sessions">
|
| 59 |
+
<i class="fas fa-users"></i>
|
| 60 |
+
<span>Sessions</span>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="nav-item" data-section="logs">
|
| 63 |
+
<i class="fas fa-file-alt"></i>
|
| 64 |
+
<span>Logs</span>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="nav-item" data-section="vpn">
|
| 67 |
+
<i class="fas fa-shield-alt"></i>
|
| 68 |
+
<span>VPN</span>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="nav-item" data-section="config">
|
| 71 |
+
<i class="fas fa-cog"></i>
|
| 72 |
+
<span>Config</span>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
</nav>
|
| 76 |
+
|
| 77 |
+
<!-- Main Content -->
|
| 78 |
+
<main class="main-content">
|
| 79 |
+
<!-- Dashboard Section -->
|
| 80 |
+
<section id="dashboard" class="content-section active">
|
| 81 |
+
<div class="section-header">
|
| 82 |
+
<h2>System Dashboard</h2>
|
| 83 |
+
<p>Overview of Virtual ISP Stack components and performance</p>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<!-- System Status Cards -->
|
| 87 |
+
<div class="stats-grid">
|
| 88 |
+
<div class="stat-card">
|
| 89 |
+
<div class="stat-icon">
|
| 90 |
+
<i class="fas fa-server"></i>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="stat-content">
|
| 93 |
+
<h3 id="dhcpLeaseCount">0</h3>
|
| 94 |
+
<p>DHCP Leases</p>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
<div class="stat-card">
|
| 99 |
+
<div class="stat-icon">
|
| 100 |
+
<i class="fas fa-exchange-alt"></i>
|
| 101 |
+
</div>
|
| 102 |
+
<div class="stat-content">
|
| 103 |
+
<h3 id="natSessionCount">0</h3>
|
| 104 |
+
<p>NAT Sessions</p>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<div class="stat-card">
|
| 109 |
+
<div class="stat-icon">
|
| 110 |
+
<i class="fas fa-shield-alt"></i>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="stat-content">
|
| 113 |
+
<h3 id="firewallRuleCount">0</h3>
|
| 114 |
+
<p>Firewall Rules</p>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<div class="stat-card">
|
| 119 |
+
<div class="stat-icon">
|
| 120 |
+
<i class="fas fa-link"></i>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="stat-content">
|
| 123 |
+
<h3 id="bridgeClientCount">0</h3>
|
| 124 |
+
<p>Bridge Clients</p>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<!-- Component Status -->
|
| 130 |
+
<div class="component-status">
|
| 131 |
+
<h3>Component Status</h3>
|
| 132 |
+
<div class="component-grid" id="componentStatus">
|
| 133 |
+
<!-- Component status items will be populated by JavaScript -->
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<!-- Real-time Charts -->
|
| 138 |
+
<div class="charts-container">
|
| 139 |
+
<div class="chart-card">
|
| 140 |
+
<h3>Network Traffic</h3>
|
| 141 |
+
<canvas id="trafficChart"></canvas>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="chart-card">
|
| 144 |
+
<h3>Connection Distribution</h3>
|
| 145 |
+
<canvas id="connectionChart"></canvas>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
</section>
|
| 149 |
+
|
| 150 |
+
<!-- DHCP Section -->
|
| 151 |
+
<section id="dhcp" class="content-section">
|
| 152 |
+
<div class="section-header">
|
| 153 |
+
<h2>DHCP Management</h2>
|
| 154 |
+
<p>Manage DHCP leases and configuration</p>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div class="table-container">
|
| 158 |
+
<div class="table-header">
|
| 159 |
+
<h3>Active Leases</h3>
|
| 160 |
+
<button class="btn btn-secondary" onclick="refreshDHCPLeases()">
|
| 161 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 162 |
+
</button>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="table-wrapper">
|
| 165 |
+
<table id="dhcpTable">
|
| 166 |
+
<thead>
|
| 167 |
+
<tr>
|
| 168 |
+
<th>MAC Address</th>
|
| 169 |
+
<th>IP Address</th>
|
| 170 |
+
<th>Lease Time</th>
|
| 171 |
+
<th>Remaining</th>
|
| 172 |
+
<th>State</th>
|
| 173 |
+
<th>Actions</th>
|
| 174 |
+
</tr>
|
| 175 |
+
</thead>
|
| 176 |
+
<tbody id="dhcpTableBody">
|
| 177 |
+
<!-- DHCP leases will be populated here -->
|
| 178 |
+
</tbody>
|
| 179 |
+
</table>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</section>
|
| 183 |
+
|
| 184 |
+
<!-- NAT Section -->
|
| 185 |
+
<section id="nat" class="content-section">
|
| 186 |
+
<div class="section-header">
|
| 187 |
+
<h2>NAT Management</h2>
|
| 188 |
+
<p>Network Address Translation sessions and statistics</p>
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
<div class="nat-stats">
|
| 192 |
+
<div class="stat-row">
|
| 193 |
+
<div class="stat-item">
|
| 194 |
+
<span class="stat-label">Active Sessions:</span>
|
| 195 |
+
<span class="stat-value" id="natActiveSessions">0</span>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="stat-item">
|
| 198 |
+
<span class="stat-label">Port Utilization:</span>
|
| 199 |
+
<span class="stat-value" id="natPortUtilization">0%</span>
|
| 200 |
+
</div>
|
| 201 |
+
<div class="stat-item">
|
| 202 |
+
<span class="stat-label">Bytes Translated:</span>
|
| 203 |
+
<span class="stat-value" id="natBytesTranslated">0</span>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div class="table-container">
|
| 209 |
+
<div class="table-header">
|
| 210 |
+
<h3>NAT Sessions</h3>
|
| 211 |
+
<button class="btn btn-secondary" onclick="refreshNATSessions()">
|
| 212 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 213 |
+
</button>
|
| 214 |
+
</div>
|
| 215 |
+
<div class="table-wrapper">
|
| 216 |
+
<table id="natTable">
|
| 217 |
+
<thead>
|
| 218 |
+
<tr>
|
| 219 |
+
<th>Virtual IP:Port</th>
|
| 220 |
+
<th>Real IP:Port</th>
|
| 221 |
+
<th>Host IP:Port</th>
|
| 222 |
+
<th>Protocol</th>
|
| 223 |
+
<th>Duration</th>
|
| 224 |
+
<th>Bytes In/Out</th>
|
| 225 |
+
<th>Actions</th>
|
| 226 |
+
</tr>
|
| 227 |
+
</thead>
|
| 228 |
+
<tbody id="natTableBody">
|
| 229 |
+
<!-- NAT sessions will be populated here -->
|
| 230 |
+
</tbody>
|
| 231 |
+
</table>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
</section>
|
| 235 |
+
|
| 236 |
+
<!-- Firewall Section -->
|
| 237 |
+
<section id="firewall" class="content-section">
|
| 238 |
+
<div class="section-header">
|
| 239 |
+
<h2>Firewall Management</h2>
|
| 240 |
+
<p>Configure firewall rules and monitor traffic</p>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<div class="firewall-controls">
|
| 244 |
+
<button class="btn btn-primary" onclick="showAddRuleModal()">
|
| 245 |
+
<i class="fas fa-plus"></i> Add Rule
|
| 246 |
+
</button>
|
| 247 |
+
<button class="btn btn-secondary" onclick="refreshFirewallRules()">
|
| 248 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 249 |
+
</button>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
<div class="table-container">
|
| 253 |
+
<div class="table-header">
|
| 254 |
+
<h3>Firewall Rules</h3>
|
| 255 |
+
</div>
|
| 256 |
+
<div class="table-wrapper">
|
| 257 |
+
<table id="firewallTable">
|
| 258 |
+
<thead>
|
| 259 |
+
<tr>
|
| 260 |
+
<th>Priority</th>
|
| 261 |
+
<th>Rule ID</th>
|
| 262 |
+
<th>Action</th>
|
| 263 |
+
<th>Direction</th>
|
| 264 |
+
<th>Source</th>
|
| 265 |
+
<th>Destination</th>
|
| 266 |
+
<th>Protocol</th>
|
| 267 |
+
<th>Hits</th>
|
| 268 |
+
<th>Status</th>
|
| 269 |
+
<th>Actions</th>
|
| 270 |
+
</tr>
|
| 271 |
+
</thead>
|
| 272 |
+
<tbody id="firewallTableBody">
|
| 273 |
+
<!-- Firewall rules will be populated here -->
|
| 274 |
+
</tbody>
|
| 275 |
+
</table>
|
| 276 |
+
</div>
|
| 277 |
+
</div>
|
| 278 |
+
</section>
|
| 279 |
+
|
| 280 |
+
<!-- Router Section -->
|
| 281 |
+
<section id="router" class="content-section">
|
| 282 |
+
<div class="section-header">
|
| 283 |
+
<h2>Router Management</h2>
|
| 284 |
+
<p>Routing table and network interfaces</p>
|
| 285 |
+
</div>
|
| 286 |
+
|
| 287 |
+
<div class="router-tabs">
|
| 288 |
+
<div class="tab-buttons">
|
| 289 |
+
<button class="tab-btn active" data-tab="routes">Routes</button>
|
| 290 |
+
<button class="tab-btn" data-tab="interfaces">Interfaces</button>
|
| 291 |
+
<button class="tab-btn" data-tab="arp">ARP Table</button>
|
| 292 |
+
</div>
|
| 293 |
+
|
| 294 |
+
<div class="tab-content">
|
| 295 |
+
<div id="routes" class="tab-pane active">
|
| 296 |
+
<div class="table-container">
|
| 297 |
+
<table id="routesTable">
|
| 298 |
+
<thead>
|
| 299 |
+
<tr>
|
| 300 |
+
<th>Destination</th>
|
| 301 |
+
<th>Gateway</th>
|
| 302 |
+
<th>Interface</th>
|
| 303 |
+
<th>Metric</th>
|
| 304 |
+
<th>Type</th>
|
| 305 |
+
<th>Use Count</th>
|
| 306 |
+
<th>Last Used</th>
|
| 307 |
+
</tr>
|
| 308 |
+
</thead>
|
| 309 |
+
<tbody id="routesTableBody">
|
| 310 |
+
<!-- Routes will be populated here -->
|
| 311 |
+
</tbody>
|
| 312 |
+
</table>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<div id="interfaces" class="tab-pane">
|
| 317 |
+
<div class="table-container">
|
| 318 |
+
<table id="interfacesTable">
|
| 319 |
+
<thead>
|
| 320 |
+
<tr>
|
| 321 |
+
<th>Name</th>
|
| 322 |
+
<th>IP Address</th>
|
| 323 |
+
<th>Network</th>
|
| 324 |
+
<th>MTU</th>
|
| 325 |
+
<th>Status</th>
|
| 326 |
+
<th>Actions</th>
|
| 327 |
+
</tr>
|
| 328 |
+
</thead>
|
| 329 |
+
<tbody id="interfacesTableBody">
|
| 330 |
+
<!-- Interfaces will be populated here -->
|
| 331 |
+
</tbody>
|
| 332 |
+
</table>
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
|
| 336 |
+
<div id="arp" class="tab-pane">
|
| 337 |
+
<div class="table-container">
|
| 338 |
+
<table id="arpTable">
|
| 339 |
+
<thead>
|
| 340 |
+
<tr>
|
| 341 |
+
<th>IP Address</th>
|
| 342 |
+
<th>MAC Address</th>
|
| 343 |
+
<th>Actions</th>
|
| 344 |
+
</tr>
|
| 345 |
+
</thead>
|
| 346 |
+
<tbody id="arpTableBody">
|
| 347 |
+
<!-- ARP entries will be populated here -->
|
| 348 |
+
</tbody>
|
| 349 |
+
</table>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
</section>
|
| 355 |
+
|
| 356 |
+
<!-- Bridge Section -->
|
| 357 |
+
<section id="bridge" class="content-section">
|
| 358 |
+
<div class="section-header">
|
| 359 |
+
<h2>Packet Bridge</h2>
|
| 360 |
+
<p>Connected clients and bridge statistics</p>
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
<div class="bridge-info">
|
| 364 |
+
<div class="info-card">
|
| 365 |
+
<h4>WebSocket Server</h4>
|
| 366 |
+
<p>Port: 8765</p>
|
| 367 |
+
<p>Status: <span class="status-active">Active</span></p>
|
| 368 |
+
</div>
|
| 369 |
+
<div class="info-card">
|
| 370 |
+
<h4>TCP Server</h4>
|
| 371 |
+
<p>Port: 8766</p>
|
| 372 |
+
<p>Status: <span class="status-active">Active</span></p>
|
| 373 |
+
</div>
|
| 374 |
+
</div>
|
| 375 |
+
|
| 376 |
+
<div class="table-container">
|
| 377 |
+
<div class="table-header">
|
| 378 |
+
<h3>Connected Clients</h3>
|
| 379 |
+
<button class="btn btn-secondary" onclick="refreshBridgeClients()">
|
| 380 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 381 |
+
</button>
|
| 382 |
+
</div>
|
| 383 |
+
<div class="table-wrapper">
|
| 384 |
+
<table id="bridgeTable">
|
| 385 |
+
<thead>
|
| 386 |
+
<tr>
|
| 387 |
+
<th>Client ID</th>
|
| 388 |
+
<th>Type</th>
|
| 389 |
+
<th>Remote Address</th>
|
| 390 |
+
<th>Connected Time</th>
|
| 391 |
+
<th>Packets In/Out</th>
|
| 392 |
+
<th>Bytes In/Out</th>
|
| 393 |
+
<th>Actions</th>
|
| 394 |
+
</tr>
|
| 395 |
+
</thead>
|
| 396 |
+
<tbody id="bridgeTableBody">
|
| 397 |
+
<!-- Bridge clients will be populated here -->
|
| 398 |
+
</tbody>
|
| 399 |
+
</table>
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
</section>
|
| 403 |
+
|
| 404 |
+
<!-- Sessions Section -->
|
| 405 |
+
<section id="sessions" class="content-section">
|
| 406 |
+
<div class="section-header">
|
| 407 |
+
<h2>Session Tracking</h2>
|
| 408 |
+
<p>Unified view of all network sessions</p>
|
| 409 |
+
</div>
|
| 410 |
+
|
| 411 |
+
<div class="session-summary" id="sessionSummary">
|
| 412 |
+
<!-- Session summary will be populated here -->
|
| 413 |
+
</div>
|
| 414 |
+
|
| 415 |
+
<div class="table-container">
|
| 416 |
+
<div class="table-header">
|
| 417 |
+
<h3>Active Sessions</h3>
|
| 418 |
+
<div class="table-controls">
|
| 419 |
+
<select id="sessionTypeFilter" onchange="filterSessions()">
|
| 420 |
+
<option value="">All Types</option>
|
| 421 |
+
<option value="DHCP_LEASE">DHCP Lease</option>
|
| 422 |
+
<option value="NAT_SESSION">NAT Session</option>
|
| 423 |
+
<option value="TCP_CONNECTION">TCP Connection</option>
|
| 424 |
+
<option value="SOCKET_CONNECTION">Socket Connection</option>
|
| 425 |
+
<option value="BRIDGE_CLIENT">Bridge Client</option>
|
| 426 |
+
</select>
|
| 427 |
+
<button class="btn btn-secondary" onclick="refreshSessions()">
|
| 428 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 429 |
+
</button>
|
| 430 |
+
</div>
|
| 431 |
+
</div>
|
| 432 |
+
<div class="table-wrapper">
|
| 433 |
+
<table id="sessionsTable">
|
| 434 |
+
<thead>
|
| 435 |
+
<tr>
|
| 436 |
+
<th>Session ID</th>
|
| 437 |
+
<th>Type</th>
|
| 438 |
+
<th>State</th>
|
| 439 |
+
<th>Virtual IP:Port</th>
|
| 440 |
+
<th>Real IP:Port</th>
|
| 441 |
+
<th>Protocol</th>
|
| 442 |
+
<th>Duration</th>
|
| 443 |
+
<th>Idle Time</th>
|
| 444 |
+
<th>Metrics</th>
|
| 445 |
+
</tr>
|
| 446 |
+
</thead>
|
| 447 |
+
<tbody id="sessionsTableBody">
|
| 448 |
+
<!-- Sessions will be populated here -->
|
| 449 |
+
</tbody>
|
| 450 |
+
</table>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
</section>
|
| 454 |
+
|
| 455 |
+
<!-- Logs Section -->
|
| 456 |
+
<section id="logs" class="content-section">
|
| 457 |
+
<div class="section-header">
|
| 458 |
+
<h2>System Logs</h2>
|
| 459 |
+
<p>Monitor system events and troubleshoot issues</p>
|
| 460 |
+
</div>
|
| 461 |
+
|
| 462 |
+
<div class="log-controls">
|
| 463 |
+
<div class="log-filters">
|
| 464 |
+
<select id="logLevelFilter" onchange="filterLogs()">
|
| 465 |
+
<option value="">All Levels</option>
|
| 466 |
+
<option value="DEBUG">Debug</option>
|
| 467 |
+
<option value="INFO">Info</option>
|
| 468 |
+
<option value="WARNING">Warning</option>
|
| 469 |
+
<option value="ERROR">Error</option>
|
| 470 |
+
<option value="CRITICAL">Critical</option>
|
| 471 |
+
</select>
|
| 472 |
+
<select id="logCategoryFilter" onchange="filterLogs()">
|
| 473 |
+
<option value="">All Categories</option>
|
| 474 |
+
<option value="SYSTEM">System</option>
|
| 475 |
+
<option value="DHCP">DHCP</option>
|
| 476 |
+
<option value="NAT">NAT</option>
|
| 477 |
+
<option value="FIREWALL">Firewall</option>
|
| 478 |
+
<option value="TCP">TCP</option>
|
| 479 |
+
<option value="ROUTER">Router</option>
|
| 480 |
+
<option value="BRIDGE">Bridge</option>
|
| 481 |
+
<option value="SOCKET">Socket</option>
|
| 482 |
+
<option value="SESSION">Session</option>
|
| 483 |
+
<option value="SECURITY">Security</option>
|
| 484 |
+
</select>
|
| 485 |
+
<input type="text" id="logSearchInput" placeholder="Search logs..." onkeyup="searchLogs()">
|
| 486 |
+
</div>
|
| 487 |
+
<div class="log-actions">
|
| 488 |
+
<button class="btn btn-secondary" onclick="refreshLogs()">
|
| 489 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 490 |
+
</button>
|
| 491 |
+
<button class="btn btn-danger" onclick="clearLogs()">
|
| 492 |
+
<i class="fas fa-trash"></i> Clear
|
| 493 |
+
</button>
|
| 494 |
+
</div>
|
| 495 |
+
</div>
|
| 496 |
+
|
| 497 |
+
<div class="log-container" id="logContainer">
|
| 498 |
+
<!-- Log entries will be populated here -->
|
| 499 |
+
</div>
|
| 500 |
+
</section>
|
| 501 |
+
|
| 502 |
+
<!-- VPN Section -->
|
| 503 |
+
<section id="vpn" class="content-section">
|
| 504 |
+
<div class="section-header">
|
| 505 |
+
<h2>VPN Management</h2>
|
| 506 |
+
<p>OpenVPN server management and client connections</p>
|
| 507 |
+
</div>
|
| 508 |
+
|
| 509 |
+
<!-- VPN Server Status -->
|
| 510 |
+
<div class="vpn-status">
|
| 511 |
+
<div class="status-card">
|
| 512 |
+
<div class="status-header">
|
| 513 |
+
<h3>OpenVPN Server</h3>
|
| 514 |
+
<div class="server-controls">
|
| 515 |
+
<button class="btn btn-success" id="startVpnBtn" onclick="startVpnServer()">
|
| 516 |
+
<i class="fas fa-play"></i> Start
|
| 517 |
+
</button>
|
| 518 |
+
<button class="btn btn-danger" id="stopVpnBtn" onclick="stopVpnServer()">
|
| 519 |
+
<i class="fas fa-stop"></i> Stop
|
| 520 |
+
</button>
|
| 521 |
+
<button class="btn btn-secondary" onclick="refreshVpnStatus()">
|
| 522 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 523 |
+
</button>
|
| 524 |
+
</div>
|
| 525 |
+
</div>
|
| 526 |
+
<div class="status-info">
|
| 527 |
+
<div class="info-row">
|
| 528 |
+
<span class="info-label">Status:</span>
|
| 529 |
+
<span class="info-value" id="vpnServerStatus">Unknown</span>
|
| 530 |
+
</div>
|
| 531 |
+
<div class="info-row">
|
| 532 |
+
<span class="info-label">Server IP:</span>
|
| 533 |
+
<span class="info-value" id="vpnServerIp">-</span>
|
| 534 |
+
</div>
|
| 535 |
+
<div class="info-row">
|
| 536 |
+
<span class="info-label">Port:</span>
|
| 537 |
+
<span class="info-value" id="vpnServerPort">-</span>
|
| 538 |
+
</div>
|
| 539 |
+
<div class="info-row">
|
| 540 |
+
<span class="info-label">Connected Clients:</span>
|
| 541 |
+
<span class="info-value" id="vpnConnectedClients">0</span>
|
| 542 |
+
</div>
|
| 543 |
+
<div class="info-row">
|
| 544 |
+
<span class="info-label">Uptime:</span>
|
| 545 |
+
<span class="info-value" id="vpnUptime">-</span>
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
</div>
|
| 550 |
+
|
| 551 |
+
<!-- VPN Statistics -->
|
| 552 |
+
<div class="vpn-stats">
|
| 553 |
+
<div class="stat-item">
|
| 554 |
+
<span class="stat-label">Total Bytes Received:</span>
|
| 555 |
+
<span class="stat-value" id="vpnBytesReceived">0</span>
|
| 556 |
+
</div>
|
| 557 |
+
<div class="stat-item">
|
| 558 |
+
<span class="stat-label">Total Bytes Sent:</span>
|
| 559 |
+
<span class="stat-value" id="vpnBytesSent">0</span>
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
|
| 563 |
+
<!-- Connected Clients -->
|
| 564 |
+
<div class="table-container">
|
| 565 |
+
<div class="table-header">
|
| 566 |
+
<h3>Connected VPN Clients</h3>
|
| 567 |
+
<div class="table-controls">
|
| 568 |
+
<button class="btn btn-primary" onclick="showGenerateConfigModal()">
|
| 569 |
+
<i class="fas fa-plus"></i> Generate Client Config
|
| 570 |
+
</button>
|
| 571 |
+
<button class="btn btn-secondary" onclick="refreshVpnClients()">
|
| 572 |
+
<i class="fas fa-sync-alt"></i> Refresh
|
| 573 |
+
</button>
|
| 574 |
+
</div>
|
| 575 |
+
</div>
|
| 576 |
+
<div class="table-wrapper">
|
| 577 |
+
<table id="vpnClientsTable">
|
| 578 |
+
<thead>
|
| 579 |
+
<tr>
|
| 580 |
+
<th>Client ID</th>
|
| 581 |
+
<th>Common Name</th>
|
| 582 |
+
<th>VPN IP Address</th>
|
| 583 |
+
<th>Connected Since</th>
|
| 584 |
+
<th>Bytes Received</th>
|
| 585 |
+
<th>Bytes Sent</th>
|
| 586 |
+
<th>Status</th>
|
| 587 |
+
<th>Actions</th>
|
| 588 |
+
</tr>
|
| 589 |
+
</thead>
|
| 590 |
+
<tbody id="vpnClientsTableBody">
|
| 591 |
+
<!-- VPN clients will be populated here -->
|
| 592 |
+
</tbody>
|
| 593 |
+
</table>
|
| 594 |
+
</div>
|
| 595 |
+
</div>
|
| 596 |
+
</section>
|
| 597 |
+
|
| 598 |
+
<!-- Config Section -->
|
| 599 |
+
<section id="config" class="content-section">
|
| 600 |
+
<div class="section-header">
|
| 601 |
+
<h2>System Configuration</h2>
|
| 602 |
+
<p>Configure system parameters and settings</p>
|
| 603 |
+
</div>
|
| 604 |
+
|
| 605 |
+
<div class="config-container">
|
| 606 |
+
<div class="config-section">
|
| 607 |
+
<h3>DHCP Configuration</h3>
|
| 608 |
+
<div class="config-form" id="dhcpConfig">
|
| 609 |
+
<!-- DHCP config form will be populated here -->
|
| 610 |
+
</div>
|
| 611 |
+
</div>
|
| 612 |
+
|
| 613 |
+
<div class="config-section">
|
| 614 |
+
<h3>NAT Configuration</h3>
|
| 615 |
+
<div class="config-form" id="natConfig">
|
| 616 |
+
<!-- NAT config form will be populated here -->
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
|
| 620 |
+
<div class="config-section">
|
| 621 |
+
<h3>Firewall Configuration</h3>
|
| 622 |
+
<div class="config-form" id="firewallConfig">
|
| 623 |
+
<!-- Firewall config form will be populated here -->
|
| 624 |
+
</div>
|
| 625 |
+
</div>
|
| 626 |
+
</div>
|
| 627 |
+
|
| 628 |
+
<div class="config-actions">
|
| 629 |
+
<button class="btn btn-primary" onclick="saveConfiguration()">
|
| 630 |
+
<i class="fas fa-save"></i> Save Configuration
|
| 631 |
+
</button>
|
| 632 |
+
<button class="btn btn-secondary" onclick="resetConfiguration()">
|
| 633 |
+
<i class="fas fa-undo"></i> Reset to Defaults
|
| 634 |
+
</button>
|
| 635 |
+
</div>
|
| 636 |
+
</section>
|
| 637 |
+
</main>
|
| 638 |
+
</div>
|
| 639 |
+
|
| 640 |
+
<!-- Modals -->
|
| 641 |
+
<div id="addRuleModal" class="modal">
|
| 642 |
+
<div class="modal-content">
|
| 643 |
+
<div class="modal-header">
|
| 644 |
+
<h3>Add Firewall Rule</h3>
|
| 645 |
+
<span class="close" onclick="closeModal('addRuleModal')">×</span>
|
| 646 |
+
</div>
|
| 647 |
+
<div class="modal-body">
|
| 648 |
+
<form id="addRuleForm">
|
| 649 |
+
<div class="form-group">
|
| 650 |
+
<label for="ruleId">Rule ID:</label>
|
| 651 |
+
<input type="text" id="ruleId" name="ruleId" required>
|
| 652 |
+
</div>
|
| 653 |
+
<div class="form-group">
|
| 654 |
+
<label for="rulePriority">Priority:</label>
|
| 655 |
+
<input type="number" id="rulePriority" name="priority" value="100" required>
|
| 656 |
+
</div>
|
| 657 |
+
<div class="form-group">
|
| 658 |
+
<label for="ruleAction">Action:</label>
|
| 659 |
+
<select id="ruleAction" name="action" required>
|
| 660 |
+
<option value="ACCEPT">Accept</option>
|
| 661 |
+
<option value="DROP">Drop</option>
|
| 662 |
+
<option value="REJECT">Reject</option>
|
| 663 |
+
</select>
|
| 664 |
+
</div>
|
| 665 |
+
<div class="form-group">
|
| 666 |
+
<label for="ruleDirection">Direction:</label>
|
| 667 |
+
<select id="ruleDirection" name="direction">
|
| 668 |
+
<option value="BOTH">Both</option>
|
| 669 |
+
<option value="INBOUND">Inbound</option>
|
| 670 |
+
<option value="OUTBOUND">Outbound</option>
|
| 671 |
+
</select>
|
| 672 |
+
</div>
|
| 673 |
+
<div class="form-group">
|
| 674 |
+
<label for="ruleSourceIp">Source IP:</label>
|
| 675 |
+
<input type="text" id="ruleSourceIp" name="source_ip" placeholder="e.g., 192.168.1.0/24">
|
| 676 |
+
</div>
|
| 677 |
+
<div class="form-group">
|
| 678 |
+
<label for="ruleDestIp">Destination IP:</label>
|
| 679 |
+
<input type="text" id="ruleDestIp" name="dest_ip" placeholder="e.g., 10.0.0.0/8">
|
| 680 |
+
</div>
|
| 681 |
+
<div class="form-group">
|
| 682 |
+
<label for="ruleSourcePort">Source Port:</label>
|
| 683 |
+
<input type="text" id="ruleSourcePort" name="source_port" placeholder="e.g., 80, 80-90, 80,443">
|
| 684 |
+
</div>
|
| 685 |
+
<div class="form-group">
|
| 686 |
+
<label for="ruleDestPort">Destination Port:</label>
|
| 687 |
+
<input type="text" id="ruleDestPort" name="dest_port" placeholder="e.g., 80, 80-90, 80,443">
|
| 688 |
+
</div>
|
| 689 |
+
<div class="form-group">
|
| 690 |
+
<label for="ruleProtocol">Protocol:</label>
|
| 691 |
+
<select id="ruleProtocol" name="protocol">
|
| 692 |
+
<option value="">Any</option>
|
| 693 |
+
<option value="TCP">TCP</option>
|
| 694 |
+
<option value="UDP">UDP</option>
|
| 695 |
+
<option value="ICMP">ICMP</option>
|
| 696 |
+
</select>
|
| 697 |
+
</div>
|
| 698 |
+
<div class="form-group">
|
| 699 |
+
<label for="ruleDescription">Description:</label>
|
| 700 |
+
<input type="text" id="ruleDescription" name="description" placeholder="Rule description">
|
| 701 |
+
</div>
|
| 702 |
+
</form>
|
| 703 |
+
</div>
|
| 704 |
+
<div class="modal-footer">
|
| 705 |
+
<button type="button" class="btn btn-secondary" onclick="closeModal('addRuleModal')">Cancel</button>
|
| 706 |
+
<button type="button" class="btn btn-primary" onclick="addFirewallRule()">Add Rule</button>
|
| 707 |
+
</div>
|
| 708 |
+
</div>
|
| 709 |
+
</div>
|
| 710 |
+
|
| 711 |
+
<!-- Generate VPN Config Modal -->
|
| 712 |
+
<div id="generateConfigModal" class="modal">
|
| 713 |
+
<div class="modal-content">
|
| 714 |
+
<div class="modal-header">
|
| 715 |
+
<h3>Generate VPN Client Configuration</h3>
|
| 716 |
+
<span class="close" onclick="closeModal('generateConfigModal')">×</span>
|
| 717 |
+
</div>
|
| 718 |
+
<div class="modal-body">
|
| 719 |
+
<form id="generateConfigForm">
|
| 720 |
+
<div class="form-group">
|
| 721 |
+
<label for="clientName">Client Name:</label>
|
| 722 |
+
<input type="text" id="clientName" name="clientName" required
|
| 723 |
+
placeholder="Enter client name (e.g., client1)">
|
| 724 |
+
</div>
|
| 725 |
+
<div class="form-group">
|
| 726 |
+
<label for="serverIp">Server IP:</label>
|
| 727 |
+
<input type="text" id="serverIp" name="serverIp" required
|
| 728 |
+
placeholder="Enter server IP address">
|
| 729 |
+
</div>
|
| 730 |
+
</form>
|
| 731 |
+
</div>
|
| 732 |
+
<div class="modal-footer">
|
| 733 |
+
<button type="button" class="btn btn-secondary" onclick="closeModal('generateConfigModal')">Cancel</button>
|
| 734 |
+
<button type="button" class="btn btn-primary" onclick="generateClientConfig()">Generate Config</button>
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
</div>
|
| 738 |
+
|
| 739 |
+
<!-- Loading Overlay -->
|
| 740 |
+
<div id="loadingOverlay" class="loading-overlay">
|
| 741 |
+
<div class="loading-spinner">
|
| 742 |
+
<i class="fas fa-spinner fa-spin"></i>
|
| 743 |
+
<p>Loading...</p>
|
| 744 |
+
</div>
|
| 745 |
+
</div>
|
| 746 |
+
|
| 747 |
+
<!-- Toast Notifications -->
|
| 748 |
+
<div id="toastContainer" class="toast-container"></div>
|
| 749 |
+
|
| 750 |
+
<!-- JavaScript -->
|
| 751 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 752 |
+
<script src="app.js"></script>
|
| 753 |
+
</body>
|
| 754 |
+
</html>
|
| 755 |
+
|
config.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dhcp": {
|
| 3 |
+
"network": "10.0.0.0/24",
|
| 4 |
+
"range_start": "10.0.0.10",
|
| 5 |
+
"range_end": "10.0.0.100",
|
| 6 |
+
"lease_time": 3600,
|
| 7 |
+
"gateway": "10.0.0.1",
|
| 8 |
+
"dns_servers": [
|
| 9 |
+
"8.8.8.8",
|
| 10 |
+
"8.8.4.4"
|
| 11 |
+
]
|
| 12 |
+
},
|
| 13 |
+
"nat": {
|
| 14 |
+
"port_range_start": 10000,
|
| 15 |
+
"port_range_end": 65535,
|
| 16 |
+
"session_timeout": 300,
|
| 17 |
+
"host_ip": "0.0.0.0"
|
| 18 |
+
},
|
| 19 |
+
"firewall": {
|
| 20 |
+
"default_policy": "ACCEPT",
|
| 21 |
+
"log_blocked": true,
|
| 22 |
+
"log_accepted": false,
|
| 23 |
+
"max_log_entries": 10000,
|
| 24 |
+
"rules": [
|
| 25 |
+
{
|
| 26 |
+
"rule_id": "allow_dhcp",
|
| 27 |
+
"priority": 1,
|
| 28 |
+
"action": "ACCEPT",
|
| 29 |
+
"direction": "BOTH",
|
| 30 |
+
"dest_port": "67,68",
|
| 31 |
+
"protocol": "UDP",
|
| 32 |
+
"description": "Allow DHCP traffic",
|
| 33 |
+
"enabled": true
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"rule_id": "allow_dns",
|
| 37 |
+
"priority": 2,
|
| 38 |
+
"action": "ACCEPT",
|
| 39 |
+
"direction": "BOTH",
|
| 40 |
+
"dest_port": "53",
|
| 41 |
+
"protocol": "UDP",
|
| 42 |
+
"description": "Allow DNS traffic",
|
| 43 |
+
"enabled": true
|
| 44 |
+
}
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
"tcp": {
|
| 48 |
+
"initial_window": 65535,
|
| 49 |
+
"max_retries": 3,
|
| 50 |
+
"timeout": 300,
|
| 51 |
+
"time_wait_timeout": 120,
|
| 52 |
+
"mss": 1460
|
| 53 |
+
},
|
| 54 |
+
"router": {
|
| 55 |
+
"router_id": "virtual-isp-router",
|
| 56 |
+
"default_gateway": "10.0.0.1",
|
| 57 |
+
"interfaces": [
|
| 58 |
+
{
|
| 59 |
+
"name": "virtual0",
|
| 60 |
+
"ip_address": "10.0.0.1",
|
| 61 |
+
"netmask": "255.255.255.0",
|
| 62 |
+
"enabled": true,
|
| 63 |
+
"mtu": 1500
|
| 64 |
+
}
|
| 65 |
+
],
|
| 66 |
+
"static_routes": []
|
| 67 |
+
},
|
| 68 |
+
"socket_translator": {
|
| 69 |
+
"connect_timeout": 10,
|
| 70 |
+
"read_timeout": 30,
|
| 71 |
+
"max_connections": 1000,
|
| 72 |
+
"buffer_size": 8192
|
| 73 |
+
},
|
| 74 |
+
"packet_bridge": {
|
| 75 |
+
"websocket_host": "0.0.0.0",
|
| 76 |
+
"websocket_port": 8765,
|
| 77 |
+
"tcp_host": "0.0.0.0",
|
| 78 |
+
"tcp_port": 8766,
|
| 79 |
+
"max_clients": 100,
|
| 80 |
+
"client_timeout": 300
|
| 81 |
+
},
|
| 82 |
+
"session_tracker": {
|
| 83 |
+
"max_sessions": 10000,
|
| 84 |
+
"session_timeout": 3600,
|
| 85 |
+
"cleanup_interval": 300,
|
| 86 |
+
"metrics_retention": 86400
|
| 87 |
+
},
|
| 88 |
+
"logger": {
|
| 89 |
+
"log_level": "INFO",
|
| 90 |
+
"log_to_file": true,
|
| 91 |
+
"log_file_path": "/tmp/virtual_isp.log",
|
| 92 |
+
"log_file_max_size": 10485760,
|
| 93 |
+
"log_file_backup_count": 5,
|
| 94 |
+
"log_to_console": true,
|
| 95 |
+
"structured_logging": true,
|
| 96 |
+
"max_memory_logs": 10000
|
| 97 |
+
}
|
| 98 |
+
}
|
core/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core networking modules for the virtual ISP stack
|
| 2 |
+
|
core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (154 Bytes). View file
|
|
|
core/__pycache__/dhcp_server.cpython-311.pyc
ADDED
|
Binary file (21.2 kB). View file
|
|
|
core/__pycache__/firewall.cpython-311.pyc
ADDED
|
Binary file (27.4 kB). View file
|
|
|
core/__pycache__/ip_parser.cpython-311.pyc
ADDED
|
Binary file (23 kB). View file
|
|
|
core/__pycache__/logger.cpython-311.pyc
ADDED
|
Binary file (29.4 kB). View file
|
|
|
core/__pycache__/nat_engine.cpython-311.pyc
ADDED
|
Binary file (34.9 kB). View file
|
|
|
core/__pycache__/openvpn_manager.cpython-311.pyc
ADDED
|
Binary file (25.7 kB). View file
|
|
|
core/__pycache__/packet_bridge.cpython-311.pyc
ADDED
|
Binary file (34.3 kB). View file
|
|
|
core/__pycache__/session_tracker.cpython-311.pyc
ADDED
|
Binary file (33.9 kB). View file
|
|
|
core/__pycache__/socket_translator.cpython-311.pyc
ADDED
|
Binary file (32.8 kB). View file
|
|
|
core/__pycache__/tcp_engine.cpython-311.pyc
ADDED
|
Binary file (33.1 kB). View file
|
|
|
core/__pycache__/traffic_router.cpython-311.pyc
ADDED
|
Binary file (8.18 kB). View file
|
|
|
core/__pycache__/virtual_router.cpython-311.pyc
ADDED
|
Binary file (30.7 kB). View file
|
|
|
core/dhcp_server.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DHCP Server Module
|
| 3 |
+
|
| 4 |
+
Implements a user-space DHCP server that handles:
|
| 5 |
+
- DHCP DISCOVER → OFFER → REQUEST → ACK sequence
|
| 6 |
+
- IP lease management
|
| 7 |
+
- Lease renewals and expiration
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import struct
|
| 11 |
+
import time
|
| 12 |
+
import socket
|
| 13 |
+
import threading
|
| 14 |
+
from typing import Dict, Optional, Tuple
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from enum import Enum
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class DHCPMessageType(Enum):
|
| 20 |
+
DISCOVER = 1
|
| 21 |
+
OFFER = 2
|
| 22 |
+
REQUEST = 3
|
| 23 |
+
DECLINE = 4
|
| 24 |
+
ACK = 5
|
| 25 |
+
NAK = 6
|
| 26 |
+
RELEASE = 7
|
| 27 |
+
INFORM = 8
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class DHCPLease:
|
| 32 |
+
"""Represents a DHCP lease"""
|
| 33 |
+
mac_address: str
|
| 34 |
+
ip_address: str
|
| 35 |
+
lease_time: int
|
| 36 |
+
lease_start: float
|
| 37 |
+
state: str = 'BOUND'
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def is_expired(self) -> bool:
|
| 41 |
+
return time.time() > (self.lease_start + self.lease_time)
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def remaining_time(self) -> int:
|
| 45 |
+
remaining = int((self.lease_start + self.lease_time) - time.time())
|
| 46 |
+
return max(0, remaining)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class DHCPPacket:
|
| 50 |
+
"""DHCP packet parser and builder"""
|
| 51 |
+
|
| 52 |
+
def __init__(self):
|
| 53 |
+
self.op = 0 # Message op code / message type
|
| 54 |
+
self.htype = 1 # Hardware address type (Ethernet = 1)
|
| 55 |
+
self.hlen = 6 # Hardware address length
|
| 56 |
+
self.hops = 0 # Hops
|
| 57 |
+
self.xid = 0 # Transaction ID
|
| 58 |
+
self.secs = 0 # Seconds elapsed
|
| 59 |
+
self.flags = 0 # Flags
|
| 60 |
+
self.ciaddr = '0.0.0.0' # Client IP address
|
| 61 |
+
self.yiaddr = '0.0.0.0' # Your IP address
|
| 62 |
+
self.siaddr = '0.0.0.0' # Server IP address
|
| 63 |
+
self.giaddr = '0.0.0.0' # Gateway IP address
|
| 64 |
+
self.chaddr = b'\x00' * 16 # Client hardware address
|
| 65 |
+
self.sname = b'\x00' * 64 # Server name
|
| 66 |
+
self.file = b'\x00' * 128 # Boot file name
|
| 67 |
+
self.options = {} # DHCP options
|
| 68 |
+
|
| 69 |
+
@classmethod
|
| 70 |
+
def parse(cls, data: bytes) -> 'DHCPPacket':
|
| 71 |
+
"""Parse DHCP packet from raw bytes"""
|
| 72 |
+
packet = cls()
|
| 73 |
+
|
| 74 |
+
# Parse fixed fields (first 236 bytes)
|
| 75 |
+
if len(data) < 236:
|
| 76 |
+
raise ValueError("DHCP packet too short")
|
| 77 |
+
|
| 78 |
+
fields = struct.unpack('!BBBBIHH4s4s4s4s16s64s128s', data[:236])
|
| 79 |
+
packet.op = fields[0]
|
| 80 |
+
packet.htype = fields[1]
|
| 81 |
+
packet.hlen = fields[2]
|
| 82 |
+
packet.hops = fields[3]
|
| 83 |
+
packet.xid = fields[4]
|
| 84 |
+
packet.secs = fields[5]
|
| 85 |
+
packet.flags = fields[6]
|
| 86 |
+
packet.ciaddr = socket.inet_ntoa(fields[7])
|
| 87 |
+
packet.yiaddr = socket.inet_ntoa(fields[8])
|
| 88 |
+
packet.siaddr = socket.inet_ntoa(fields[9])
|
| 89 |
+
packet.giaddr = socket.inet_ntoa(fields[10])
|
| 90 |
+
packet.chaddr = fields[11]
|
| 91 |
+
packet.sname = fields[12]
|
| 92 |
+
packet.file = fields[13]
|
| 93 |
+
|
| 94 |
+
# Parse options (after magic cookie)
|
| 95 |
+
options_data = data[236:]
|
| 96 |
+
if len(options_data) >= 4:
|
| 97 |
+
magic = struct.unpack('!I', options_data[:4])[0]
|
| 98 |
+
if magic == 0x63825363: # DHCP magic cookie
|
| 99 |
+
packet.options = packet._parse_options(options_data[4:])
|
| 100 |
+
|
| 101 |
+
return packet
|
| 102 |
+
|
| 103 |
+
def _parse_options(self, data: bytes) -> Dict[int, bytes]:
|
| 104 |
+
"""Parse DHCP options"""
|
| 105 |
+
options = {}
|
| 106 |
+
i = 0
|
| 107 |
+
|
| 108 |
+
while i < len(data):
|
| 109 |
+
if data[i] == 255: # End option
|
| 110 |
+
break
|
| 111 |
+
elif data[i] == 0: # Pad option
|
| 112 |
+
i += 1
|
| 113 |
+
continue
|
| 114 |
+
|
| 115 |
+
option_type = data[i]
|
| 116 |
+
if i + 1 >= len(data):
|
| 117 |
+
break
|
| 118 |
+
|
| 119 |
+
option_length = data[i + 1]
|
| 120 |
+
if i + 2 + option_length > len(data):
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
option_data = data[i + 2:i + 2 + option_length]
|
| 124 |
+
options[option_type] = option_data
|
| 125 |
+
i += 2 + option_length
|
| 126 |
+
|
| 127 |
+
return options
|
| 128 |
+
|
| 129 |
+
def build(self) -> bytes:
|
| 130 |
+
"""Build DHCP packet as bytes"""
|
| 131 |
+
# Build fixed fields
|
| 132 |
+
packet_data = struct.pack(
|
| 133 |
+
'!BBBBIHH4s4s4s4s16s64s128s',
|
| 134 |
+
self.op, self.htype, self.hlen, self.hops,
|
| 135 |
+
self.xid, self.secs, self.flags,
|
| 136 |
+
socket.inet_aton(self.ciaddr),
|
| 137 |
+
socket.inet_aton(self.yiaddr),
|
| 138 |
+
socket.inet_aton(self.siaddr),
|
| 139 |
+
socket.inet_aton(self.giaddr),
|
| 140 |
+
self.chaddr, self.sname, self.file
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Add magic cookie
|
| 144 |
+
packet_data += struct.pack('!I', 0x63825363)
|
| 145 |
+
|
| 146 |
+
# Add options
|
| 147 |
+
for option_type, option_data in self.options.items():
|
| 148 |
+
packet_data += struct.pack('!BB', option_type, len(option_data))
|
| 149 |
+
packet_data += option_data
|
| 150 |
+
|
| 151 |
+
# Add end option
|
| 152 |
+
packet_data += b'\xff'
|
| 153 |
+
|
| 154 |
+
# Pad to minimum size
|
| 155 |
+
while len(packet_data) < 300:
|
| 156 |
+
packet_data += b'\x00'
|
| 157 |
+
|
| 158 |
+
return packet_data
|
| 159 |
+
|
| 160 |
+
def get_mac_address(self) -> str:
|
| 161 |
+
"""Get client MAC address as string"""
|
| 162 |
+
return ':'.join(f'{b:02x}' for b in self.chaddr[:6])
|
| 163 |
+
|
| 164 |
+
def get_message_type(self) -> Optional[DHCPMessageType]:
|
| 165 |
+
"""Get DHCP message type from options"""
|
| 166 |
+
if 53 in self.options and len(self.options[53]) == 1:
|
| 167 |
+
msg_type = self.options[53][0]
|
| 168 |
+
try:
|
| 169 |
+
return DHCPMessageType(msg_type)
|
| 170 |
+
except ValueError:
|
| 171 |
+
return None
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class DHCPServer:
|
| 176 |
+
"""User-space DHCP server implementation"""
|
| 177 |
+
|
| 178 |
+
def __init__(self, config: Dict):
|
| 179 |
+
self.config = config
|
| 180 |
+
self.leases: Dict[str, DHCPLease] = {} # MAC -> Lease
|
| 181 |
+
self.ip_pool = self._build_ip_pool()
|
| 182 |
+
self.running = False
|
| 183 |
+
self.server_thread = None
|
| 184 |
+
self.lock = threading.Lock()
|
| 185 |
+
|
| 186 |
+
def _build_ip_pool(self) -> set:
|
| 187 |
+
"""Build available IP address pool"""
|
| 188 |
+
network = self.config['network']
|
| 189 |
+
start_ip = self.config['range_start']
|
| 190 |
+
end_ip = self.config['range_end']
|
| 191 |
+
|
| 192 |
+
# Convert IP addresses to integers for range calculation
|
| 193 |
+
start_int = struct.unpack('!I', socket.inet_aton(start_ip))[0]
|
| 194 |
+
end_int = struct.unpack('!I', socket.inet_aton(end_ip))[0]
|
| 195 |
+
|
| 196 |
+
pool = set()
|
| 197 |
+
for ip_int in range(start_int, end_int + 1):
|
| 198 |
+
ip_str = socket.inet_ntoa(struct.pack('!I', ip_int))
|
| 199 |
+
pool.add(ip_str)
|
| 200 |
+
|
| 201 |
+
return pool
|
| 202 |
+
|
| 203 |
+
def _get_available_ip(self) -> Optional[str]:
|
| 204 |
+
"""Get next available IP address"""
|
| 205 |
+
with self.lock:
|
| 206 |
+
# Remove expired leases
|
| 207 |
+
self._cleanup_expired_leases()
|
| 208 |
+
|
| 209 |
+
# Find available IP
|
| 210 |
+
used_ips = {lease.ip_address for lease in self.leases.values()}
|
| 211 |
+
available_ips = self.ip_pool - used_ips
|
| 212 |
+
|
| 213 |
+
if available_ips:
|
| 214 |
+
return min(available_ips) # Return lowest available IP
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
def _cleanup_expired_leases(self):
|
| 218 |
+
"""Remove expired leases"""
|
| 219 |
+
expired_macs = [
|
| 220 |
+
mac for mac, lease in self.leases.items()
|
| 221 |
+
if lease.is_expired
|
| 222 |
+
]
|
| 223 |
+
for mac in expired_macs:
|
| 224 |
+
del self.leases[mac]
|
| 225 |
+
|
| 226 |
+
def _create_dhcp_offer(self, discover_packet: DHCPPacket) -> DHCPPacket:
|
| 227 |
+
"""Create DHCP OFFER response"""
|
| 228 |
+
mac_address = discover_packet.get_mac_address()
|
| 229 |
+
|
| 230 |
+
# Check for existing lease
|
| 231 |
+
if mac_address in self.leases and not self.leases[mac_address].is_expired:
|
| 232 |
+
offered_ip = self.leases[mac_address].ip_address
|
| 233 |
+
else:
|
| 234 |
+
offered_ip = self._get_available_ip()
|
| 235 |
+
if not offered_ip:
|
| 236 |
+
return None # No available IPs
|
| 237 |
+
|
| 238 |
+
# Create OFFER packet
|
| 239 |
+
offer = DHCPPacket()
|
| 240 |
+
offer.op = 2 # BOOTREPLY
|
| 241 |
+
offer.htype = discover_packet.htype
|
| 242 |
+
offer.hlen = discover_packet.hlen
|
| 243 |
+
offer.xid = discover_packet.xid
|
| 244 |
+
offer.yiaddr = offered_ip
|
| 245 |
+
offer.siaddr = self.config['gateway']
|
| 246 |
+
offer.chaddr = discover_packet.chaddr
|
| 247 |
+
|
| 248 |
+
# Add DHCP options
|
| 249 |
+
offer.options[53] = bytes([DHCPMessageType.OFFER.value]) # Message type
|
| 250 |
+
offer.options[1] = socket.inet_aton('255.255.255.0') # Subnet mask
|
| 251 |
+
offer.options[3] = socket.inet_aton(self.config['gateway']) # Router
|
| 252 |
+
offer.options[6] = b''.join(socket.inet_aton(dns) for dns in self.config['dns_servers']) # DNS
|
| 253 |
+
offer.options[51] = struct.pack('!I', self.config['lease_time']) # Lease time
|
| 254 |
+
offer.options[54] = socket.inet_aton(self.config['gateway']) # DHCP server identifier
|
| 255 |
+
|
| 256 |
+
return offer
|
| 257 |
+
|
| 258 |
+
def _create_dhcp_ack(self, request_packet: DHCPPacket) -> DHCPPacket:
|
| 259 |
+
"""Create DHCP ACK response"""
|
| 260 |
+
mac_address = request_packet.get_mac_address()
|
| 261 |
+
requested_ip = request_packet.ciaddr
|
| 262 |
+
|
| 263 |
+
# If no requested IP in ciaddr, check option 50
|
| 264 |
+
if requested_ip == '0.0.0.0' and 50 in request_packet.options:
|
| 265 |
+
requested_ip = socket.inet_ntoa(request_packet.options[50])
|
| 266 |
+
|
| 267 |
+
# Validate request
|
| 268 |
+
if not self._validate_request(mac_address, requested_ip):
|
| 269 |
+
return self._create_dhcp_nak(request_packet)
|
| 270 |
+
|
| 271 |
+
# Create or update lease
|
| 272 |
+
lease = DHCPLease(
|
| 273 |
+
mac_address=mac_address,
|
| 274 |
+
ip_address=requested_ip,
|
| 275 |
+
lease_time=self.config['lease_time'],
|
| 276 |
+
lease_start=time.time()
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
with self.lock:
|
| 280 |
+
self.leases[mac_address] = lease
|
| 281 |
+
|
| 282 |
+
# Create ACK packet
|
| 283 |
+
ack = DHCPPacket()
|
| 284 |
+
ack.op = 2 # BOOTREPLY
|
| 285 |
+
ack.htype = request_packet.htype
|
| 286 |
+
ack.hlen = request_packet.hlen
|
| 287 |
+
ack.xid = request_packet.xid
|
| 288 |
+
ack.yiaddr = requested_ip
|
| 289 |
+
ack.siaddr = self.config['gateway']
|
| 290 |
+
ack.chaddr = request_packet.chaddr
|
| 291 |
+
|
| 292 |
+
# Add DHCP options
|
| 293 |
+
ack.options[53] = bytes([DHCPMessageType.ACK.value]) # Message type
|
| 294 |
+
ack.options[1] = socket.inet_aton('255.255.255.0') # Subnet mask
|
| 295 |
+
ack.options[3] = socket.inet_aton(self.config['gateway']) # Router
|
| 296 |
+
ack.options[6] = b''.join(socket.inet_aton(dns) for dns in self.config['dns_servers']) # DNS
|
| 297 |
+
ack.options[51] = struct.pack('!I', self.config['lease_time']) # Lease time
|
| 298 |
+
ack.options[54] = socket.inet_aton(self.config['gateway']) # DHCP server identifier
|
| 299 |
+
|
| 300 |
+
return ack
|
| 301 |
+
|
| 302 |
+
def _create_dhcp_nak(self, request_packet: DHCPPacket) -> DHCPPacket:
|
| 303 |
+
"""Create DHCP NAK response"""
|
| 304 |
+
nak = DHCPPacket()
|
| 305 |
+
nak.op = 2 # BOOTREPLY
|
| 306 |
+
nak.htype = request_packet.htype
|
| 307 |
+
nak.hlen = request_packet.hlen
|
| 308 |
+
nak.xid = request_packet.xid
|
| 309 |
+
nak.chaddr = request_packet.chaddr
|
| 310 |
+
|
| 311 |
+
# Add DHCP options
|
| 312 |
+
nak.options[53] = bytes([DHCPMessageType.NAK.value]) # Message type
|
| 313 |
+
nak.options[54] = socket.inet_aton(self.config['gateway']) # DHCP server identifier
|
| 314 |
+
|
| 315 |
+
return nak
|
| 316 |
+
|
| 317 |
+
def _validate_request(self, mac_address: str, requested_ip: str) -> bool:
|
| 318 |
+
"""Validate DHCP request"""
|
| 319 |
+
# Check if IP is in our pool
|
| 320 |
+
if requested_ip not in self.ip_pool:
|
| 321 |
+
return False
|
| 322 |
+
|
| 323 |
+
# Check if IP is available or already assigned to this MAC
|
| 324 |
+
with self.lock:
|
| 325 |
+
for mac, lease in self.leases.items():
|
| 326 |
+
if lease.ip_address == requested_ip:
|
| 327 |
+
if mac != mac_address and not lease.is_expired:
|
| 328 |
+
return False # IP already assigned to different MAC
|
| 329 |
+
|
| 330 |
+
return True
|
| 331 |
+
|
| 332 |
+
def process_packet(self, packet_data: bytes, client_addr: Tuple[str, int]) -> Optional[bytes]:
|
| 333 |
+
"""Process incoming DHCP packet and return response"""
|
| 334 |
+
try:
|
| 335 |
+
packet = DHCPPacket.parse(packet_data)
|
| 336 |
+
message_type = packet.get_message_type()
|
| 337 |
+
|
| 338 |
+
if message_type == DHCPMessageType.DISCOVER:
|
| 339 |
+
response = self._create_dhcp_offer(packet)
|
| 340 |
+
elif message_type == DHCPMessageType.REQUEST:
|
| 341 |
+
response = self._create_dhcp_ack(packet)
|
| 342 |
+
elif message_type == DHCPMessageType.RELEASE:
|
| 343 |
+
# Handle lease release
|
| 344 |
+
mac_address = packet.get_mac_address()
|
| 345 |
+
with self.lock:
|
| 346 |
+
if mac_address in self.leases:
|
| 347 |
+
del self.leases[mac_address]
|
| 348 |
+
return None
|
| 349 |
+
else:
|
| 350 |
+
return None
|
| 351 |
+
|
| 352 |
+
if response:
|
| 353 |
+
return response.build()
|
| 354 |
+
|
| 355 |
+
except Exception as e:
|
| 356 |
+
print(f"Error processing DHCP packet: {e}")
|
| 357 |
+
return None
|
| 358 |
+
|
| 359 |
+
def get_leases(self) -> Dict[str, Dict]:
|
| 360 |
+
"""Get current lease table"""
|
| 361 |
+
with self.lock:
|
| 362 |
+
self._cleanup_expired_leases()
|
| 363 |
+
return {
|
| 364 |
+
mac: {
|
| 365 |
+
'ip_address': lease.ip_address,
|
| 366 |
+
'lease_time': lease.lease_time,
|
| 367 |
+
'lease_start': lease.lease_start,
|
| 368 |
+
'remaining_time': lease.remaining_time,
|
| 369 |
+
'state': lease.state
|
| 370 |
+
}
|
| 371 |
+
for mac, lease in self.leases.items()
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
def release_lease(self, mac_address: str) -> bool:
|
| 375 |
+
"""Manually release a lease"""
|
| 376 |
+
with self.lock:
|
| 377 |
+
if mac_address in self.leases:
|
| 378 |
+
del self.leases[mac_address]
|
| 379 |
+
return True
|
| 380 |
+
return False
|
| 381 |
+
|
| 382 |
+
def start(self):
|
| 383 |
+
"""Start DHCP server (placeholder for integration with packet bridge)"""
|
| 384 |
+
self.running = True
|
| 385 |
+
print(f"DHCP server started - Pool: {self.config['range_start']} - {self.config['range_end']}")
|
| 386 |
+
|
| 387 |
+
def stop(self):
|
| 388 |
+
"""Stop DHCP server"""
|
| 389 |
+
self.running = False
|
| 390 |
+
print("DHCP server stopped")
|
| 391 |
+
|
core/firewall.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Firewall Module
|
| 3 |
+
|
| 4 |
+
Implements packet filtering and access control:
|
| 5 |
+
- Rule-based packet filtering (allow/block by IP, port, protocol)
|
| 6 |
+
- Ordered rule processing
|
| 7 |
+
- Logging and statistics
|
| 8 |
+
- Dynamic rule management via API
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import time
|
| 12 |
+
import threading
|
| 13 |
+
import ipaddress
|
| 14 |
+
import re
|
| 15 |
+
from typing import Dict, List, Optional, Tuple, Any
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from enum import Enum
|
| 18 |
+
|
| 19 |
+
from .ip_parser import ParsedPacket, TCPHeader, UDPHeader
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class FirewallAction(Enum):
|
| 23 |
+
ACCEPT = "ACCEPT"
|
| 24 |
+
DROP = "DROP"
|
| 25 |
+
REJECT = "REJECT"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class FirewallDirection(Enum):
|
| 29 |
+
INBOUND = "INBOUND"
|
| 30 |
+
OUTBOUND = "OUTBOUND"
|
| 31 |
+
BOTH = "BOTH"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class FirewallRule:
|
| 36 |
+
"""Represents a firewall rule"""
|
| 37 |
+
rule_id: str
|
| 38 |
+
priority: int # Lower number = higher priority
|
| 39 |
+
action: FirewallAction
|
| 40 |
+
direction: FirewallDirection
|
| 41 |
+
|
| 42 |
+
# Match criteria
|
| 43 |
+
source_ip: Optional[str] = None # IP or CIDR
|
| 44 |
+
dest_ip: Optional[str] = None # IP or CIDR
|
| 45 |
+
source_port: Optional[str] = None # Port or range (e.g., "80", "80-90", "80,443")
|
| 46 |
+
dest_port: Optional[str] = None # Port or range
|
| 47 |
+
protocol: Optional[str] = None # TCP, UDP, ICMP, or None for any
|
| 48 |
+
|
| 49 |
+
# Metadata
|
| 50 |
+
description: str = ""
|
| 51 |
+
enabled: bool = True
|
| 52 |
+
created_time: float = 0
|
| 53 |
+
hit_count: int = 0
|
| 54 |
+
last_hit: Optional[float] = None
|
| 55 |
+
|
| 56 |
+
def __post_init__(self):
|
| 57 |
+
if self.created_time == 0:
|
| 58 |
+
self.created_time = time.time()
|
| 59 |
+
|
| 60 |
+
def record_hit(self):
|
| 61 |
+
"""Record a rule hit"""
|
| 62 |
+
self.hit_count += 1
|
| 63 |
+
self.last_hit = time.time()
|
| 64 |
+
|
| 65 |
+
def to_dict(self) -> Dict:
|
| 66 |
+
"""Convert rule to dictionary"""
|
| 67 |
+
return {
|
| 68 |
+
'rule_id': self.rule_id,
|
| 69 |
+
'priority': self.priority,
|
| 70 |
+
'action': self.action.value,
|
| 71 |
+
'direction': self.direction.value,
|
| 72 |
+
'source_ip': self.source_ip,
|
| 73 |
+
'dest_ip': self.dest_ip,
|
| 74 |
+
'source_port': self.source_port,
|
| 75 |
+
'dest_port': self.dest_port,
|
| 76 |
+
'protocol': self.protocol,
|
| 77 |
+
'description': self.description,
|
| 78 |
+
'enabled': self.enabled,
|
| 79 |
+
'created_time': self.created_time,
|
| 80 |
+
'hit_count': self.hit_count,
|
| 81 |
+
'last_hit': self.last_hit
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@dataclass
|
| 86 |
+
class FirewallLogEntry:
|
| 87 |
+
"""Represents a firewall log entry"""
|
| 88 |
+
timestamp: float
|
| 89 |
+
action: str
|
| 90 |
+
rule_id: Optional[str]
|
| 91 |
+
source_ip: str
|
| 92 |
+
dest_ip: str
|
| 93 |
+
source_port: int
|
| 94 |
+
dest_port: int
|
| 95 |
+
protocol: str
|
| 96 |
+
packet_size: int
|
| 97 |
+
reason: str = ""
|
| 98 |
+
|
| 99 |
+
def to_dict(self) -> Dict:
|
| 100 |
+
"""Convert log entry to dictionary"""
|
| 101 |
+
return {
|
| 102 |
+
'timestamp': self.timestamp,
|
| 103 |
+
'action': self.action,
|
| 104 |
+
'rule_id': self.rule_id,
|
| 105 |
+
'source_ip': self.source_ip,
|
| 106 |
+
'dest_ip': self.dest_ip,
|
| 107 |
+
'source_port': self.source_port,
|
| 108 |
+
'dest_port': self.dest_port,
|
| 109 |
+
'protocol': self.protocol,
|
| 110 |
+
'packet_size': self.packet_size,
|
| 111 |
+
'reason': self.reason
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class FirewallEngine:
|
| 116 |
+
"""Firewall engine implementation"""
|
| 117 |
+
|
| 118 |
+
def __init__(self, config: Dict):
|
| 119 |
+
self.config = config
|
| 120 |
+
self.rules: Dict[str, FirewallRule] = {}
|
| 121 |
+
self.logs: List[FirewallLogEntry] = []
|
| 122 |
+
self.lock = threading.Lock()
|
| 123 |
+
|
| 124 |
+
# Configuration
|
| 125 |
+
self.default_policy = FirewallAction(config.get('default_policy', 'ACCEPT'))
|
| 126 |
+
self.log_blocked = config.get('log_blocked', True)
|
| 127 |
+
self.log_accepted = config.get('log_accepted', False)
|
| 128 |
+
self.max_log_entries = config.get('max_log_entries', 10000)
|
| 129 |
+
|
| 130 |
+
# Statistics
|
| 131 |
+
self.stats = {
|
| 132 |
+
'packets_processed': 0,
|
| 133 |
+
'packets_accepted': 0,
|
| 134 |
+
'packets_dropped': 0,
|
| 135 |
+
'packets_rejected': 0,
|
| 136 |
+
'rules_hit': 0,
|
| 137 |
+
'default_policy_hits': 0
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
# Load initial rules
|
| 141 |
+
initial_rules = config.get('rules', [])
|
| 142 |
+
for rule_config in initial_rules:
|
| 143 |
+
self._add_rule_from_config(rule_config)
|
| 144 |
+
|
| 145 |
+
def _add_rule_from_config(self, rule_config: Dict):
|
| 146 |
+
"""Add rule from configuration"""
|
| 147 |
+
rule = FirewallRule(
|
| 148 |
+
rule_id=rule_config['rule_id'],
|
| 149 |
+
priority=rule_config.get('priority', 100),
|
| 150 |
+
action=FirewallAction(rule_config['action']),
|
| 151 |
+
direction=FirewallDirection(rule_config.get('direction', 'BOTH')),
|
| 152 |
+
source_ip=rule_config.get('source_ip'),
|
| 153 |
+
dest_ip=rule_config.get('dest_ip'),
|
| 154 |
+
source_port=rule_config.get('source_port'),
|
| 155 |
+
dest_port=rule_config.get('dest_port'),
|
| 156 |
+
protocol=rule_config.get('protocol'),
|
| 157 |
+
description=rule_config.get('description', ''),
|
| 158 |
+
enabled=rule_config.get('enabled', True)
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
with self.lock:
|
| 162 |
+
self.rules[rule.rule_id] = rule
|
| 163 |
+
|
| 164 |
+
def _match_ip(self, ip: str, pattern: str) -> bool:
|
| 165 |
+
"""Match IP address against pattern (IP or CIDR)"""
|
| 166 |
+
try:
|
| 167 |
+
if '/' in pattern:
|
| 168 |
+
# CIDR notation
|
| 169 |
+
network = ipaddress.ip_network(pattern, strict=False)
|
| 170 |
+
return ipaddress.ip_address(ip) in network
|
| 171 |
+
else:
|
| 172 |
+
# Exact IP match
|
| 173 |
+
return ip == pattern
|
| 174 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 175 |
+
return False
|
| 176 |
+
|
| 177 |
+
def _match_port(self, port: int, pattern: str) -> bool:
|
| 178 |
+
"""Match port against pattern (port, range, or list)"""
|
| 179 |
+
try:
|
| 180 |
+
if ',' in pattern:
|
| 181 |
+
# List of ports: "80,443,8080"
|
| 182 |
+
ports = [int(p.strip()) for p in pattern.split(',')]
|
| 183 |
+
return port in ports
|
| 184 |
+
elif '-' in pattern:
|
| 185 |
+
# Port range: "80-90"
|
| 186 |
+
start, end = map(int, pattern.split('-', 1))
|
| 187 |
+
return start <= port <= end
|
| 188 |
+
else:
|
| 189 |
+
# Single port: "80"
|
| 190 |
+
return port == int(pattern)
|
| 191 |
+
except (ValueError, TypeError):
|
| 192 |
+
return False
|
| 193 |
+
|
| 194 |
+
def _match_protocol(self, protocol: str, pattern: str) -> bool:
|
| 195 |
+
"""Match protocol against pattern"""
|
| 196 |
+
if pattern is None:
|
| 197 |
+
return True # Match any protocol
|
| 198 |
+
return protocol.upper() == pattern.upper()
|
| 199 |
+
|
| 200 |
+
def _evaluate_rule(self, rule: FirewallRule, packet: ParsedPacket, direction: FirewallDirection) -> bool:
|
| 201 |
+
"""Evaluate if a rule matches a packet"""
|
| 202 |
+
if not rule.enabled:
|
| 203 |
+
return False
|
| 204 |
+
|
| 205 |
+
# Check direction
|
| 206 |
+
if rule.direction != FirewallDirection.BOTH and rule.direction != direction:
|
| 207 |
+
return False
|
| 208 |
+
|
| 209 |
+
# Check source IP
|
| 210 |
+
if rule.source_ip and not self._match_ip(packet.ip_header.source_ip, rule.source_ip):
|
| 211 |
+
return False
|
| 212 |
+
|
| 213 |
+
# Check destination IP
|
| 214 |
+
if rule.dest_ip and not self._match_ip(packet.ip_header.dest_ip, rule.dest_ip):
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
# Check protocol
|
| 218 |
+
if packet.transport_header:
|
| 219 |
+
if isinstance(packet.transport_header, TCPHeader):
|
| 220 |
+
protocol = 'TCP'
|
| 221 |
+
source_port = packet.transport_header.source_port
|
| 222 |
+
dest_port = packet.transport_header.dest_port
|
| 223 |
+
elif isinstance(packet.transport_header, UDPHeader):
|
| 224 |
+
protocol = 'UDP'
|
| 225 |
+
source_port = packet.transport_header.source_port
|
| 226 |
+
dest_port = packet.transport_header.dest_port
|
| 227 |
+
else:
|
| 228 |
+
protocol = 'OTHER'
|
| 229 |
+
source_port = 0
|
| 230 |
+
dest_port = 0
|
| 231 |
+
else:
|
| 232 |
+
protocol = 'OTHER'
|
| 233 |
+
source_port = 0
|
| 234 |
+
dest_port = 0
|
| 235 |
+
|
| 236 |
+
if not self._match_protocol(protocol, rule.protocol):
|
| 237 |
+
return False
|
| 238 |
+
|
| 239 |
+
# Check source port
|
| 240 |
+
if rule.source_port and not self._match_port(source_port, rule.source_port):
|
| 241 |
+
return False
|
| 242 |
+
|
| 243 |
+
# Check destination port
|
| 244 |
+
if rule.dest_port and not self._match_port(dest_port, rule.dest_port):
|
| 245 |
+
return False
|
| 246 |
+
|
| 247 |
+
return True
|
| 248 |
+
|
| 249 |
+
def _log_packet(self, action: str, packet: ParsedPacket, rule_id: Optional[str] = None, reason: str = ""):
|
| 250 |
+
"""Log packet processing"""
|
| 251 |
+
if not (self.log_blocked or self.log_accepted):
|
| 252 |
+
return
|
| 253 |
+
|
| 254 |
+
# Only log if configured
|
| 255 |
+
if action == 'ACCEPT' and not self.log_accepted:
|
| 256 |
+
return
|
| 257 |
+
if action in ['DROP', 'REJECT'] and not self.log_blocked:
|
| 258 |
+
return
|
| 259 |
+
|
| 260 |
+
# Extract packet information
|
| 261 |
+
if packet.transport_header:
|
| 262 |
+
if isinstance(packet.transport_header, (TCPHeader, UDPHeader)):
|
| 263 |
+
source_port = packet.transport_header.source_port
|
| 264 |
+
dest_port = packet.transport_header.dest_port
|
| 265 |
+
protocol = 'TCP' if isinstance(packet.transport_header, TCPHeader) else 'UDP'
|
| 266 |
+
else:
|
| 267 |
+
source_port = 0
|
| 268 |
+
dest_port = 0
|
| 269 |
+
protocol = 'OTHER'
|
| 270 |
+
else:
|
| 271 |
+
source_port = 0
|
| 272 |
+
dest_port = 0
|
| 273 |
+
protocol = 'OTHER'
|
| 274 |
+
|
| 275 |
+
log_entry = FirewallLogEntry(
|
| 276 |
+
timestamp=time.time(),
|
| 277 |
+
action=action,
|
| 278 |
+
rule_id=rule_id,
|
| 279 |
+
source_ip=packet.ip_header.source_ip,
|
| 280 |
+
dest_ip=packet.ip_header.dest_ip,
|
| 281 |
+
source_port=source_port,
|
| 282 |
+
dest_port=dest_port,
|
| 283 |
+
protocol=protocol,
|
| 284 |
+
packet_size=len(packet.raw_packet),
|
| 285 |
+
reason=reason
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
with self.lock:
|
| 289 |
+
self.logs.append(log_entry)
|
| 290 |
+
|
| 291 |
+
# Trim logs if too many
|
| 292 |
+
if len(self.logs) > self.max_log_entries:
|
| 293 |
+
self.logs = self.logs[-self.max_log_entries:]
|
| 294 |
+
|
| 295 |
+
def process_packet(self, packet: ParsedPacket, direction: FirewallDirection) -> FirewallAction:
|
| 296 |
+
"""Process packet through firewall rules"""
|
| 297 |
+
self.stats['packets_processed'] += 1
|
| 298 |
+
|
| 299 |
+
# Get sorted rules by priority
|
| 300 |
+
with self.lock:
|
| 301 |
+
sorted_rules = sorted(self.rules.values(), key=lambda r: r.priority)
|
| 302 |
+
|
| 303 |
+
# Evaluate rules in order
|
| 304 |
+
for rule in sorted_rules:
|
| 305 |
+
if self._evaluate_rule(rule, packet, direction):
|
| 306 |
+
rule.record_hit()
|
| 307 |
+
self.stats['rules_hit'] += 1
|
| 308 |
+
|
| 309 |
+
# Log the action
|
| 310 |
+
self._log_packet(rule.action.value, packet, rule.rule_id, f"Matched rule: {rule.description}")
|
| 311 |
+
|
| 312 |
+
# Update statistics
|
| 313 |
+
if rule.action == FirewallAction.ACCEPT:
|
| 314 |
+
self.stats['packets_accepted'] += 1
|
| 315 |
+
elif rule.action == FirewallAction.DROP:
|
| 316 |
+
self.stats['packets_dropped'] += 1
|
| 317 |
+
elif rule.action == FirewallAction.REJECT:
|
| 318 |
+
self.stats['packets_rejected'] += 1
|
| 319 |
+
|
| 320 |
+
return rule.action
|
| 321 |
+
|
| 322 |
+
# No rule matched, apply default policy
|
| 323 |
+
self.stats['default_policy_hits'] += 1
|
| 324 |
+
self._log_packet(self.default_policy.value, packet, None, "Default policy")
|
| 325 |
+
|
| 326 |
+
if self.default_policy == FirewallAction.ACCEPT:
|
| 327 |
+
self.stats['packets_accepted'] += 1
|
| 328 |
+
elif self.default_policy == FirewallAction.DROP:
|
| 329 |
+
self.stats['packets_dropped'] += 1
|
| 330 |
+
elif self.default_policy == FirewallAction.REJECT:
|
| 331 |
+
self.stats['packets_rejected'] += 1
|
| 332 |
+
|
| 333 |
+
return self.default_policy
|
| 334 |
+
|
| 335 |
+
def add_rule(self, rule: FirewallRule) -> bool:
|
| 336 |
+
"""Add firewall rule"""
|
| 337 |
+
with self.lock:
|
| 338 |
+
if rule.rule_id in self.rules:
|
| 339 |
+
return False
|
| 340 |
+
self.rules[rule.rule_id] = rule
|
| 341 |
+
return True
|
| 342 |
+
|
| 343 |
+
def remove_rule(self, rule_id: str) -> bool:
|
| 344 |
+
"""Remove firewall rule"""
|
| 345 |
+
with self.lock:
|
| 346 |
+
if rule_id in self.rules:
|
| 347 |
+
del self.rules[rule_id]
|
| 348 |
+
return True
|
| 349 |
+
return False
|
| 350 |
+
|
| 351 |
+
def update_rule(self, rule_id: str, **kwargs) -> bool:
|
| 352 |
+
"""Update firewall rule"""
|
| 353 |
+
with self.lock:
|
| 354 |
+
if rule_id not in self.rules:
|
| 355 |
+
return False
|
| 356 |
+
|
| 357 |
+
rule = self.rules[rule_id]
|
| 358 |
+
for key, value in kwargs.items():
|
| 359 |
+
if hasattr(rule, key):
|
| 360 |
+
if key in ['action', 'direction']:
|
| 361 |
+
# Handle enum values
|
| 362 |
+
if key == 'action':
|
| 363 |
+
value = FirewallAction(value)
|
| 364 |
+
elif key == 'direction':
|
| 365 |
+
value = FirewallDirection(value)
|
| 366 |
+
setattr(rule, key, value)
|
| 367 |
+
|
| 368 |
+
return True
|
| 369 |
+
|
| 370 |
+
def enable_rule(self, rule_id: str) -> bool:
|
| 371 |
+
"""Enable firewall rule"""
|
| 372 |
+
return self.update_rule(rule_id, enabled=True)
|
| 373 |
+
|
| 374 |
+
def disable_rule(self, rule_id: str) -> bool:
|
| 375 |
+
"""Disable firewall rule"""
|
| 376 |
+
return self.update_rule(rule_id, enabled=False)
|
| 377 |
+
|
| 378 |
+
def get_rules(self) -> List[Dict]:
|
| 379 |
+
"""Get all firewall rules"""
|
| 380 |
+
with self.lock:
|
| 381 |
+
return [rule.to_dict() for rule in sorted(self.rules.values(), key=lambda r: r.priority)]
|
| 382 |
+
|
| 383 |
+
def get_rule(self, rule_id: str) -> Optional[Dict]:
|
| 384 |
+
"""Get specific firewall rule"""
|
| 385 |
+
with self.lock:
|
| 386 |
+
rule = self.rules.get(rule_id)
|
| 387 |
+
return rule.to_dict() if rule else None
|
| 388 |
+
|
| 389 |
+
def get_logs(self, limit: int = 100, filter_action: Optional[str] = None) -> List[Dict]:
|
| 390 |
+
"""Get firewall logs"""
|
| 391 |
+
with self.lock:
|
| 392 |
+
logs = self.logs.copy()
|
| 393 |
+
|
| 394 |
+
# Filter by action if specified
|
| 395 |
+
if filter_action:
|
| 396 |
+
logs = [log for log in logs if log.action == filter_action.upper()]
|
| 397 |
+
|
| 398 |
+
# Return most recent logs
|
| 399 |
+
return [log.to_dict() for log in logs[-limit:]]
|
| 400 |
+
|
| 401 |
+
def clear_logs(self):
|
| 402 |
+
"""Clear firewall logs"""
|
| 403 |
+
with self.lock:
|
| 404 |
+
self.logs.clear()
|
| 405 |
+
|
| 406 |
+
def get_stats(self) -> Dict:
|
| 407 |
+
"""Get firewall statistics"""
|
| 408 |
+
with self.lock:
|
| 409 |
+
stats = self.stats.copy()
|
| 410 |
+
stats['total_rules'] = len(self.rules)
|
| 411 |
+
stats['enabled_rules'] = sum(1 for rule in self.rules.values() if rule.enabled)
|
| 412 |
+
stats['log_entries'] = len(self.logs)
|
| 413 |
+
stats['default_policy'] = self.default_policy.value
|
| 414 |
+
|
| 415 |
+
return stats
|
| 416 |
+
|
| 417 |
+
def reset_stats(self):
|
| 418 |
+
"""Reset firewall statistics"""
|
| 419 |
+
self.stats = {
|
| 420 |
+
'packets_processed': 0,
|
| 421 |
+
'packets_accepted': 0,
|
| 422 |
+
'packets_dropped': 0,
|
| 423 |
+
'packets_rejected': 0,
|
| 424 |
+
'rules_hit': 0,
|
| 425 |
+
'default_policy_hits': 0
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
# Reset rule hit counts
|
| 429 |
+
with self.lock:
|
| 430 |
+
for rule in self.rules.values():
|
| 431 |
+
rule.hit_count = 0
|
| 432 |
+
rule.last_hit = None
|
| 433 |
+
|
| 434 |
+
def set_default_policy(self, policy: str):
|
| 435 |
+
"""Set default firewall policy"""
|
| 436 |
+
self.default_policy = FirewallAction(policy.upper())
|
| 437 |
+
|
| 438 |
+
def export_rules(self) -> List[Dict]:
|
| 439 |
+
"""Export rules for backup/configuration"""
|
| 440 |
+
return self.get_rules()
|
| 441 |
+
|
| 442 |
+
def import_rules(self, rules_config: List[Dict], replace: bool = False):
|
| 443 |
+
"""Import rules from configuration"""
|
| 444 |
+
if replace:
|
| 445 |
+
with self.lock:
|
| 446 |
+
self.rules.clear()
|
| 447 |
+
|
| 448 |
+
for rule_config in rules_config:
|
| 449 |
+
self._add_rule_from_config(rule_config)
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
class FirewallRuleBuilder:
|
| 453 |
+
"""Helper class to build firewall rules"""
|
| 454 |
+
|
| 455 |
+
def __init__(self, rule_id: str):
|
| 456 |
+
self.rule_id = rule_id
|
| 457 |
+
self.priority = 100
|
| 458 |
+
self.action = FirewallAction.ACCEPT
|
| 459 |
+
self.direction = FirewallDirection.BOTH
|
| 460 |
+
self.source_ip = None
|
| 461 |
+
self.dest_ip = None
|
| 462 |
+
self.source_port = None
|
| 463 |
+
self.dest_port = None
|
| 464 |
+
self.protocol = None
|
| 465 |
+
self.description = ""
|
| 466 |
+
self.enabled = True
|
| 467 |
+
|
| 468 |
+
def set_priority(self, priority: int) -> 'FirewallRuleBuilder':
|
| 469 |
+
self.priority = priority
|
| 470 |
+
return self
|
| 471 |
+
|
| 472 |
+
def set_action(self, action: str) -> 'FirewallRuleBuilder':
|
| 473 |
+
self.action = FirewallAction(action.upper())
|
| 474 |
+
return self
|
| 475 |
+
|
| 476 |
+
def set_direction(self, direction: str) -> 'FirewallRuleBuilder':
|
| 477 |
+
self.direction = FirewallDirection(direction.upper())
|
| 478 |
+
return self
|
| 479 |
+
|
| 480 |
+
def set_source_ip(self, ip: str) -> 'FirewallRuleBuilder':
|
| 481 |
+
self.source_ip = ip
|
| 482 |
+
return self
|
| 483 |
+
|
| 484 |
+
def set_dest_ip(self, ip: str) -> 'FirewallRuleBuilder':
|
| 485 |
+
self.dest_ip = ip
|
| 486 |
+
return self
|
| 487 |
+
|
| 488 |
+
def set_source_port(self, port: str) -> 'FirewallRuleBuilder':
|
| 489 |
+
self.source_port = port
|
| 490 |
+
return self
|
| 491 |
+
|
| 492 |
+
def set_dest_port(self, port: str) -> 'FirewallRuleBuilder':
|
| 493 |
+
self.dest_port = port
|
| 494 |
+
return self
|
| 495 |
+
|
| 496 |
+
def set_protocol(self, protocol: str) -> 'FirewallRuleBuilder':
|
| 497 |
+
self.protocol = protocol.upper()
|
| 498 |
+
return self
|
| 499 |
+
|
| 500 |
+
def set_description(self, description: str) -> 'FirewallRuleBuilder':
|
| 501 |
+
self.description = description
|
| 502 |
+
return self
|
| 503 |
+
|
| 504 |
+
def set_enabled(self, enabled: bool) -> 'FirewallRuleBuilder':
|
| 505 |
+
self.enabled = enabled
|
| 506 |
+
return self
|
| 507 |
+
|
| 508 |
+
def build(self) -> FirewallRule:
|
| 509 |
+
"""Build the firewall rule"""
|
| 510 |
+
return FirewallRule(
|
| 511 |
+
rule_id=self.rule_id,
|
| 512 |
+
priority=self.priority,
|
| 513 |
+
action=self.action,
|
| 514 |
+
direction=self.direction,
|
| 515 |
+
source_ip=self.source_ip,
|
| 516 |
+
dest_ip=self.dest_ip,
|
| 517 |
+
source_port=self.source_port,
|
| 518 |
+
dest_port=self.dest_port,
|
| 519 |
+
protocol=self.protocol,
|
| 520 |
+
description=self.description,
|
| 521 |
+
enabled=self.enabled
|
| 522 |
+
)
|
| 523 |
+
|
core/ip_parser.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
IP Parser/Assembler Module
|
| 3 |
+
|
| 4 |
+
Handles IPv4 packet parsing and construction:
|
| 5 |
+
- Parse IPv4, UDP, and TCP headers
|
| 6 |
+
- Calculate and verify checksums
|
| 7 |
+
- Handle packet fragmentation and reassembly
|
| 8 |
+
- Support various IP options
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import struct
|
| 12 |
+
import socket
|
| 13 |
+
from typing import Dict, List, Optional, Tuple
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from enum import Enum
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class IPProtocol(Enum):
|
| 19 |
+
ICMP = 1
|
| 20 |
+
TCP = 6
|
| 21 |
+
UDP = 17
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class IPv4Header:
|
| 26 |
+
"""IPv4 header structure"""
|
| 27 |
+
version: int = 4
|
| 28 |
+
ihl: int = 5 # Internet Header Length (in 32-bit words)
|
| 29 |
+
tos: int = 0 # Type of Service
|
| 30 |
+
total_length: int = 0
|
| 31 |
+
identification: int = 0
|
| 32 |
+
flags: int = 0 # 3 bits: Reserved, Don't Fragment, More Fragments
|
| 33 |
+
fragment_offset: int = 0 # 13 bits
|
| 34 |
+
ttl: int = 64 # Time to Live
|
| 35 |
+
protocol: int = 0
|
| 36 |
+
header_checksum: int = 0
|
| 37 |
+
source_ip: str = '0.0.0.0'
|
| 38 |
+
dest_ip: str = '0.0.0.0'
|
| 39 |
+
options: bytes = b''
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def header_length(self) -> int:
|
| 43 |
+
"""Get header length in bytes"""
|
| 44 |
+
return self.ihl * 4
|
| 45 |
+
|
| 46 |
+
@property
|
| 47 |
+
def dont_fragment(self) -> bool:
|
| 48 |
+
"""Check if Don't Fragment flag is set"""
|
| 49 |
+
return bool(self.flags & 0x2)
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def more_fragments(self) -> bool:
|
| 53 |
+
"""Check if More Fragments flag is set"""
|
| 54 |
+
return bool(self.flags & 0x1)
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def is_fragment(self) -> bool:
|
| 58 |
+
"""Check if this is a fragment"""
|
| 59 |
+
return self.more_fragments or self.fragment_offset > 0
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@dataclass
|
| 63 |
+
class TCPHeader:
|
| 64 |
+
"""TCP header structure"""
|
| 65 |
+
source_port: int = 0
|
| 66 |
+
dest_port: int = 0
|
| 67 |
+
seq_num: int = 0
|
| 68 |
+
ack_num: int = 0
|
| 69 |
+
data_offset: int = 5 # Header length in 32-bit words
|
| 70 |
+
reserved: int = 0
|
| 71 |
+
flags: int = 0 # 9 bits: NS, CWR, ECE, URG, ACK, PSH, RST, SYN, FIN
|
| 72 |
+
window_size: int = 65535
|
| 73 |
+
checksum: int = 0
|
| 74 |
+
urgent_pointer: int = 0
|
| 75 |
+
options: bytes = b''
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def header_length(self) -> int:
|
| 79 |
+
"""Get header length in bytes"""
|
| 80 |
+
return self.data_offset * 4
|
| 81 |
+
|
| 82 |
+
# TCP Flag properties
|
| 83 |
+
@property
|
| 84 |
+
def fin(self) -> bool:
|
| 85 |
+
return bool(self.flags & 0x01)
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def syn(self) -> bool:
|
| 89 |
+
return bool(self.flags & 0x02)
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def rst(self) -> bool:
|
| 93 |
+
return bool(self.flags & 0x04)
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def psh(self) -> bool:
|
| 97 |
+
return bool(self.flags & 0x08)
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def ack(self) -> bool:
|
| 101 |
+
return bool(self.flags & 0x10)
|
| 102 |
+
|
| 103 |
+
@property
|
| 104 |
+
def urg(self) -> bool:
|
| 105 |
+
return bool(self.flags & 0x20)
|
| 106 |
+
|
| 107 |
+
def set_flag(self, flag_name: str, value: bool = True):
|
| 108 |
+
"""Set TCP flag"""
|
| 109 |
+
flag_bits = {
|
| 110 |
+
'fin': 0x01, 'syn': 0x02, 'rst': 0x04, 'psh': 0x08,
|
| 111 |
+
'ack': 0x10, 'urg': 0x20, 'ece': 0x40, 'cwr': 0x80, 'ns': 0x100
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
if flag_name.lower() in flag_bits:
|
| 115 |
+
bit = flag_bits[flag_name.lower()]
|
| 116 |
+
if value:
|
| 117 |
+
self.flags |= bit
|
| 118 |
+
else:
|
| 119 |
+
self.flags &= ~bit
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@dataclass
|
| 123 |
+
class UDPHeader:
|
| 124 |
+
"""UDP header structure"""
|
| 125 |
+
source_port: int = 0
|
| 126 |
+
dest_port: int = 0
|
| 127 |
+
length: int = 8 # Header + data length
|
| 128 |
+
checksum: int = 0
|
| 129 |
+
|
| 130 |
+
@property
|
| 131 |
+
def header_length(self) -> int:
|
| 132 |
+
"""Get header length in bytes (always 8 for UDP)"""
|
| 133 |
+
return 8
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@dataclass
|
| 137 |
+
class ParsedPacket:
|
| 138 |
+
"""Parsed packet structure"""
|
| 139 |
+
ip_header: IPv4Header
|
| 140 |
+
transport_header: Optional[object] = None # TCPHeader or UDPHeader
|
| 141 |
+
payload: bytes = b''
|
| 142 |
+
raw_packet: bytes = b''
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class IPParser:
|
| 146 |
+
"""IPv4 packet parser and assembler"""
|
| 147 |
+
|
| 148 |
+
@staticmethod
|
| 149 |
+
def calculate_checksum(data: bytes) -> int:
|
| 150 |
+
"""Calculate Internet checksum"""
|
| 151 |
+
# Pad data to even length
|
| 152 |
+
if len(data) % 2:
|
| 153 |
+
data += b'\x00'
|
| 154 |
+
|
| 155 |
+
checksum = 0
|
| 156 |
+
for i in range(0, len(data), 2):
|
| 157 |
+
word = (data[i] << 8) + data[i + 1]
|
| 158 |
+
checksum += word
|
| 159 |
+
|
| 160 |
+
# Add carry bits
|
| 161 |
+
while checksum >> 16:
|
| 162 |
+
checksum = (checksum & 0xFFFF) + (checksum >> 16)
|
| 163 |
+
|
| 164 |
+
# One's complement
|
| 165 |
+
return (~checksum) & 0xFFFF
|
| 166 |
+
|
| 167 |
+
@staticmethod
|
| 168 |
+
def verify_checksum(data: bytes, checksum: int) -> bool:
|
| 169 |
+
"""Verify Internet checksum"""
|
| 170 |
+
calculated = IPParser.calculate_checksum(data)
|
| 171 |
+
return calculated == checksum or (calculated + checksum) == 0xFFFF
|
| 172 |
+
|
| 173 |
+
@classmethod
|
| 174 |
+
def parse_ipv4_header(cls, data: bytes) -> Tuple[IPv4Header, int]:
|
| 175 |
+
"""Parse IPv4 header from raw bytes"""
|
| 176 |
+
if len(data) < 20:
|
| 177 |
+
raise ValueError("IPv4 header too short")
|
| 178 |
+
|
| 179 |
+
# Parse fixed part of header
|
| 180 |
+
header_data = struct.unpack('!BBHHHBBH4s4s', data[:20])
|
| 181 |
+
|
| 182 |
+
header = IPv4Header()
|
| 183 |
+
version_ihl = header_data[0]
|
| 184 |
+
header.version = (version_ihl >> 4) & 0xF
|
| 185 |
+
header.ihl = version_ihl & 0xF
|
| 186 |
+
header.tos = header_data[1]
|
| 187 |
+
header.total_length = header_data[2]
|
| 188 |
+
header.identification = header_data[3]
|
| 189 |
+
flags_fragment = header_data[4]
|
| 190 |
+
header.flags = (flags_fragment >> 13) & 0x7
|
| 191 |
+
header.fragment_offset = flags_fragment & 0x1FFF
|
| 192 |
+
header.ttl = header_data[5]
|
| 193 |
+
header.protocol = header_data[6]
|
| 194 |
+
header.header_checksum = header_data[7]
|
| 195 |
+
header.source_ip = socket.inet_ntoa(header_data[8])
|
| 196 |
+
header.dest_ip = socket.inet_ntoa(header_data[9])
|
| 197 |
+
|
| 198 |
+
# Validate version
|
| 199 |
+
if header.version != 4:
|
| 200 |
+
raise ValueError(f"Unsupported IP version: {header.version}")
|
| 201 |
+
|
| 202 |
+
# Parse options if present
|
| 203 |
+
options_length = header.header_length - 20
|
| 204 |
+
if options_length > 0:
|
| 205 |
+
if len(data) < 20 + options_length:
|
| 206 |
+
raise ValueError("IPv4 options truncated")
|
| 207 |
+
header.options = data[20:20 + options_length]
|
| 208 |
+
|
| 209 |
+
return header, header.header_length
|
| 210 |
+
|
| 211 |
+
@classmethod
|
| 212 |
+
def parse_tcp_header(cls, data: bytes) -> Tuple[TCPHeader, int]:
|
| 213 |
+
"""Parse TCP header from raw bytes"""
|
| 214 |
+
if len(data) < 20:
|
| 215 |
+
raise ValueError("TCP header too short")
|
| 216 |
+
|
| 217 |
+
# Parse fixed part of header
|
| 218 |
+
header_data = struct.unpack('!HHIIBBHHH', data[:20])
|
| 219 |
+
|
| 220 |
+
header = TCPHeader()
|
| 221 |
+
header.source_port = header_data[0]
|
| 222 |
+
header.dest_port = header_data[1]
|
| 223 |
+
header.seq_num = header_data[2]
|
| 224 |
+
header.ack_num = header_data[3]
|
| 225 |
+
offset_reserved = header_data[4]
|
| 226 |
+
header.data_offset = (offset_reserved >> 4) & 0xF
|
| 227 |
+
header.reserved = (offset_reserved >> 1) & 0x7
|
| 228 |
+
header.flags = ((offset_reserved & 0x1) << 8) | header_data[5]
|
| 229 |
+
header.window_size = header_data[6]
|
| 230 |
+
header.checksum = header_data[7]
|
| 231 |
+
header.urgent_pointer = header_data[8]
|
| 232 |
+
|
| 233 |
+
# Parse options if present
|
| 234 |
+
options_length = header.header_length - 20
|
| 235 |
+
if options_length > 0:
|
| 236 |
+
if len(data) < 20 + options_length:
|
| 237 |
+
raise ValueError("TCP options truncated")
|
| 238 |
+
header.options = data[20:20 + options_length]
|
| 239 |
+
|
| 240 |
+
return header, header.header_length
|
| 241 |
+
|
| 242 |
+
@classmethod
|
| 243 |
+
def parse_udp_header(cls, data: bytes) -> Tuple[UDPHeader, int]:
|
| 244 |
+
"""Parse UDP header from raw bytes"""
|
| 245 |
+
if len(data) < 8:
|
| 246 |
+
raise ValueError("UDP header too short")
|
| 247 |
+
|
| 248 |
+
header_data = struct.unpack('!HHHH', data[:8])
|
| 249 |
+
|
| 250 |
+
header = UDPHeader()
|
| 251 |
+
header.source_port = header_data[0]
|
| 252 |
+
header.dest_port = header_data[1]
|
| 253 |
+
header.length = header_data[2]
|
| 254 |
+
header.checksum = header_data[3]
|
| 255 |
+
|
| 256 |
+
return header, 8
|
| 257 |
+
|
| 258 |
+
@classmethod
|
| 259 |
+
def parse_packet(cls, data: bytes) -> ParsedPacket:
|
| 260 |
+
"""Parse complete packet"""
|
| 261 |
+
packet = ParsedPacket(raw_packet=data)
|
| 262 |
+
|
| 263 |
+
# Parse IP header
|
| 264 |
+
packet.ip_header, ip_header_len = cls.parse_ipv4_header(data)
|
| 265 |
+
|
| 266 |
+
# Extract payload after IP header
|
| 267 |
+
ip_payload = data[ip_header_len:packet.ip_header.total_length]
|
| 268 |
+
|
| 269 |
+
# Parse transport layer header
|
| 270 |
+
if packet.ip_header.protocol == IPProtocol.TCP.value:
|
| 271 |
+
packet.transport_header, transport_header_len = cls.parse_tcp_header(ip_payload)
|
| 272 |
+
packet.payload = ip_payload[transport_header_len:]
|
| 273 |
+
elif packet.ip_header.protocol == IPProtocol.UDP.value:
|
| 274 |
+
packet.transport_header, transport_header_len = cls.parse_udp_header(ip_payload)
|
| 275 |
+
packet.payload = ip_payload[transport_header_len:]
|
| 276 |
+
else:
|
| 277 |
+
# Unsupported protocol, treat as raw payload
|
| 278 |
+
packet.payload = ip_payload
|
| 279 |
+
|
| 280 |
+
return packet
|
| 281 |
+
|
| 282 |
+
@classmethod
|
| 283 |
+
def build_ipv4_header(cls, header: IPv4Header) -> bytes:
|
| 284 |
+
"""Build IPv4 header as bytes"""
|
| 285 |
+
# Calculate header length including options
|
| 286 |
+
header.ihl = (20 + len(header.options) + 3) // 4 # Round up to 32-bit boundary
|
| 287 |
+
|
| 288 |
+
# Build header without checksum
|
| 289 |
+
version_ihl = (header.version << 4) | header.ihl
|
| 290 |
+
flags_fragment = (header.flags << 13) | header.fragment_offset
|
| 291 |
+
|
| 292 |
+
header_data = struct.pack(
|
| 293 |
+
'!BBHHHBBH4s4s',
|
| 294 |
+
version_ihl, header.tos, header.total_length,
|
| 295 |
+
header.identification, flags_fragment,
|
| 296 |
+
header.ttl, header.protocol, 0, # Checksum = 0 for calculation
|
| 297 |
+
socket.inet_aton(header.source_ip),
|
| 298 |
+
socket.inet_aton(header.dest_ip)
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
# Add options and padding
|
| 302 |
+
if header.options:
|
| 303 |
+
header_data += header.options
|
| 304 |
+
# Pad to 32-bit boundary
|
| 305 |
+
padding_needed = (header.ihl * 4) - len(header_data)
|
| 306 |
+
if padding_needed > 0:
|
| 307 |
+
header_data += b'\x00' * padding_needed
|
| 308 |
+
|
| 309 |
+
# Calculate and insert checksum
|
| 310 |
+
checksum = cls.calculate_checksum(header_data)
|
| 311 |
+
header_data = header_data[:10] + struct.pack('!H', checksum) + header_data[12:]
|
| 312 |
+
|
| 313 |
+
return header_data
|
| 314 |
+
|
| 315 |
+
@classmethod
|
| 316 |
+
def build_tcp_header(cls, header: TCPHeader, source_ip: str, dest_ip: str, payload: bytes) -> bytes:
|
| 317 |
+
"""Build TCP header as bytes with checksum"""
|
| 318 |
+
# Calculate header length including options
|
| 319 |
+
header.data_offset = (20 + len(header.options) + 3) // 4 # Round up to 32-bit boundary
|
| 320 |
+
|
| 321 |
+
# Build header without checksum
|
| 322 |
+
offset_reserved_flags = (header.data_offset << 12) | (header.reserved << 9) | header.flags
|
| 323 |
+
|
| 324 |
+
header_data = struct.pack(
|
| 325 |
+
'!HHIIHHH',
|
| 326 |
+
header.source_port, header.dest_port,
|
| 327 |
+
header.seq_num, header.ack_num,
|
| 328 |
+
offset_reserved_flags, header.window_size,
|
| 329 |
+
0, header.urgent_pointer # Checksum = 0 for calculation
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
# Add options and padding
|
| 333 |
+
if header.options:
|
| 334 |
+
header_data += header.options
|
| 335 |
+
# Pad to 32-bit boundary
|
| 336 |
+
padding_needed = (header.data_offset * 4) - len(header_data)
|
| 337 |
+
if padding_needed > 0:
|
| 338 |
+
header_data += b'\x00' * padding_needed
|
| 339 |
+
|
| 340 |
+
# Calculate TCP checksum with pseudo-header
|
| 341 |
+
pseudo_header = struct.pack(
|
| 342 |
+
'!4s4sBBH',
|
| 343 |
+
socket.inet_aton(source_ip),
|
| 344 |
+
socket.inet_aton(dest_ip),
|
| 345 |
+
0, IPProtocol.TCP.value,
|
| 346 |
+
len(header_data) + len(payload)
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
checksum_data = pseudo_header + header_data + payload
|
| 350 |
+
checksum = cls.calculate_checksum(checksum_data)
|
| 351 |
+
|
| 352 |
+
# Insert checksum
|
| 353 |
+
header_data = header_data[:16] + struct.pack('!H', checksum) + header_data[18:]
|
| 354 |
+
|
| 355 |
+
return header_data
|
| 356 |
+
|
| 357 |
+
@classmethod
|
| 358 |
+
def build_udp_header(cls, header: UDPHeader, source_ip: str, dest_ip: str, payload: bytes) -> bytes:
|
| 359 |
+
"""Build UDP header as bytes with checksum"""
|
| 360 |
+
header.length = 8 + len(payload)
|
| 361 |
+
|
| 362 |
+
# Build header without checksum
|
| 363 |
+
header_data = struct.pack(
|
| 364 |
+
'!HHHH',
|
| 365 |
+
header.source_port, header.dest_port,
|
| 366 |
+
header.length, 0 # Checksum = 0 for calculation
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# Calculate UDP checksum with pseudo-header (optional for IPv4)
|
| 370 |
+
if header.checksum != 0: # If checksum is required
|
| 371 |
+
pseudo_header = struct.pack(
|
| 372 |
+
'!4s4sBBH',
|
| 373 |
+
socket.inet_aton(source_ip),
|
| 374 |
+
socket.inet_aton(dest_ip),
|
| 375 |
+
0, IPProtocol.UDP.value,
|
| 376 |
+
header.length
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
checksum_data = pseudo_header + header_data + payload
|
| 380 |
+
checksum = cls.calculate_checksum(checksum_data)
|
| 381 |
+
|
| 382 |
+
# Insert checksum
|
| 383 |
+
header_data = header_data[:6] + struct.pack('!H', checksum) + header_data[8:]
|
| 384 |
+
|
| 385 |
+
return header_data
|
| 386 |
+
|
| 387 |
+
@classmethod
|
| 388 |
+
def build_packet(cls, ip_header: IPv4Header, transport_header: Optional[object] = None, payload: bytes = b'') -> bytes:
|
| 389 |
+
"""Build complete packet"""
|
| 390 |
+
transport_data = b''
|
| 391 |
+
|
| 392 |
+
# Build transport header
|
| 393 |
+
if transport_header:
|
| 394 |
+
if isinstance(transport_header, TCPHeader):
|
| 395 |
+
transport_data = cls.build_tcp_header(
|
| 396 |
+
transport_header, ip_header.source_ip, ip_header.dest_ip, payload
|
| 397 |
+
)
|
| 398 |
+
elif isinstance(transport_header, UDPHeader):
|
| 399 |
+
transport_data = cls.build_udp_header(
|
| 400 |
+
transport_header, ip_header.source_ip, ip_header.dest_ip, payload
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
# Update IP header total length
|
| 404 |
+
ip_header.total_length = ip_header.header_length + len(transport_data) + len(payload)
|
| 405 |
+
|
| 406 |
+
# Build IP header
|
| 407 |
+
ip_data = cls.build_ipv4_header(ip_header)
|
| 408 |
+
|
| 409 |
+
# Combine all parts
|
| 410 |
+
return ip_data + transport_data + payload
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
class PacketFragmenter:
|
| 414 |
+
"""Handle packet fragmentation and reassembly"""
|
| 415 |
+
|
| 416 |
+
def __init__(self, mtu: int = 1500):
|
| 417 |
+
self.mtu = mtu
|
| 418 |
+
self.fragments: Dict[Tuple[str, str, int], List[Tuple[int, bytes]]] = {} # (src, dst, id) -> [(offset, data)]
|
| 419 |
+
|
| 420 |
+
def fragment_packet(self, packet: bytes, mtu: int = None) -> List[bytes]:
|
| 421 |
+
"""Fragment a packet if it exceeds MTU"""
|
| 422 |
+
if mtu is None:
|
| 423 |
+
mtu = self.mtu
|
| 424 |
+
|
| 425 |
+
if len(packet) <= mtu:
|
| 426 |
+
return [packet]
|
| 427 |
+
|
| 428 |
+
# Parse original packet
|
| 429 |
+
parsed = IPParser.parse_packet(packet)
|
| 430 |
+
ip_header = parsed.ip_header
|
| 431 |
+
|
| 432 |
+
# Don't fragment if DF flag is set
|
| 433 |
+
if ip_header.dont_fragment:
|
| 434 |
+
raise ValueError("Packet too large and Don't Fragment flag is set")
|
| 435 |
+
|
| 436 |
+
fragments = []
|
| 437 |
+
payload_mtu = mtu - ip_header.header_length
|
| 438 |
+
payload_mtu = (payload_mtu // 8) * 8 # Must be multiple of 8 bytes
|
| 439 |
+
|
| 440 |
+
# Get the payload to fragment (everything after IP header)
|
| 441 |
+
payload_start = ip_header.header_length
|
| 442 |
+
payload = packet[payload_start:]
|
| 443 |
+
|
| 444 |
+
offset = 0
|
| 445 |
+
while offset < len(payload):
|
| 446 |
+
# Create fragment
|
| 447 |
+
fragment_payload = payload[offset:offset + payload_mtu]
|
| 448 |
+
|
| 449 |
+
# Create new IP header for fragment
|
| 450 |
+
frag_header = IPv4Header(
|
| 451 |
+
version=ip_header.version,
|
| 452 |
+
ihl=ip_header.ihl,
|
| 453 |
+
tos=ip_header.tos,
|
| 454 |
+
identification=ip_header.identification,
|
| 455 |
+
ttl=ip_header.ttl,
|
| 456 |
+
protocol=ip_header.protocol,
|
| 457 |
+
source_ip=ip_header.source_ip,
|
| 458 |
+
dest_ip=ip_header.dest_ip,
|
| 459 |
+
options=ip_header.options
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
# Set fragment offset and flags
|
| 463 |
+
frag_header.fragment_offset = (ip_header.fragment_offset * 8 + offset) // 8
|
| 464 |
+
frag_header.flags = ip_header.flags
|
| 465 |
+
|
| 466 |
+
# Set More Fragments flag if not last fragment
|
| 467 |
+
if offset + len(fragment_payload) < len(payload):
|
| 468 |
+
frag_header.flags |= 0x1 # More Fragments
|
| 469 |
+
else:
|
| 470 |
+
frag_header.flags &= ~0x1 # Clear More Fragments
|
| 471 |
+
|
| 472 |
+
# Build fragment
|
| 473 |
+
fragment = IPParser.build_packet(frag_header, payload=fragment_payload)
|
| 474 |
+
fragments.append(fragment)
|
| 475 |
+
|
| 476 |
+
offset += len(fragment_payload)
|
| 477 |
+
|
| 478 |
+
return fragments
|
| 479 |
+
|
| 480 |
+
def reassemble_packet(self, packet: bytes) -> Optional[bytes]:
|
| 481 |
+
"""Reassemble fragmented packet"""
|
| 482 |
+
parsed = IPParser.parse_packet(packet)
|
| 483 |
+
ip_header = parsed.ip_header
|
| 484 |
+
|
| 485 |
+
# If not a fragment, return as-is
|
| 486 |
+
if not ip_header.is_fragment:
|
| 487 |
+
return packet
|
| 488 |
+
|
| 489 |
+
# Create fragment key
|
| 490 |
+
key = (ip_header.source_ip, ip_header.dest_ip, ip_header.identification)
|
| 491 |
+
|
| 492 |
+
# Store fragment
|
| 493 |
+
if key not in self.fragments:
|
| 494 |
+
self.fragments[key] = []
|
| 495 |
+
|
| 496 |
+
payload_start = ip_header.header_length
|
| 497 |
+
fragment_data = packet[payload_start:]
|
| 498 |
+
self.fragments[key].append((ip_header.fragment_offset * 8, fragment_data))
|
| 499 |
+
|
| 500 |
+
# Check if we have all fragments
|
| 501 |
+
fragments = sorted(self.fragments[key])
|
| 502 |
+
|
| 503 |
+
# Verify we have contiguous fragments starting from 0
|
| 504 |
+
expected_offset = 0
|
| 505 |
+
complete_payload = b''
|
| 506 |
+
|
| 507 |
+
for offset, data in fragments:
|
| 508 |
+
if offset != expected_offset:
|
| 509 |
+
return None # Missing fragment
|
| 510 |
+
|
| 511 |
+
complete_payload += data
|
| 512 |
+
expected_offset += len(data)
|
| 513 |
+
|
| 514 |
+
# Check if last fragment (no More Fragments flag)
|
| 515 |
+
last_fragment = None
|
| 516 |
+
for frag_packet in [packet]: # We only have current packet, need to track all
|
| 517 |
+
frag_parsed = IPParser.parse_packet(frag_packet)
|
| 518 |
+
if not frag_parsed.ip_header.more_fragments:
|
| 519 |
+
last_fragment = frag_parsed
|
| 520 |
+
break
|
| 521 |
+
|
| 522 |
+
if last_fragment is None:
|
| 523 |
+
return None # Don't have last fragment yet
|
| 524 |
+
|
| 525 |
+
# Reassemble complete packet
|
| 526 |
+
complete_header = IPv4Header(
|
| 527 |
+
version=ip_header.version,
|
| 528 |
+
ihl=ip_header.ihl,
|
| 529 |
+
tos=ip_header.tos,
|
| 530 |
+
identification=ip_header.identification,
|
| 531 |
+
flags=ip_header.flags & ~0x1, # Clear More Fragments
|
| 532 |
+
fragment_offset=0,
|
| 533 |
+
ttl=ip_header.ttl,
|
| 534 |
+
protocol=ip_header.protocol,
|
| 535 |
+
source_ip=ip_header.source_ip,
|
| 536 |
+
dest_ip=ip_header.dest_ip,
|
| 537 |
+
options=ip_header.options
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
complete_packet = IPParser.build_packet(complete_header, payload=complete_payload)
|
| 541 |
+
|
| 542 |
+
# Clean up fragments
|
| 543 |
+
del self.fragments[key]
|
| 544 |
+
|
| 545 |
+
return complete_packet
|
| 546 |
+
|
core/logger.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Logger Module
|
| 3 |
+
|
| 4 |
+
Centralized logging system for the virtual ISP stack:
|
| 5 |
+
- Structured logging with multiple levels
|
| 6 |
+
- Log aggregation and filtering
|
| 7 |
+
- Real-time log streaming
|
| 8 |
+
- Log persistence and rotation
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import logging
|
| 12 |
+
import logging.handlers
|
| 13 |
+
import time
|
| 14 |
+
import threading
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
from typing import Dict, List, Optional, Any, Callable
|
| 18 |
+
from dataclasses import dataclass, asdict
|
| 19 |
+
from enum import Enum
|
| 20 |
+
from collections import deque
|
| 21 |
+
import queue
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class LogLevel(Enum):
|
| 25 |
+
DEBUG = "DEBUG"
|
| 26 |
+
INFO = "INFO"
|
| 27 |
+
WARNING = "WARNING"
|
| 28 |
+
ERROR = "ERROR"
|
| 29 |
+
CRITICAL = "CRITICAL"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class LogCategory(Enum):
|
| 33 |
+
SYSTEM = "SYSTEM"
|
| 34 |
+
DHCP = "DHCP"
|
| 35 |
+
NAT = "NAT"
|
| 36 |
+
FIREWALL = "FIREWALL"
|
| 37 |
+
TCP = "TCP"
|
| 38 |
+
ROUTER = "ROUTER"
|
| 39 |
+
BRIDGE = "BRIDGE"
|
| 40 |
+
SOCKET = "SOCKET"
|
| 41 |
+
SESSION = "SESSION"
|
| 42 |
+
SECURITY = "SECURITY"
|
| 43 |
+
PERFORMANCE = "PERFORMANCE"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@dataclass
|
| 47 |
+
class LogEntry:
|
| 48 |
+
"""Structured log entry"""
|
| 49 |
+
timestamp: float
|
| 50 |
+
level: str
|
| 51 |
+
category: str
|
| 52 |
+
module: str
|
| 53 |
+
message: str
|
| 54 |
+
session_id: Optional[str] = None
|
| 55 |
+
client_id: Optional[str] = None
|
| 56 |
+
source_ip: Optional[str] = None
|
| 57 |
+
dest_ip: Optional[str] = None
|
| 58 |
+
protocol: Optional[str] = None
|
| 59 |
+
metadata: Dict[str, Any] = None
|
| 60 |
+
|
| 61 |
+
def __post_init__(self):
|
| 62 |
+
if self.timestamp == 0:
|
| 63 |
+
self.timestamp = time.time()
|
| 64 |
+
if self.metadata is None:
|
| 65 |
+
self.metadata = {}
|
| 66 |
+
|
| 67 |
+
def to_dict(self) -> Dict:
|
| 68 |
+
"""Convert to dictionary"""
|
| 69 |
+
return asdict(self)
|
| 70 |
+
|
| 71 |
+
def to_json(self) -> str:
|
| 72 |
+
"""Convert to JSON string"""
|
| 73 |
+
return json.dumps(self.to_dict(), default=str)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class LogFilter:
|
| 77 |
+
"""Log filtering class"""
|
| 78 |
+
|
| 79 |
+
def __init__(self):
|
| 80 |
+
self.level_filter: Optional[LogLevel] = None
|
| 81 |
+
self.category_filter: Optional[LogCategory] = None
|
| 82 |
+
self.module_filter: Optional[str] = None
|
| 83 |
+
self.session_filter: Optional[str] = None
|
| 84 |
+
self.client_filter: Optional[str] = None
|
| 85 |
+
self.ip_filter: Optional[str] = None
|
| 86 |
+
self.text_filter: Optional[str] = None
|
| 87 |
+
self.time_range: Optional[tuple] = None
|
| 88 |
+
|
| 89 |
+
def matches(self, entry: LogEntry) -> bool:
|
| 90 |
+
"""Check if log entry matches filter criteria"""
|
| 91 |
+
# Level filter
|
| 92 |
+
if self.level_filter:
|
| 93 |
+
entry_level_value = getattr(logging, entry.level)
|
| 94 |
+
filter_level_value = getattr(logging, self.level_filter.value)
|
| 95 |
+
if entry_level_value < filter_level_value:
|
| 96 |
+
return False
|
| 97 |
+
|
| 98 |
+
# Category filter
|
| 99 |
+
if self.category_filter and entry.category != self.category_filter.value:
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
# Module filter
|
| 103 |
+
if self.module_filter and self.module_filter.lower() not in entry.module.lower():
|
| 104 |
+
return False
|
| 105 |
+
|
| 106 |
+
# Session filter
|
| 107 |
+
if self.session_filter and entry.session_id != self.session_filter:
|
| 108 |
+
return False
|
| 109 |
+
|
| 110 |
+
# Client filter
|
| 111 |
+
if self.client_filter and entry.client_id != self.client_filter:
|
| 112 |
+
return False
|
| 113 |
+
|
| 114 |
+
# IP filter
|
| 115 |
+
if self.ip_filter:
|
| 116 |
+
if (entry.source_ip != self.ip_filter and
|
| 117 |
+
entry.dest_ip != self.ip_filter):
|
| 118 |
+
return False
|
| 119 |
+
|
| 120 |
+
# Text filter
|
| 121 |
+
if self.text_filter and self.text_filter.lower() not in entry.message.lower():
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
# Time range filter
|
| 125 |
+
if self.time_range:
|
| 126 |
+
start_time, end_time = self.time_range
|
| 127 |
+
if not (start_time <= entry.timestamp <= end_time):
|
| 128 |
+
return False
|
| 129 |
+
|
| 130 |
+
return True
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class LogSubscriber:
|
| 134 |
+
"""Log subscriber for real-time streaming"""
|
| 135 |
+
|
| 136 |
+
def __init__(self, subscriber_id: str, callback: Callable[[LogEntry], None],
|
| 137 |
+
log_filter: Optional[LogFilter] = None):
|
| 138 |
+
self.subscriber_id = subscriber_id
|
| 139 |
+
self.callback = callback
|
| 140 |
+
self.filter = log_filter or LogFilter()
|
| 141 |
+
self.created_time = time.time()
|
| 142 |
+
self.message_count = 0
|
| 143 |
+
self.last_message_time = None
|
| 144 |
+
self.is_active = True
|
| 145 |
+
|
| 146 |
+
def send_log(self, entry: LogEntry) -> bool:
|
| 147 |
+
"""Send log entry to subscriber if it matches filter"""
|
| 148 |
+
if not self.is_active:
|
| 149 |
+
return False
|
| 150 |
+
|
| 151 |
+
if self.filter.matches(entry):
|
| 152 |
+
try:
|
| 153 |
+
self.callback(entry)
|
| 154 |
+
self.message_count += 1
|
| 155 |
+
self.last_message_time = time.time()
|
| 156 |
+
return True
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"Error sending log to subscriber {self.subscriber_id}: {e}")
|
| 159 |
+
self.is_active = False
|
| 160 |
+
return False
|
| 161 |
+
|
| 162 |
+
return False
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class VirtualISPLogger:
|
| 166 |
+
"""Centralized logger for Virtual ISP stack"""
|
| 167 |
+
|
| 168 |
+
def __init__(self, config: Dict):
|
| 169 |
+
self.config = config
|
| 170 |
+
self.log_entries: deque = deque(maxlen=config.get('max_memory_logs', 10000))
|
| 171 |
+
self.subscribers: Dict[str, LogSubscriber] = {}
|
| 172 |
+
self.lock = threading.Lock()
|
| 173 |
+
|
| 174 |
+
# Configuration
|
| 175 |
+
self.log_level = LogLevel(config.get('log_level', 'INFO'))
|
| 176 |
+
self.log_to_file = config.get('log_to_file', True)
|
| 177 |
+
self.log_file_path = config.get('log_file_path', '/tmp/virtual_isp.log')
|
| 178 |
+
self.log_file_max_size = config.get('log_file_max_size', 10 * 1024 * 1024) # 10MB
|
| 179 |
+
self.log_file_backup_count = config.get('log_file_backup_count', 5)
|
| 180 |
+
self.log_to_console = config.get('log_to_console', True)
|
| 181 |
+
self.structured_logging = config.get('structured_logging', True)
|
| 182 |
+
|
| 183 |
+
# Statistics
|
| 184 |
+
self.stats = {
|
| 185 |
+
'total_logs': 0,
|
| 186 |
+
'logs_by_level': {level.value: 0 for level in LogLevel},
|
| 187 |
+
'logs_by_category': {cat.value: 0 for cat in LogCategory},
|
| 188 |
+
'active_subscribers': 0,
|
| 189 |
+
'file_logs_written': 0,
|
| 190 |
+
'console_logs_written': 0,
|
| 191 |
+
'dropped_logs': 0
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
# Setup logging
|
| 195 |
+
self._setup_logging()
|
| 196 |
+
|
| 197 |
+
# Background processing
|
| 198 |
+
self.running = False
|
| 199 |
+
self.log_queue = queue.Queue()
|
| 200 |
+
self.processing_thread = None
|
| 201 |
+
|
| 202 |
+
def _setup_logging(self):
|
| 203 |
+
"""Setup Python logging infrastructure"""
|
| 204 |
+
# Create logger
|
| 205 |
+
self.logger = logging.getLogger('virtual_isp')
|
| 206 |
+
self.logger.setLevel(getattr(logging, self.log_level.value))
|
| 207 |
+
|
| 208 |
+
# Remove existing handlers
|
| 209 |
+
for handler in self.logger.handlers[:]:
|
| 210 |
+
self.logger.removeHandler(handler)
|
| 211 |
+
|
| 212 |
+
# Console handler
|
| 213 |
+
if self.log_to_console:
|
| 214 |
+
console_handler = logging.StreamHandler()
|
| 215 |
+
if self.structured_logging:
|
| 216 |
+
console_formatter = logging.Formatter(
|
| 217 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 218 |
+
)
|
| 219 |
+
else:
|
| 220 |
+
console_formatter = logging.Formatter('%(message)s')
|
| 221 |
+
console_handler.setFormatter(console_formatter)
|
| 222 |
+
self.logger.addHandler(console_handler)
|
| 223 |
+
|
| 224 |
+
# File handler with rotation
|
| 225 |
+
if self.log_to_file:
|
| 226 |
+
# Ensure log directory exists
|
| 227 |
+
log_dir = os.path.dirname(self.log_file_path)
|
| 228 |
+
if log_dir and not os.path.exists(log_dir):
|
| 229 |
+
os.makedirs(log_dir, exist_ok=True)
|
| 230 |
+
|
| 231 |
+
file_handler = logging.handlers.RotatingFileHandler(
|
| 232 |
+
self.log_file_path,
|
| 233 |
+
maxBytes=self.log_file_max_size,
|
| 234 |
+
backupCount=self.log_file_backup_count
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if self.structured_logging:
|
| 238 |
+
file_formatter = logging.Formatter(
|
| 239 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 240 |
+
)
|
| 241 |
+
else:
|
| 242 |
+
file_formatter = logging.Formatter('%(message)s')
|
| 243 |
+
|
| 244 |
+
file_handler.setFormatter(file_formatter)
|
| 245 |
+
self.logger.addHandler(file_handler)
|
| 246 |
+
|
| 247 |
+
def _process_log_queue(self):
|
| 248 |
+
"""Background thread to process log queue"""
|
| 249 |
+
while self.running:
|
| 250 |
+
try:
|
| 251 |
+
# Get log entry from queue (with timeout)
|
| 252 |
+
try:
|
| 253 |
+
entry = self.log_queue.get(timeout=1.0)
|
| 254 |
+
except queue.Empty:
|
| 255 |
+
continue
|
| 256 |
+
|
| 257 |
+
# Store in memory
|
| 258 |
+
with self.lock:
|
| 259 |
+
self.log_entries.append(entry)
|
| 260 |
+
|
| 261 |
+
# Send to subscribers
|
| 262 |
+
inactive_subscribers = []
|
| 263 |
+
with self.lock:
|
| 264 |
+
for subscriber_id, subscriber in self.subscribers.items():
|
| 265 |
+
if not subscriber.send_log(entry):
|
| 266 |
+
inactive_subscribers.append(subscriber_id)
|
| 267 |
+
|
| 268 |
+
# Remove inactive subscribers
|
| 269 |
+
for subscriber_id in inactive_subscribers:
|
| 270 |
+
self.remove_subscriber(subscriber_id)
|
| 271 |
+
|
| 272 |
+
# Update statistics
|
| 273 |
+
self.stats['total_logs'] += 1
|
| 274 |
+
self.stats['logs_by_level'][entry.level] += 1
|
| 275 |
+
self.stats['logs_by_category'][entry.category] += 1
|
| 276 |
+
|
| 277 |
+
# Mark task as done
|
| 278 |
+
self.log_queue.task_done()
|
| 279 |
+
|
| 280 |
+
except Exception as e:
|
| 281 |
+
print(f"Error processing log queue: {e}")
|
| 282 |
+
time.sleep(1)
|
| 283 |
+
|
| 284 |
+
def log(self, level: LogLevel, category: LogCategory, module: str, message: str,
|
| 285 |
+
session_id: Optional[str] = None, client_id: Optional[str] = None,
|
| 286 |
+
source_ip: Optional[str] = None, dest_ip: Optional[str] = None,
|
| 287 |
+
protocol: Optional[str] = None, **metadata):
|
| 288 |
+
"""Log a message"""
|
| 289 |
+
# Check if we should log this level
|
| 290 |
+
level_value = getattr(logging, level.value)
|
| 291 |
+
min_level_value = getattr(logging, self.log_level.value)
|
| 292 |
+
if level_value < min_level_value:
|
| 293 |
+
return
|
| 294 |
+
|
| 295 |
+
# Create log entry
|
| 296 |
+
entry = LogEntry(
|
| 297 |
+
timestamp=time.time(),
|
| 298 |
+
level=level.value,
|
| 299 |
+
category=category.value,
|
| 300 |
+
module=module,
|
| 301 |
+
message=message,
|
| 302 |
+
session_id=session_id,
|
| 303 |
+
client_id=client_id,
|
| 304 |
+
source_ip=source_ip,
|
| 305 |
+
dest_ip=dest_ip,
|
| 306 |
+
protocol=protocol,
|
| 307 |
+
metadata=metadata
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# Add to queue for background processing
|
| 311 |
+
try:
|
| 312 |
+
self.log_queue.put_nowait(entry)
|
| 313 |
+
except queue.Full:
|
| 314 |
+
self.stats['dropped_logs'] += 1
|
| 315 |
+
|
| 316 |
+
# Also log through Python logging system
|
| 317 |
+
if self.structured_logging:
|
| 318 |
+
log_data = entry.to_dict()
|
| 319 |
+
log_message = f"{message} | {json.dumps(log_data, default=str)}"
|
| 320 |
+
else:
|
| 321 |
+
log_message = message
|
| 322 |
+
|
| 323 |
+
# Log to Python logger
|
| 324 |
+
python_logger_level = getattr(logging, level.value)
|
| 325 |
+
self.logger.log(python_logger_level, log_message)
|
| 326 |
+
|
| 327 |
+
# Update console/file stats
|
| 328 |
+
if self.log_to_console:
|
| 329 |
+
self.stats['console_logs_written'] += 1
|
| 330 |
+
if self.log_to_file:
|
| 331 |
+
self.stats['file_logs_written'] += 1
|
| 332 |
+
|
| 333 |
+
def debug(self, category: LogCategory, module: str, message: str, **kwargs):
|
| 334 |
+
"""Log debug message"""
|
| 335 |
+
self.log(LogLevel.DEBUG, category, module, message, **kwargs)
|
| 336 |
+
|
| 337 |
+
def info(self, category: LogCategory, module: str, message: str, **kwargs):
|
| 338 |
+
"""Log info message"""
|
| 339 |
+
self.log(LogLevel.INFO, category, module, message, **kwargs)
|
| 340 |
+
|
| 341 |
+
def warning(self, category: LogCategory, module: str, message: str, **kwargs):
|
| 342 |
+
"""Log warning message"""
|
| 343 |
+
self.log(LogLevel.WARNING, category, module, message, **kwargs)
|
| 344 |
+
|
| 345 |
+
def error(self, category: LogCategory, module: str, message: str, **kwargs):
|
| 346 |
+
"""Log error message"""
|
| 347 |
+
self.log(LogLevel.ERROR, category, module, message, **kwargs)
|
| 348 |
+
|
| 349 |
+
def critical(self, category: LogCategory, module: str, message: str, **kwargs):
|
| 350 |
+
"""Log critical message"""
|
| 351 |
+
self.log(LogLevel.CRITICAL, category, module, message, **kwargs)
|
| 352 |
+
|
| 353 |
+
def add_subscriber(self, subscriber_id: str, callback: Callable[[LogEntry], None],
|
| 354 |
+
log_filter: Optional[LogFilter] = None) -> bool:
|
| 355 |
+
"""Add log subscriber for real-time streaming"""
|
| 356 |
+
with self.lock:
|
| 357 |
+
if subscriber_id in self.subscribers:
|
| 358 |
+
return False
|
| 359 |
+
|
| 360 |
+
subscriber = LogSubscriber(subscriber_id, callback, log_filter)
|
| 361 |
+
self.subscribers[subscriber_id] = subscriber
|
| 362 |
+
self.stats['active_subscribers'] = len(self.subscribers)
|
| 363 |
+
|
| 364 |
+
return True
|
| 365 |
+
|
| 366 |
+
def remove_subscriber(self, subscriber_id: str) -> bool:
|
| 367 |
+
"""Remove log subscriber"""
|
| 368 |
+
with self.lock:
|
| 369 |
+
if subscriber_id in self.subscribers:
|
| 370 |
+
del self.subscribers[subscriber_id]
|
| 371 |
+
self.stats['active_subscribers'] = len(self.subscribers)
|
| 372 |
+
return True
|
| 373 |
+
return False
|
| 374 |
+
|
| 375 |
+
def get_logs(self, limit: int = 100, offset: int = 0,
|
| 376 |
+
log_filter: Optional[LogFilter] = None) -> List[Dict]:
|
| 377 |
+
"""Get logs with filtering and pagination"""
|
| 378 |
+
with self.lock:
|
| 379 |
+
# Convert deque to list for easier manipulation
|
| 380 |
+
all_logs = list(self.log_entries)
|
| 381 |
+
|
| 382 |
+
# Apply filter
|
| 383 |
+
if log_filter:
|
| 384 |
+
filtered_logs = [entry for entry in all_logs if log_filter.matches(entry)]
|
| 385 |
+
else:
|
| 386 |
+
filtered_logs = all_logs
|
| 387 |
+
|
| 388 |
+
# Sort by timestamp (newest first)
|
| 389 |
+
filtered_logs.sort(key=lambda x: x.timestamp, reverse=True)
|
| 390 |
+
|
| 391 |
+
# Apply pagination
|
| 392 |
+
paginated_logs = filtered_logs[offset:offset + limit]
|
| 393 |
+
|
| 394 |
+
return [entry.to_dict() for entry in paginated_logs]
|
| 395 |
+
|
| 396 |
+
def search_logs(self, query: str, limit: int = 100) -> List[Dict]:
|
| 397 |
+
"""Search logs by text query"""
|
| 398 |
+
log_filter = LogFilter()
|
| 399 |
+
log_filter.text_filter = query
|
| 400 |
+
|
| 401 |
+
return self.get_logs(limit=limit, log_filter=log_filter)
|
| 402 |
+
|
| 403 |
+
def get_logs_by_session(self, session_id: str, limit: int = 100) -> List[Dict]:
|
| 404 |
+
"""Get logs for specific session"""
|
| 405 |
+
log_filter = LogFilter()
|
| 406 |
+
log_filter.session_filter = session_id
|
| 407 |
+
|
| 408 |
+
return self.get_logs(limit=limit, log_filter=log_filter)
|
| 409 |
+
|
| 410 |
+
def get_logs_by_client(self, client_id: str, limit: int = 100) -> List[Dict]:
|
| 411 |
+
"""Get logs for specific client"""
|
| 412 |
+
log_filter = LogFilter()
|
| 413 |
+
log_filter.client_filter = client_id
|
| 414 |
+
|
| 415 |
+
return self.get_logs(limit=limit, log_filter=log_filter)
|
| 416 |
+
|
| 417 |
+
def get_logs_by_ip(self, ip_address: str, limit: int = 100) -> List[Dict]:
|
| 418 |
+
"""Get logs for specific IP address"""
|
| 419 |
+
log_filter = LogFilter()
|
| 420 |
+
log_filter.ip_filter = ip_address
|
| 421 |
+
|
| 422 |
+
return self.get_logs(limit=limit, log_filter=log_filter)
|
| 423 |
+
|
| 424 |
+
def get_recent_errors(self, limit: int = 50) -> List[Dict]:
|
| 425 |
+
"""Get recent error and critical logs"""
|
| 426 |
+
log_filter = LogFilter()
|
| 427 |
+
log_filter.level_filter = LogLevel.ERROR
|
| 428 |
+
|
| 429 |
+
return self.get_logs(limit=limit, log_filter=log_filter)
|
| 430 |
+
|
| 431 |
+
def clear_logs(self):
|
| 432 |
+
"""Clear all logs from memory"""
|
| 433 |
+
with self.lock:
|
| 434 |
+
self.log_entries.clear()
|
| 435 |
+
|
| 436 |
+
def get_stats(self) -> Dict:
|
| 437 |
+
"""Get logging statistics"""
|
| 438 |
+
with self.lock:
|
| 439 |
+
stats = self.stats.copy()
|
| 440 |
+
stats['memory_logs_count'] = len(self.log_entries)
|
| 441 |
+
stats['active_subscribers'] = len(self.subscribers)
|
| 442 |
+
stats['queue_size'] = self.log_queue.qsize()
|
| 443 |
+
|
| 444 |
+
return stats
|
| 445 |
+
|
| 446 |
+
def reset_stats(self):
|
| 447 |
+
"""Reset logging statistics"""
|
| 448 |
+
self.stats = {
|
| 449 |
+
'total_logs': 0,
|
| 450 |
+
'logs_by_level': {level.value: 0 for level in LogLevel},
|
| 451 |
+
'logs_by_category': {cat.value: 0 for cat in LogCategory},
|
| 452 |
+
'active_subscribers': len(self.subscribers),
|
| 453 |
+
'file_logs_written': 0,
|
| 454 |
+
'console_logs_written': 0,
|
| 455 |
+
'dropped_logs': 0
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
def export_logs(self, format: str = 'json', log_filter: Optional[LogFilter] = None) -> str:
|
| 459 |
+
"""Export logs in specified format"""
|
| 460 |
+
logs = self.get_logs(limit=10000, log_filter=log_filter)
|
| 461 |
+
|
| 462 |
+
if format == 'json':
|
| 463 |
+
return json.dumps(logs, indent=2, default=str)
|
| 464 |
+
elif format == 'csv':
|
| 465 |
+
import csv
|
| 466 |
+
import io
|
| 467 |
+
|
| 468 |
+
output = io.StringIO()
|
| 469 |
+
if logs:
|
| 470 |
+
writer = csv.DictWriter(output, fieldnames=logs[0].keys())
|
| 471 |
+
writer.writeheader()
|
| 472 |
+
writer.writerows(logs)
|
| 473 |
+
|
| 474 |
+
return output.getvalue()
|
| 475 |
+
else:
|
| 476 |
+
raise ValueError(f"Unsupported export format: {format}")
|
| 477 |
+
|
| 478 |
+
def set_log_level(self, level: LogLevel):
|
| 479 |
+
"""Set logging level"""
|
| 480 |
+
self.log_level = level
|
| 481 |
+
self.logger.setLevel(getattr(logging, level.value))
|
| 482 |
+
|
| 483 |
+
def start(self):
|
| 484 |
+
"""Start logger"""
|
| 485 |
+
self.running = True
|
| 486 |
+
self.processing_thread = threading.Thread(target=self._process_log_queue, daemon=True)
|
| 487 |
+
self.processing_thread.start()
|
| 488 |
+
|
| 489 |
+
self.info(LogCategory.SYSTEM, 'logger', 'Virtual ISP Logger started')
|
| 490 |
+
|
| 491 |
+
def stop(self):
|
| 492 |
+
"""Stop logger"""
|
| 493 |
+
self.info(LogCategory.SYSTEM, 'logger', 'Virtual ISP Logger stopping')
|
| 494 |
+
|
| 495 |
+
self.running = False
|
| 496 |
+
|
| 497 |
+
# Wait for queue to be processed
|
| 498 |
+
self.log_queue.join()
|
| 499 |
+
|
| 500 |
+
# Wait for processing thread
|
| 501 |
+
if self.processing_thread:
|
| 502 |
+
self.processing_thread.join()
|
| 503 |
+
|
| 504 |
+
# Remove all subscribers
|
| 505 |
+
with self.lock:
|
| 506 |
+
self.subscribers.clear()
|
| 507 |
+
|
| 508 |
+
print("Virtual ISP Logger stopped")
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
# Global logger instance
|
| 512 |
+
_global_logger: Optional[VirtualISPLogger] = None
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def get_logger() -> Optional[VirtualISPLogger]:
|
| 516 |
+
"""Get global logger instance"""
|
| 517 |
+
return _global_logger
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def init_logger(config: Dict) -> VirtualISPLogger:
|
| 521 |
+
"""Initialize global logger"""
|
| 522 |
+
global _global_logger
|
| 523 |
+
_global_logger = VirtualISPLogger(config)
|
| 524 |
+
return _global_logger
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def log_debug(category: LogCategory, module: str, message: str, **kwargs):
|
| 528 |
+
"""Global debug logging function"""
|
| 529 |
+
if _global_logger:
|
| 530 |
+
_global_logger.debug(category, module, message, **kwargs)
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
def log_info(category: LogCategory, module: str, message: str, **kwargs):
|
| 534 |
+
"""Global info logging function"""
|
| 535 |
+
if _global_logger:
|
| 536 |
+
_global_logger.info(category, module, message, **kwargs)
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def log_warning(category: LogCategory, module: str, message: str, **kwargs):
|
| 540 |
+
"""Global warning logging function"""
|
| 541 |
+
if _global_logger:
|
| 542 |
+
_global_logger.warning(category, module, message, **kwargs)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def log_error(category: LogCategory, module: str, message: str, **kwargs):
|
| 546 |
+
"""Global error logging function"""
|
| 547 |
+
if _global_logger:
|
| 548 |
+
_global_logger.error(category, module, message, **kwargs)
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def log_critical(category: LogCategory, module: str, message: str, **kwargs):
|
| 552 |
+
"""Global critical logging function"""
|
| 553 |
+
if _global_logger:
|
| 554 |
+
_global_logger.critical(category, module, message, **kwargs)
|
| 555 |
+
|
core/nat_engine.py
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
NAT Engine Module
|
| 3 |
+
|
| 4 |
+
Implements Network Address Translation:
|
| 5 |
+
- Map (virtualIP, virtualPort) to (hostIP, hostPort)
|
| 6 |
+
- Maintain connection tracking table
|
| 7 |
+
- Handle port allocation and deallocation
|
| 8 |
+
- Support connection state tracking
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import time
|
| 12 |
+
import threading
|
| 13 |
+
import socket
|
| 14 |
+
import random
|
| 15 |
+
import struct
|
| 16 |
+
from typing import Dict, Optional, Tuple, Set
|
| 17 |
+
from dataclasses import dataclass
|
| 18 |
+
from enum import Enum
|
| 19 |
+
|
| 20 |
+
# Assuming IPProtocol is defined elsewhere or will be defined
|
| 21 |
+
# from .ip_parser import IPProtocol
|
| 22 |
+
|
| 23 |
+
class NATType(Enum):
|
| 24 |
+
SNAT = "SNAT" # Source NAT
|
| 25 |
+
DNAT = "DNAT" # Destination NAT
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class NATSession:
|
| 30 |
+
"""Represents a NAT session"""
|
| 31 |
+
# Virtual (internal) endpoint
|
| 32 |
+
virtual_ip: str
|
| 33 |
+
virtual_port: int
|
| 34 |
+
|
| 35 |
+
# Real (external) endpoint
|
| 36 |
+
real_ip: str
|
| 37 |
+
real_port: int
|
| 38 |
+
|
| 39 |
+
# Host (translated) endpoint
|
| 40 |
+
host_ip: str
|
| 41 |
+
host_port: int
|
| 42 |
+
|
| 43 |
+
# Session metadata
|
| 44 |
+
protocol: int # IP protocol number (e.g., 6 for TCP, 17 for UDP)
|
| 45 |
+
nat_type: NATType
|
| 46 |
+
created_time: float
|
| 47 |
+
last_activity: float
|
| 48 |
+
bytes_in: int = 0
|
| 49 |
+
bytes_out: int = 0
|
| 50 |
+
packets_in: int = 0
|
| 51 |
+
packets_out: int = 0
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def session_id(self) -> str:
|
| 55 |
+
"""Get unique session identifier"""
|
| 56 |
+
return f"{self.virtual_ip}:{self.virtual_port}-{self.real_ip}:{self.real_port}-{self.protocol}"
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def is_expired(self) -> bool:
|
| 60 |
+
"""Check if session has expired"""
|
| 61 |
+
timeout = 300 if self.protocol == socket.IPPROTO_TCP else 60 # 5 min for TCP, 1 min for UDP
|
| 62 |
+
return time.time() - self.last_activity > timeout
|
| 63 |
+
|
| 64 |
+
@property
|
| 65 |
+
def duration(self) -> float:
|
| 66 |
+
"""Get session duration in seconds"""
|
| 67 |
+
return time.time() - self.created_time
|
| 68 |
+
|
| 69 |
+
def update_activity(self, bytes_transferred: int = 0, direction: str = 'out'):
|
| 70 |
+
"""Update session activity"""
|
| 71 |
+
self.last_activity = time.time()
|
| 72 |
+
|
| 73 |
+
if direction == 'out':
|
| 74 |
+
self.bytes_out += bytes_transferred
|
| 75 |
+
self.packets_out += 1
|
| 76 |
+
else:
|
| 77 |
+
self.bytes_in += bytes_transferred
|
| 78 |
+
self.packets_in += 1
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class PortPool:
|
| 82 |
+
"""Manages available ports for NAT"""
|
| 83 |
+
|
| 84 |
+
def __init__(self, start_port: int = 10000, end_port: int = 65535):
|
| 85 |
+
self.start_port = start_port
|
| 86 |
+
self.end_port = end_port
|
| 87 |
+
self.available_ports: Set[int] = set(range(start_port, end_port + 1))
|
| 88 |
+
self.allocated_ports: Dict[int, str] = {} # port -> session_id
|
| 89 |
+
self.lock = threading.Lock()
|
| 90 |
+
|
| 91 |
+
def allocate_port(self, session_id: str) -> Optional[int]:
|
| 92 |
+
"""Allocate a port for a session"""
|
| 93 |
+
with self.lock:
|
| 94 |
+
if not self.available_ports:
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
# Try to get a random port to distribute load
|
| 98 |
+
port = random.choice(list(self.available_ports))
|
| 99 |
+
self.available_ports.remove(port)
|
| 100 |
+
self.allocated_ports[port] = session_id
|
| 101 |
+
|
| 102 |
+
return port
|
| 103 |
+
|
| 104 |
+
def release_port(self, port: int) -> bool:
|
| 105 |
+
"""Release a port back to the pool"""
|
| 106 |
+
with self.lock:
|
| 107 |
+
if port in self.allocated_ports:
|
| 108 |
+
del self.allocated_ports[port]
|
| 109 |
+
if self.start_port <= port <= self.end_port:
|
| 110 |
+
self.available_ports.add(port)
|
| 111 |
+
return True
|
| 112 |
+
return False
|
| 113 |
+
|
| 114 |
+
def get_session_for_port(self, port: int) -> Optional[str]:
|
| 115 |
+
"""Get session ID for a port"""
|
| 116 |
+
with self.lock:
|
| 117 |
+
return self.allocated_ports.get(port)
|
| 118 |
+
|
| 119 |
+
def get_stats(self) -> Dict:
|
| 120 |
+
"""Get port pool statistics"""
|
| 121 |
+
with self.lock:
|
| 122 |
+
return {
|
| 123 |
+
'total_ports': self.end_port - self.start_port + 1,
|
| 124 |
+
'available_ports': len(self.available_ports),
|
| 125 |
+
'allocated_ports': len(self.allocated_ports),
|
| 126 |
+
'utilization': len(self.allocated_ports) / (self.end_port - self.start_port + 1)
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class NATEngine:
|
| 131 |
+
"""Network Address Translation engine"""
|
| 132 |
+
|
| 133 |
+
def __init__(self, config: Dict):
|
| 134 |
+
self.config = config
|
| 135 |
+
self.sessions: Dict[str, NATSession] = {} # session_id -> session
|
| 136 |
+
self.virtual_to_session: Dict[Tuple[str, int, int], str] = {} # (vip, vport, proto) -> session_id
|
| 137 |
+
self.host_to_session: Dict[Tuple[str, int, int], str] = {} # (hip, hport, proto) -> session_id
|
| 138 |
+
self.lock = threading.Lock()
|
| 139 |
+
|
| 140 |
+
# Port pool for outbound connections
|
| 141 |
+
self.port_pool = PortPool(
|
| 142 |
+
config.get('port_range_start', 10000),
|
| 143 |
+
config.get('port_range_end', 65535)
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Host IP for outbound connections
|
| 147 |
+
self.host_ip = config.get('host_ip', self._get_default_host_ip())
|
| 148 |
+
|
| 149 |
+
# Session timeout
|
| 150 |
+
self.session_timeout = config.get('session_timeout', 300)
|
| 151 |
+
|
| 152 |
+
# Statistics
|
| 153 |
+
self.stats = {
|
| 154 |
+
'total_sessions': 0,
|
| 155 |
+
'active_sessions': 0,
|
| 156 |
+
'expired_sessions': 0,
|
| 157 |
+
'port_exhaustion_events': 0,
|
| 158 |
+
'bytes_translated': 0,
|
| 159 |
+
'packets_translated': 0
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
# Cleanup thread
|
| 163 |
+
self.running = False
|
| 164 |
+
self.cleanup_thread = None
|
| 165 |
+
|
| 166 |
+
def _get_default_host_ip(self) -> str:
|
| 167 |
+
"""Get default host IP address"""
|
| 168 |
+
try:
|
| 169 |
+
# Connect to a remote address to determine local IP
|
| 170 |
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
| 171 |
+
s.connect(('8.8.8.8', 80))
|
| 172 |
+
return s.getsockname()[0]
|
| 173 |
+
except Exception:
|
| 174 |
+
return '127.0.0.1'
|
| 175 |
+
|
| 176 |
+
def _cleanup_expired_sessions(self):
|
| 177 |
+
"""Clean up expired sessions"""
|
| 178 |
+
current_time = time.time()
|
| 179 |
+
expired_sessions = []
|
| 180 |
+
|
| 181 |
+
with self.lock:
|
| 182 |
+
for session_id, session in self.sessions.items():
|
| 183 |
+
if session.is_expired:
|
| 184 |
+
expired_sessions.append(session_id)
|
| 185 |
+
|
| 186 |
+
for session_id in expired_sessions:
|
| 187 |
+
self._remove_session(session_id)
|
| 188 |
+
self.stats['expired_sessions'] += 1
|
| 189 |
+
|
| 190 |
+
def _remove_session(self, session_id: str):
|
| 191 |
+
"""Remove a session and clean up resources"""
|
| 192 |
+
with self.lock:
|
| 193 |
+
if session_id not in self.sessions:
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
session = self.sessions[session_id]
|
| 197 |
+
|
| 198 |
+
# Remove from lookup tables
|
| 199 |
+
virtual_key = (session.virtual_ip, session.virtual_port, session.protocol)
|
| 200 |
+
if virtual_key in self.virtual_to_session:
|
| 201 |
+
del self.virtual_to_session[virtual_key]
|
| 202 |
+
|
| 203 |
+
host_key = (session.host_ip, session.host_port, session.protocol)
|
| 204 |
+
if host_key in self.host_to_session:
|
| 205 |
+
del self.host_to_session[host_key]
|
| 206 |
+
|
| 207 |
+
# Release port
|
| 208 |
+
self.port_pool.release_port(session.host_port)
|
| 209 |
+
|
| 210 |
+
# Remove session
|
| 211 |
+
del self.sessions[session_id]
|
| 212 |
+
|
| 213 |
+
self.stats['active_sessions'] = len(self.sessions)
|
| 214 |
+
|
| 215 |
+
def create_outbound_session(self, virtual_ip: str, virtual_port: int,
|
| 216 |
+
real_ip: str, real_port: int, protocol: int) -> Optional[NATSession]:
|
| 217 |
+
"""Create NAT session for outbound connection"""
|
| 218 |
+
# Allocate host port
|
| 219 |
+
session_id = f"{virtual_ip}:{virtual_port}-{real_ip}:{real_port}-{protocol}"
|
| 220 |
+
host_port = self.port_pool.allocate_port(session_id)
|
| 221 |
+
|
| 222 |
+
if host_port is None:
|
| 223 |
+
self.stats['port_exhaustion_events'] += 1
|
| 224 |
+
return None
|
| 225 |
+
|
| 226 |
+
# Create session
|
| 227 |
+
session = NATSession(
|
| 228 |
+
virtual_ip=virtual_ip,
|
| 229 |
+
virtual_port=virtual_port,
|
| 230 |
+
real_ip=real_ip,
|
| 231 |
+
real_port=real_port,
|
| 232 |
+
host_ip=self.host_ip,
|
| 233 |
+
host_port=host_port,
|
| 234 |
+
protocol=protocol,
|
| 235 |
+
nat_type=NATType.SNAT,
|
| 236 |
+
created_time=time.time(),
|
| 237 |
+
last_activity=time.time()
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
with self.lock:
|
| 241 |
+
self.sessions[session_id] = session
|
| 242 |
+
|
| 243 |
+
# Add to lookup tables
|
| 244 |
+
virtual_key = (virtual_ip, virtual_port, protocol)
|
| 245 |
+
self.virtual_to_session[virtual_key] = session_id
|
| 246 |
+
|
| 247 |
+
host_key = (self.host_ip, host_port, protocol)
|
| 248 |
+
self.host_to_session[host_key] = session_id
|
| 249 |
+
|
| 250 |
+
self.stats['total_sessions'] += 1
|
| 251 |
+
self.stats['active_sessions'] = len(self.sessions)
|
| 252 |
+
|
| 253 |
+
return session
|
| 254 |
+
|
| 255 |
+
def translate_outbound(self, virtual_ip: str, virtual_port: int,
|
| 256 |
+
real_ip: str, real_port: int, protocol: int) -> Optional[Tuple[str, int]]:
|
| 257 |
+
"""Translate outbound packet (virtual -> host)"""
|
| 258 |
+
virtual_key = (virtual_ip, virtual_port, protocol)
|
| 259 |
+
|
| 260 |
+
with self.lock:
|
| 261 |
+
session_id = self.virtual_to_session.get(virtual_key)
|
| 262 |
+
|
| 263 |
+
if session_id:
|
| 264 |
+
session = self.sessions[session_id]
|
| 265 |
+
session.update_activity(direction='out')
|
| 266 |
+
return (session.host_ip, session.host_port)
|
| 267 |
+
else:
|
| 268 |
+
# Create new session
|
| 269 |
+
session = self.create_outbound_session(virtual_ip, virtual_port, real_ip, real_port, protocol)
|
| 270 |
+
if session:
|
| 271 |
+
return (session.host_ip, session.host_port)
|
| 272 |
+
|
| 273 |
+
return None
|
| 274 |
+
|
| 275 |
+
def translate_inbound(self, host_ip: str, host_port: int, protocol: int) -> Optional[Tuple[str, int]]:
|
| 276 |
+
"""Translate inbound packet (host -> virtual)"""
|
| 277 |
+
host_key = (host_ip, host_port, protocol)
|
| 278 |
+
|
| 279 |
+
with self.lock:
|
| 280 |
+
session_id = self.host_to_session.get(host_key)
|
| 281 |
+
|
| 282 |
+
if session_id and session_id in self.sessions:
|
| 283 |
+
session = self.sessions[session_id]
|
| 284 |
+
session.update_activity(direction='in')
|
| 285 |
+
return (session.virtual_ip, session.virtual_port)
|
| 286 |
+
|
| 287 |
+
return None
|
| 288 |
+
|
| 289 |
+
def get_session_by_virtual(self, virtual_ip: str, virtual_port: int, protocol: int) -> Optional[NATSession]:
|
| 290 |
+
"""Get session by virtual endpoint"""
|
| 291 |
+
virtual_key = (virtual_ip, virtual_port, protocol)
|
| 292 |
+
|
| 293 |
+
with self.lock:
|
| 294 |
+
session_id = self.virtual_to_session.get(virtual_key)
|
| 295 |
+
if session_id and session_id in self.sessions:
|
| 296 |
+
return self.sessions[session_id]
|
| 297 |
+
|
| 298 |
+
return None
|
| 299 |
+
|
| 300 |
+
def get_session_by_host(self, host_ip: str, host_port: int, protocol: int) -> Optional[NATSession]:
|
| 301 |
+
"""Get session by host endpoint"""
|
| 302 |
+
host_key = (host_ip, host_port, protocol)
|
| 303 |
+
|
| 304 |
+
with self.lock:
|
| 305 |
+
session_id = self.host_to_session.get(host_key)
|
| 306 |
+
if session_id and session_id in self.sessions:
|
| 307 |
+
return self.sessions[session_id]
|
| 308 |
+
|
| 309 |
+
return None
|
| 310 |
+
|
| 311 |
+
def close_session(self, session_id: str) -> bool:
|
| 312 |
+
"""Manually close a session"""
|
| 313 |
+
with self.lock:
|
| 314 |
+
if session_id in self.sessions:
|
| 315 |
+
self._remove_session(session_id)
|
| 316 |
+
return True
|
| 317 |
+
return False
|
| 318 |
+
|
| 319 |
+
def close_session_by_virtual(self, virtual_ip: str, virtual_port: int, protocol: int) -> bool:
|
| 320 |
+
"""Close session by virtual endpoint"""
|
| 321 |
+
virtual_key = (virtual_ip, virtual_port, protocol)
|
| 322 |
+
|
| 323 |
+
with self.lock:
|
| 324 |
+
session_id = self.virtual_to_session.get(virtual_key)
|
| 325 |
+
if session_id:
|
| 326 |
+
self._remove_session(session_id)
|
| 327 |
+
return True
|
| 328 |
+
return False
|
| 329 |
+
|
| 330 |
+
def get_sessions(self) -> Dict[str, Dict]:
|
| 331 |
+
"""Get all active sessions"""
|
| 332 |
+
with self.lock:
|
| 333 |
+
return {
|
| 334 |
+
session_id: {
|
| 335 |
+
'virtual_ip': session.virtual_ip,
|
| 336 |
+
'virtual_port': session.virtual_port,
|
| 337 |
+
'real_ip': session.real_ip,
|
| 338 |
+
'real_port': session.real_port,
|
| 339 |
+
'host_ip': session.host_ip,
|
| 340 |
+
'host_port': session.host_port,
|
| 341 |
+
'protocol': session.protocol,
|
| 342 |
+
'nat_type': session.nat_type.value,
|
| 343 |
+
'created_time': session.created_time,
|
| 344 |
+
'last_activity': session.last_activity,
|
| 345 |
+
'duration': session.duration,
|
| 346 |
+
'bytes_in': session.bytes_in,
|
| 347 |
+
'bytes_out': session.bytes_out,
|
| 348 |
+
'packets_in': session.packets_in,
|
| 349 |
+
'packets_out': session.packets_out,
|
| 350 |
+
'is_expired': session.is_expired
|
| 351 |
+
}
|
| 352 |
+
for session_id, session in self.sessions.items()
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
def get_stats(self) -> Dict:
|
| 356 |
+
"""Get NAT statistics"""
|
| 357 |
+
port_stats = self.port_pool.get_stats()
|
| 358 |
+
|
| 359 |
+
with self.lock:
|
| 360 |
+
current_stats = self.stats.copy()
|
| 361 |
+
current_stats['active_sessions'] = len(self.sessions)
|
| 362 |
+
current_stats.update(port_stats)
|
| 363 |
+
|
| 364 |
+
return current_stats
|
| 365 |
+
|
| 366 |
+
def update_packet_stats(self, bytes_count: int):
|
| 367 |
+
"""Update packet statistics"""
|
| 368 |
+
self.stats['bytes_translated'] += bytes_count
|
| 369 |
+
self.stats['packets_translated'] += 1
|
| 370 |
+
|
| 371 |
+
def _cleanup_loop(self):
|
| 372 |
+
"""Background cleanup loop"""
|
| 373 |
+
while self.running:
|
| 374 |
+
try:
|
| 375 |
+
# print("NAT cleanup loop: Cleaning expired sessions...") # Debug print
|
| 376 |
+
self._cleanup_expired_sessions()
|
| 377 |
+
time.sleep(0.1) # Shorter sleep for faster testing
|
| 378 |
+
except Exception as e:
|
| 379 |
+
print(f"NAT cleanup error: {e}")
|
| 380 |
+
time.sleep(0.1)
|
| 381 |
+
|
| 382 |
+
def start(self):
|
| 383 |
+
"""Start NAT engine"""
|
| 384 |
+
self.running = True
|
| 385 |
+
self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
|
| 386 |
+
self.cleanup_thread.start()
|
| 387 |
+
# print(f"NAT engine started - Host IP: {self.host_ip}, Port range: {self.port_pool.start_port}-{self.port_pool.end_port}")
|
| 388 |
+
|
| 389 |
+
def stop(self):
|
| 390 |
+
"""Stop NAT engine"""
|
| 391 |
+
# print("Stopping NAT engine...") # Debug print
|
| 392 |
+
self.running = False
|
| 393 |
+
if self.cleanup_thread and self.cleanup_thread.is_alive():
|
| 394 |
+
self.cleanup_thread.join(timeout=1) # Add timeout to join
|
| 395 |
+
if self.cleanup_thread.is_alive():
|
| 396 |
+
print("NAT cleanup thread did not terminate in time.") # Debug print
|
| 397 |
+
|
| 398 |
+
# Close all sessions
|
| 399 |
+
with self.lock:
|
| 400 |
+
session_ids = list(self.sessions.keys())
|
| 401 |
+
for session_id in session_ids:
|
| 402 |
+
self._remove_session(session_id)
|
| 403 |
+
|
| 404 |
+
# print("NAT engine stopped")
|
| 405 |
+
|
| 406 |
+
def _calculate_ip_checksum(self, ip_header_no_checksum: bytes) -> int:
|
| 407 |
+
"""Calculate the IP header checksum."""
|
| 408 |
+
# IP header checksum calculation (simplified for demonstration)
|
| 409 |
+
# This is a basic implementation and might need refinement for production use
|
| 410 |
+
s = 0
|
| 411 |
+
# loop through header words
|
| 412 |
+
for i in range(0, len(ip_header_no_checksum), 2):
|
| 413 |
+
w = (ip_header_no_checksum[i] << 8) + (ip_header_no_checksum[i+1])
|
| 414 |
+
s = s + w
|
| 415 |
+
|
| 416 |
+
s = (s & 0xffff) + (s >> 16)
|
| 417 |
+
s = s + (s >> 16)
|
| 418 |
+
return ~s & 0xffff
|
| 419 |
+
|
| 420 |
+
def process_inbound_packet(self, packet: bytes) -> Optional[bytes]:
|
| 421 |
+
"""Process an inbound packet (from internet to VPN client) for DNAT."""
|
| 422 |
+
# Parse IP header
|
| 423 |
+
# Assuming Ethernet frame, IP header starts at offset 14
|
| 424 |
+
# For simplicity, let's assume we are only dealing with IPv4 for now
|
| 425 |
+
ip_header_offset = 14
|
| 426 |
+
ip_header_length = (packet[ip_header_offset] & 0xF) * 4
|
| 427 |
+
ip_header = packet[ip_header_offset : ip_header_offset + ip_header_length]
|
| 428 |
+
|
| 429 |
+
# Unpack IP header (version_ihl, tos, total_length, identification, fragment_offset, ttl, protocol, header_checksum, source_address, destination_address)
|
| 430 |
+
iph = struct.unpack('!BBHHHBBH4s4s', ip_header)
|
| 431 |
+
|
| 432 |
+
protocol = iph[6]
|
| 433 |
+
source_ip = socket.inet_ntoa(iph[8])
|
| 434 |
+
dest_ip = socket.inet_ntoa(iph[9])
|
| 435 |
+
|
| 436 |
+
# Only process TCP/UDP for now
|
| 437 |
+
if protocol not in [socket.IPPROTO_TCP, socket.IPPROTO_UDP]:
|
| 438 |
+
return None
|
| 439 |
+
|
| 440 |
+
# Parse TCP/UDP header
|
| 441 |
+
transport_header_offset = ip_header_offset + ip_header_length
|
| 442 |
+
if protocol == socket.IPPROTO_TCP:
|
| 443 |
+
tcp_header = packet[transport_header_offset : transport_header_offset + 20]
|
| 444 |
+
tcph = struct.unpack('!HHLLBBHHH', tcp_header)
|
| 445 |
+
source_port = tcph[0]
|
| 446 |
+
dest_port = tcph[1]
|
| 447 |
+
elif protocol == socket.IPPROTO_UDP:
|
| 448 |
+
udp_header = packet[transport_header_offset : transport_header_offset + 8]
|
| 449 |
+
udph = struct.unpack('!HHHH', udp_header)
|
| 450 |
+
source_port = udph[0]
|
| 451 |
+
dest_port = udph[1]
|
| 452 |
+
else:
|
| 453 |
+
return None
|
| 454 |
+
|
| 455 |
+
# Check for DNAT rule match (simplified for now, actual DNAT rules would be in DNATEngine)
|
| 456 |
+
# For now, assume we are looking for a session based on host_ip (d_addr) and host_port (dest_port)
|
| 457 |
+
translated_endpoint = self.translate_inbound(dest_ip, dest_port, protocol)
|
| 458 |
+
|
| 459 |
+
if translated_endpoint:
|
| 460 |
+
virtual_ip, virtual_port = translated_endpoint
|
| 461 |
+
|
| 462 |
+
# Reconstruct packet with translated destination IP and port
|
| 463 |
+
# Recalculate IP header checksum
|
| 464 |
+
new_dest_ip_bytes = socket.inet_aton(virtual_ip)
|
| 465 |
+
|
| 466 |
+
# Rebuild IP header with new destination IP
|
| 467 |
+
# Need to recalculate checksum for IP header
|
| 468 |
+
# For simplicity, we'll set checksum to 0 and assume it's recalculated later or by OS
|
| 469 |
+
new_ip_header_raw = struct.pack('!BBHHHBBH4s4s', iph[0], iph[1], iph[2], iph[3], iph[4], iph[5], iph[6], 0, iph[8], new_dest_ip_bytes)
|
| 470 |
+
new_ip_header_checksum = self._calculate_ip_checksum(new_ip_header_raw)
|
| 471 |
+
new_ip_header = struct.pack('!BBHHHBBH4s4s', iph[0], iph[1], iph[2], iph[3], iph[4], iph[5], iph[6], new_ip_header_checksum, iph[8], new_dest_ip_bytes)
|
| 472 |
+
|
| 473 |
+
# Rebuild TCP/UDP header with new destination port
|
| 474 |
+
if protocol == socket.IPPROTO_TCP:
|
| 475 |
+
# Recalculate TCP checksum (requires pseudo-header, IP header, and TCP data)
|
| 476 |
+
new_tcp_header_raw = struct.pack('!HHLLBBHHH', source_port, virtual_port, tcph[2], tcph[3], tcph[4], tcph[5], tcph[6], 0, tcph[8])
|
| 477 |
+
# For now, setting checksum to 0. Proper recalculation is complex.
|
| 478 |
+
new_tcp_header = struct.pack('!HHLLBBHHH', source_port, virtual_port, tcph[2], tcph[3], tcph[4], tcph[5], tcph[6], 0, tcph[8])
|
| 479 |
+
return packet[:ip_header_offset] + new_ip_header + new_tcp_header + packet[transport_header_offset + 20:]
|
| 480 |
+
elif protocol == socket.IPPROTO_UDP:
|
| 481 |
+
# Recalculate UDP checksum (requires pseudo-header, IP header, and UDP data)
|
| 482 |
+
new_udp_header_raw = struct.pack('!HHHH', source_port, virtual_port, udph[2], 0)
|
| 483 |
+
# For now, setting checksum to 0. Proper recalculation is complex.
|
| 484 |
+
new_udp_header = struct.pack('!HHHH', source_port, virtual_port, udph[2], 0)
|
| 485 |
+
return packet[:ip_header_offset] + new_ip_header + new_udp_header + packet[transport_header_offset + 8:]
|
| 486 |
+
|
| 487 |
+
return None
|
| 488 |
+
|
| 489 |
+
def process_outbound_packet(self, packet: bytes) -> Optional[bytes]:
|
| 490 |
+
"""Process an outbound packet (from VPN client to internet) for SNAT."""
|
| 491 |
+
# Parse IP header
|
| 492 |
+
ip_header_offset = 14
|
| 493 |
+
ip_header_length = (packet[ip_header_offset] & 0xF) * 4
|
| 494 |
+
ip_header = packet[ip_header_offset : ip_header_offset + ip_header_length]
|
| 495 |
+
|
| 496 |
+
# Unpack IP header
|
| 497 |
+
iph = struct.unpack('!BBHHHBBH4s4s', ip_header)
|
| 498 |
+
|
| 499 |
+
protocol = iph[6]
|
| 500 |
+
source_ip = socket.inet_ntoa(iph[8])
|
| 501 |
+
dest_ip = socket.inet_ntoa(iph[9])
|
| 502 |
+
|
| 503 |
+
# Only process TCP/UDP for now
|
| 504 |
+
if protocol not in [socket.IPPROTO_TCP, socket.IPPROTO_UDP]:
|
| 505 |
+
return None
|
| 506 |
+
|
| 507 |
+
# Parse TCP/UDP header
|
| 508 |
+
transport_header_offset = ip_header_offset + ip_header_length
|
| 509 |
+
if protocol == socket.IPPROTO_TCP:
|
| 510 |
+
tcp_header = packet[transport_header_offset : transport_header_offset + 20]
|
| 511 |
+
tcph = struct.unpack('!HHLLBBHHH', tcp_header)
|
| 512 |
+
source_port = tcph[0]
|
| 513 |
+
dest_port = tcph[1]
|
| 514 |
+
elif protocol == socket.IPPROTO_UDP:
|
| 515 |
+
udp_header = packet[transport_header_offset : transport_header_offset + 8]
|
| 516 |
+
udph = struct.unpack('!HHHH', udp_header)
|
| 517 |
+
source_port = udph[0]
|
| 518 |
+
dest_port = udph[1]
|
| 519 |
+
else:
|
| 520 |
+
return None
|
| 521 |
+
|
| 522 |
+
# Perform SNAT
|
| 523 |
+
translated_endpoint = self.translate_outbound(source_ip, source_port, dest_ip, dest_port, protocol)
|
| 524 |
+
|
| 525 |
+
if translated_endpoint:
|
| 526 |
+
host_ip, host_port = translated_endpoint
|
| 527 |
+
|
| 528 |
+
# Reconstruct packet with translated source IP and port
|
| 529 |
+
# Recalculate IP header checksum
|
| 530 |
+
new_source_ip_bytes = socket.inet_aton(host_ip)
|
| 531 |
+
|
| 532 |
+
# Rebuild IP header with new source IP
|
| 533 |
+
new_ip_header_raw = struct.pack('!BBHHHBBH4s4s', iph[0], iph[1], iph[2], iph[3], iph[4], iph[5], iph[6], 0, new_source_ip_bytes, iph[9])
|
| 534 |
+
new_ip_header_checksum = self._calculate_ip_checksum(new_ip_header_raw)
|
| 535 |
+
new_ip_header = struct.pack('!BBHHHBBH4s4s', iph[0], iph[1], iph[2], iph[3], iph[4], iph[5], iph[6], new_ip_header_checksum, new_source_ip_bytes, iph[9])
|
| 536 |
+
|
| 537 |
+
# Rebuild TCP/UDP header with new source port
|
| 538 |
+
if protocol == socket.IPPROTO_TCP:
|
| 539 |
+
# Recalculate TCP checksum
|
| 540 |
+
new_tcp_header_raw = struct.pack('!HHLLBBHHH', host_port, dest_port, tcph[2], tcph[3], tcph[4], tcph[5], tcph[6], 0, tcph[8])
|
| 541 |
+
# For now, setting checksum to 0. Proper recalculation is complex.
|
| 542 |
+
new_tcp_header = struct.pack('!HHLLBBHHH', host_port, dest_port, tcph[2], tcph[3], tcph[4], tcph[5], tcph[6], 0, tcph[8])
|
| 543 |
+
return packet[:ip_header_offset] + new_ip_header + new_tcp_header + packet[transport_header_offset + 20:]
|
| 544 |
+
elif protocol == socket.IPPROTO_UDP:
|
| 545 |
+
# Recalculate UDP checksum
|
| 546 |
+
new_udp_header_raw = struct.pack('!HHHH', host_port, dest_port, udph[2], 0)
|
| 547 |
+
# For now, setting checksum to 0. Proper recalculation is complex.
|
| 548 |
+
new_udp_header = struct.pack('!HHHH', host_port, dest_port, udph[2], 0)
|
| 549 |
+
return packet[:ip_header_offset] + new_ip_header + new_udp_header + packet[transport_header_offset + 8:]
|
| 550 |
+
|
| 551 |
+
return None
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
class NATRule:
|
| 555 |
+
"""Represents a NAT rule for DNAT (port forwarding)"""
|
| 556 |
+
|
| 557 |
+
def __init__(self, external_port: int, internal_ip: str, internal_port: int,
|
| 558 |
+
protocol: int, enabled: bool = True):
|
| 559 |
+
self.external_port = external_port
|
| 560 |
+
self.internal_ip = internal_ip
|
| 561 |
+
self.internal_port = internal_port
|
| 562 |
+
self.protocol = protocol
|
| 563 |
+
self.enabled = enabled
|
| 564 |
+
self.created_time = time.time()
|
| 565 |
+
self.hit_count = 0
|
| 566 |
+
self.last_hit = None
|
| 567 |
+
|
| 568 |
+
def matches(self, port: int, protocol: int) -> bool:
|
| 569 |
+
"""Check if rule matches the given port and protocol"""
|
| 570 |
+
return (self.enabled and
|
| 571 |
+
self.external_port == port and
|
| 572 |
+
self.protocol == protocol)
|
| 573 |
+
|
| 574 |
+
def record_hit(self):
|
| 575 |
+
"""Record a rule hit"""
|
| 576 |
+
self.hit_count += 1
|
| 577 |
+
self.last_hit = time.time()
|
| 578 |
+
|
| 579 |
+
def to_dict(self) -> Dict:
|
| 580 |
+
"""Convert rule to dictionary"""
|
| 581 |
+
return {
|
| 582 |
+
'external_port': self.external_port,
|
| 583 |
+
'internal_ip': self.internal_ip,
|
| 584 |
+
'internal_port': self.internal_port,
|
| 585 |
+
'protocol': self.protocol,
|
| 586 |
+
'enabled': self.enabled,
|
| 587 |
+
'created_time': self.created_time,
|
| 588 |
+
'hit_count': self.hit_count,
|
| 589 |
+
'last_hit': self.last_hit
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
class DNATEngine:
|
| 594 |
+
"""Destination NAT engine for port forwarding"""
|
| 595 |
+
|
| 596 |
+
def __init__(self):
|
| 597 |
+
self.rules: Dict[str, NATRule] = {} # rule_id -> rule
|
| 598 |
+
self.lock = threading.Lock()
|
| 599 |
+
|
| 600 |
+
def add_rule(self, rule_id: str, external_port: int, internal_ip: str,
|
| 601 |
+
internal_port: int, protocol: int) -> bool:
|
| 602 |
+
"""Add DNAT rule"""
|
| 603 |
+
with self.lock:
|
| 604 |
+
if rule_id in self.rules:
|
| 605 |
+
return False
|
| 606 |
+
rule = NATRule(external_port, internal_ip, internal_port, protocol)
|
| 607 |
+
self.rules[rule_id] = rule
|
| 608 |
+
return True
|
| 609 |
+
|
| 610 |
+
def remove_rule(self, rule_id: str) -> bool:
|
| 611 |
+
"""Remove DNAT rule"""
|
| 612 |
+
with self.lock:
|
| 613 |
+
if rule_id in self.rules:
|
| 614 |
+
del self.rules[rule_id]
|
| 615 |
+
return True
|
| 616 |
+
return False
|
| 617 |
+
|
| 618 |
+
def get_rule(self, rule_id: str) -> Optional[NATRule]:
|
| 619 |
+
"""Get DNAT rule by ID"""
|
| 620 |
+
with self.lock:
|
| 621 |
+
return self.rules.get(rule_id)
|
| 622 |
+
|
| 623 |
+
def get_matching_rule(self, port: int, protocol: int) -> Optional[NATRule]:
|
| 624 |
+
"""Get matching DNAT rule for given port and protocol"""
|
| 625 |
+
with self.lock:
|
| 626 |
+
for rule in self.rules.values():
|
| 627 |
+
if rule.matches(port, protocol):
|
| 628 |
+
rule.record_hit()
|
| 629 |
+
return rule
|
| 630 |
+
return None
|
| 631 |
+
|
| 632 |
+
def get_all_rules(self) -> Dict[str, Dict]:
|
| 633 |
+
"""Get all DNAT rules"""
|
| 634 |
+
with self.lock:
|
| 635 |
+
return {rule_id: rule.to_dict() for rule_id, rule in self.rules.items()}
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
|
core/openvpn_manager.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OpenVPN Manager Module
|
| 3 |
+
|
| 4 |
+
Manages OpenVPN server integration with the Virtual ISP Stack
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
import subprocess
|
| 10 |
+
import threading
|
| 11 |
+
import time
|
| 12 |
+
import logging
|
| 13 |
+
from typing import Dict, List, Optional, Any
|
| 14 |
+
from dataclasses import dataclass, asdict
|
| 15 |
+
import ipaddress
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class VPNClient:
|
| 21 |
+
"""Represents a connected VPN client"""
|
| 22 |
+
client_id: str
|
| 23 |
+
common_name: str
|
| 24 |
+
ip_address: str
|
| 25 |
+
connected_at: float
|
| 26 |
+
bytes_received: int = 0
|
| 27 |
+
bytes_sent: int = 0
|
| 28 |
+
status: str = "connected"
|
| 29 |
+
routed_through_vpn: bool = False
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class VPNServerStatus:
|
| 33 |
+
"""Represents VPN server status"""
|
| 34 |
+
is_running: bool
|
| 35 |
+
connected_clients: int
|
| 36 |
+
total_bytes_received: int
|
| 37 |
+
total_bytes_sent: int
|
| 38 |
+
uptime: float
|
| 39 |
+
server_ip: str
|
| 40 |
+
server_port: int
|
| 41 |
+
|
| 42 |
+
class OpenVPNManager:
|
| 43 |
+
"""Manages OpenVPN server and client connections with traffic routing"""
|
| 44 |
+
|
| 45 |
+
def __init__(self, config: Dict[str, Any]):
|
| 46 |
+
self.config = config
|
| 47 |
+
self.server_config_path = "/etc/openvpn/server/server.conf"
|
| 48 |
+
self.status_log_path = "/tmp/openvpn/openvpn-status.log"
|
| 49 |
+
self.clients: Dict[str, VPNClient] = {}
|
| 50 |
+
self.server_process = None
|
| 51 |
+
self.is_running = False
|
| 52 |
+
self.start_time = None
|
| 53 |
+
|
| 54 |
+
# VPN network configuration
|
| 55 |
+
self.vpn_network = ipaddress.IPv4Network("10.8.0.0/24")
|
| 56 |
+
self.vpn_server_ip = "10.8.0.1"
|
| 57 |
+
self.vpn_port = 1194
|
| 58 |
+
|
| 59 |
+
# Integration with ISP stack
|
| 60 |
+
self.dhcp_server = None
|
| 61 |
+
self.nat_engine = None
|
| 62 |
+
self.firewall = None
|
| 63 |
+
self.router = None
|
| 64 |
+
self.traffic_router = None # New traffic router component
|
| 65 |
+
|
| 66 |
+
# Status monitoring thread
|
| 67 |
+
self.monitor_thread = None
|
| 68 |
+
self.monitor_running = False
|
| 69 |
+
|
| 70 |
+
# Client configuration storage
|
| 71 |
+
self.config_storage_path = "/tmp/vpn_client_configs"
|
| 72 |
+
os.makedirs(self.config_storage_path, exist_ok=True)
|
| 73 |
+
|
| 74 |
+
def set_isp_components(self, dhcp_server=None, nat_engine=None, firewall=None, router=None, traffic_router=None):
|
| 75 |
+
"""Set references to ISP stack components for integration"""
|
| 76 |
+
self.dhcp_server = dhcp_server
|
| 77 |
+
self.nat_engine = nat_engine
|
| 78 |
+
self.firewall = firewall
|
| 79 |
+
self.router = router
|
| 80 |
+
self.traffic_router = traffic_router
|
| 81 |
+
|
| 82 |
+
# Configure traffic router with other components
|
| 83 |
+
if self.traffic_router:
|
| 84 |
+
self.traffic_router.set_components(
|
| 85 |
+
nat_engine=nat_engine,
|
| 86 |
+
firewall=firewall,
|
| 87 |
+
dhcp_server=dhcp_server
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def start_server(self) -> bool:
|
| 91 |
+
"""Start the OpenVPN server with traffic routing"""
|
| 92 |
+
try:
|
| 93 |
+
if self.is_running:
|
| 94 |
+
logger.warning("OpenVPN server is already running")
|
| 95 |
+
return True
|
| 96 |
+
|
| 97 |
+
# Ensure configuration exists
|
| 98 |
+
if not os.path.exists(self.server_config_path):
|
| 99 |
+
logger.error(f"OpenVPN server configuration not found: {self.server_config_path}")
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
# Start traffic router first
|
| 103 |
+
if self.traffic_router and not self.traffic_router.is_running:
|
| 104 |
+
if not self.traffic_router.start():
|
| 105 |
+
logger.error("Failed to start traffic router")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# Start OpenVPN server
|
| 110 |
+
self.server_process = subprocess.Popen(['sudo', 'openvpn', '--config', self.server_config_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
| 111 |
+
self.is_running = True
|
| 112 |
+
self.start_time = time.time()
|
| 113 |
+
logger.info("OpenVPN server started successfully")
|
| 114 |
+
|
| 115 |
+
# Start monitoring thread
|
| 116 |
+
self.start_monitoring()
|
| 117 |
+
|
| 118 |
+
# Configure firewall rules for VPN
|
| 119 |
+
self._configure_vpn_firewall()
|
| 120 |
+
|
| 121 |
+
# Configure NAT for VPN traffic
|
| 122 |
+
self._configure_vpn_nat()
|
| 123 |
+
|
| 124 |
+
return True
|
| 125 |
+
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.error(f"Error starting OpenVPN server: {e}")
|
| 128 |
+
return False
|
| 129 |
+
|
| 130 |
+
def stop_server(self) -> bool:
|
| 131 |
+
"""Stop the OpenVPN server and traffic routing"""
|
| 132 |
+
try:
|
| 133 |
+
if not self.is_running:
|
| 134 |
+
logger.warning("OpenVPN server is not running")
|
| 135 |
+
return True
|
| 136 |
+
|
| 137 |
+
# Stop monitoring
|
| 138 |
+
self.stop_monitoring()
|
| 139 |
+
|
| 140 |
+
# Remove all client routes before stopping
|
| 141 |
+
if self.traffic_router:
|
| 142 |
+
for client_id in list(self.clients.keys()):
|
| 143 |
+
self.traffic_router.remove_client_route(client_id)
|
| 144 |
+
|
| 145 |
+
# Stop OpenVPN server
|
| 146 |
+
if self.server_process:
|
| 147 |
+
self.server_process.terminate()
|
| 148 |
+
self.server_process.wait(timeout=5)
|
| 149 |
+
if self.server_process.poll() is None:
|
| 150 |
+
self.server_process.kill()
|
| 151 |
+
self.server_process = None
|
| 152 |
+
self.is_running = False
|
| 153 |
+
self.start_time = None
|
| 154 |
+
self.clients.clear()
|
| 155 |
+
logger.info("OpenVPN server stopped successfully")
|
| 156 |
+
return True
|
| 157 |
+
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"Error stopping OpenVPN server: {e}")
|
| 160 |
+
return False
|
| 161 |
+
|
| 162 |
+
def start_monitoring(self):
|
| 163 |
+
"""Start the client monitoring thread"""
|
| 164 |
+
if self.monitor_thread and self.monitor_thread.is_alive():
|
| 165 |
+
return
|
| 166 |
+
|
| 167 |
+
self.monitor_running = True
|
| 168 |
+
self.monitor_thread = threading.Thread(target=self._monitor_clients, daemon=True)
|
| 169 |
+
self.monitor_thread.start()
|
| 170 |
+
logger.info("Started OpenVPN client monitoring")
|
| 171 |
+
|
| 172 |
+
def stop_monitoring(self):
|
| 173 |
+
"""Stop the client monitoring thread"""
|
| 174 |
+
self.monitor_running = False
|
| 175 |
+
if self.monitor_thread:
|
| 176 |
+
self.monitor_thread.join(timeout=5)
|
| 177 |
+
logger.info("Stopped OpenVPN client monitoring")
|
| 178 |
+
|
| 179 |
+
def _monitor_clients(self):
|
| 180 |
+
"""Monitor connected VPN clients"""
|
| 181 |
+
while self.monitor_running:
|
| 182 |
+
try:
|
| 183 |
+
self._update_client_status()
|
| 184 |
+
time.sleep(10) # Update every 10 seconds
|
| 185 |
+
except Exception as e:
|
| 186 |
+
logger.error(f"Error monitoring VPN clients: {e}")
|
| 187 |
+
time.sleep(30) # Wait longer on error
|
| 188 |
+
|
| 189 |
+
def _update_client_status(self):
|
| 190 |
+
"""Update client status from OpenVPN status log and manage traffic routing"""
|
| 191 |
+
try:
|
| 192 |
+
with open(self.status_log_path, 'r') as f:
|
| 193 |
+
lines = f.readlines()
|
| 194 |
+
|
| 195 |
+
new_clients = {}
|
| 196 |
+
client_section = False
|
| 197 |
+
for line in lines:
|
| 198 |
+
if line.startswith('ROUTING TABLE'):
|
| 199 |
+
client_section = False
|
| 200 |
+
if client_section and not line.startswith('GLOBAL STATS'):
|
| 201 |
+
parts = line.strip().split(',')
|
| 202 |
+
if len(parts) >= 5:
|
| 203 |
+
common_name = parts[0]
|
| 204 |
+
real_ip_port = parts[1]
|
| 205 |
+
virtual_ip = parts[2]
|
| 206 |
+
bytes_received = int(parts[3])
|
| 207 |
+
bytes_sent = int(parts[4])
|
| 208 |
+
connected_since = float(parts[5]) # Assuming this is a timestamp
|
| 209 |
+
|
| 210 |
+
# Extract IP address from real_ip_port (e.g., 1.2.3.4:12345)
|
| 211 |
+
ip_address = real_ip_port.split(':')[0]
|
| 212 |
+
|
| 213 |
+
client = VPNClient(
|
| 214 |
+
client_id=common_name,
|
| 215 |
+
common_name=common_name,
|
| 216 |
+
ip_address=virtual_ip,
|
| 217 |
+
connected_at=connected_since,
|
| 218 |
+
bytes_received=bytes_received,
|
| 219 |
+
bytes_sent=bytes_sent,
|
| 220 |
+
status="connected",
|
| 221 |
+
routed_through_vpn=True
|
| 222 |
+
)
|
| 223 |
+
new_clients[common_name] = client
|
| 224 |
+
if line.startswith('COMMON NAME'):
|
| 225 |
+
client_section = True
|
| 226 |
+
self.clients = new_clients
|
| 227 |
+
|
| 228 |
+
except Exception as e:
|
| 229 |
+
logger.error(f"Error updating client status: {e}")
|
| 230 |
+
|
| 231 |
+
def _sync_with_dhcp(self):
|
| 232 |
+
"""Sync VPN clients with DHCP server"""
|
| 233 |
+
try:
|
| 234 |
+
for client in self.clients.values():
|
| 235 |
+
if client.ip_address != "unknown":
|
| 236 |
+
# Register VPN client IP with DHCP server
|
| 237 |
+
# This allows the ISP stack to track VPN clients
|
| 238 |
+
if hasattr(self.dhcp_server, 'register_static_lease'):
|
| 239 |
+
self.dhcp_server.register_static_lease(
|
| 240 |
+
client.common_name,
|
| 241 |
+
client.ip_address,
|
| 242 |
+
"VPN Client"
|
| 243 |
+
)
|
| 244 |
+
except Exception as e:
|
| 245 |
+
logger.error(f"Error syncing with DHCP: {e}")
|
| 246 |
+
|
| 247 |
+
def _configure_vpn_firewall(self):
|
| 248 |
+
"""Configure firewall rules for VPN traffic"""
|
| 249 |
+
try:
|
| 250 |
+
if not self.firewall:
|
| 251 |
+
return
|
| 252 |
+
|
| 253 |
+
# Add firewall rules for VPN
|
| 254 |
+
vpn_rules = [
|
| 255 |
+
{
|
| 256 |
+
"rule_id": "allow_openvpn",
|
| 257 |
+
"priority": 10,
|
| 258 |
+
"action": "ACCEPT",
|
| 259 |
+
"direction": "BOTH",
|
| 260 |
+
"dest_port": str(self.vpn_port),
|
| 261 |
+
"protocol": "UDP",
|
| 262 |
+
"description": "Allow OpenVPN traffic",
|
| 263 |
+
"enabled": True
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"rule_id": "allow_vpn_network",
|
| 267 |
+
"priority": 11,
|
| 268 |
+
"action": "ACCEPT",
|
| 269 |
+
"direction": "BOTH",
|
| 270 |
+
"source_network": str(self.vpn_network),
|
| 271 |
+
"description": "Allow VPN client network traffic",
|
| 272 |
+
"enabled": True
|
| 273 |
+
}
|
| 274 |
+
]
|
| 275 |
+
|
| 276 |
+
for rule in vpn_rules:
|
| 277 |
+
if hasattr(self.firewall, 'add_rule'):
|
| 278 |
+
self.firewall.add_rule(rule)
|
| 279 |
+
|
| 280 |
+
logger.info("Configured firewall rules for VPN")
|
| 281 |
+
|
| 282 |
+
except Exception as e:
|
| 283 |
+
logger.error(f"Error configuring VPN firewall: {e}")
|
| 284 |
+
|
| 285 |
+
def _configure_vpn_nat(self):
|
| 286 |
+
"""Configure NAT for VPN traffic"""
|
| 287 |
+
try:
|
| 288 |
+
# NAT configuration will be handled by the external environment (e.g., HuggingFace Spaces setup)
|
| 289 |
+
# or by the underlying network infrastructure. We are removing direct iptables calls.
|
| 290 |
+
logger.info("Skipping direct iptables NAT configuration as per instructions.")
|
| 291 |
+
|
| 292 |
+
except Exception as e:
|
| 293 |
+
logger.error(f"Error configuring VPN NAT: {e}")
|
| 294 |
+
|
| 295 |
+
def get_server_status(self) -> VPNServerStatus:
|
| 296 |
+
"""Get current server status"""
|
| 297 |
+
total_bytes_received = sum(client.bytes_received for client in self.clients.values())
|
| 298 |
+
total_bytes_sent = sum(client.bytes_sent for client in self.clients.values())
|
| 299 |
+
uptime = time.time() - self.start_time if self.start_time else 0
|
| 300 |
+
|
| 301 |
+
return VPNServerStatus(
|
| 302 |
+
is_running=self.is_running,
|
| 303 |
+
connected_clients=len(self.clients),
|
| 304 |
+
total_bytes_received=total_bytes_received,
|
| 305 |
+
total_bytes_sent=total_bytes_sent,
|
| 306 |
+
uptime=uptime,
|
| 307 |
+
server_ip=self.vpn_server_ip,
|
| 308 |
+
server_port=self.vpn_port
|
| 309 |
+
)
|
| 310 |
+
def get_connected_clients(self) -> List[Dict[str, Any]]:
|
| 311 |
+
"""Get list of connected clients"""
|
| 312 |
+
return [asdict(client) for client in self.clients.values()]
|
| 313 |
+
|
| 314 |
+
def disconnect_client(self, client_id: str) -> bool:
|
| 315 |
+
"""Disconnect a specific client"""
|
| 316 |
+
try:
|
| 317 |
+
if client_id not in self.clients:
|
| 318 |
+
return False
|
| 319 |
+
|
| 320 |
+
# Send kill signal to specific client
|
| 321 |
+
# This requires OpenVPN management interface, simplified for now
|
| 322 |
+
logger.info(f"Disconnecting client: {client_id}")
|
| 323 |
+
|
| 324 |
+
# Remove from clients dict
|
| 325 |
+
del self.clients[client_id]
|
| 326 |
+
return True
|
| 327 |
+
|
| 328 |
+
except Exception as e:
|
| 329 |
+
logger.error(f"Error disconnecting client {client_id}: {e}")
|
| 330 |
+
return False
|
| 331 |
+
|
| 332 |
+
def generate_client_config(self, client_name: str, server_ip: str) -> str:
|
| 333 |
+
"""Generate client configuration file with embedded certificates"""
|
| 334 |
+
try:
|
| 335 |
+
# Read real CA certificate
|
| 336 |
+
ca_cert_path = "/etc/openvpn/server/ca.crt"
|
| 337 |
+
with open(ca_cert_path, 'r') as f:
|
| 338 |
+
ca_cert = f.read()
|
| 339 |
+
|
| 340 |
+
client_cert_path = f"/home/ubuntu/easy-rsa/pki/issued/{client_name}.crt"
|
| 341 |
+
with open(client_cert_path, 'r') as f:
|
| 342 |
+
client_cert = f.read()
|
| 343 |
+
|
| 344 |
+
client_key_path = f"/home/ubuntu/easy-rsa/pki/private/{client_name}.key"
|
| 345 |
+
with open(client_key_path, 'r') as f:
|
| 346 |
+
client_key = f.read()
|
| 347 |
+
|
| 348 |
+
# Generate complete client configuration
|
| 349 |
+
client_config = f"""# OpenVPN Client Configuration for {client_name}
|
| 350 |
+
# Generated by Virtual ISP Stack
|
| 351 |
+
# Server: {server_ip}:{self.vpn_port}
|
| 352 |
+
|
| 353 |
+
client
|
| 354 |
+
dev tun
|
| 355 |
+
proto udp
|
| 356 |
+
remote {server_ip} {self.vpn_port}
|
| 357 |
+
resolv-retry infinite
|
| 358 |
+
nobind
|
| 359 |
+
persist-key
|
| 360 |
+
persist-tun
|
| 361 |
+
cipher AES-256-CBC
|
| 362 |
+
auth SHA256
|
| 363 |
+
verb 3
|
| 364 |
+
key-direction 1
|
| 365 |
+
redirect-gateway def1 bypass-dhcp
|
| 366 |
+
dhcp-option DNS 8.8.8.8
|
| 367 |
+
dhcp-option DNS 8.8.4.4
|
| 368 |
+
remote-cert-tls server
|
| 369 |
+
|
| 370 |
+
# Embedded CA Certificate
|
| 371 |
+
<ca>
|
| 372 |
+
{ca_cert}
|
| 373 |
+
</ca>
|
| 374 |
+
|
| 375 |
+
# Embedded Client Certificate
|
| 376 |
+
<cert>
|
| 377 |
+
{client_cert}
|
| 378 |
+
</cert>
|
| 379 |
+
|
| 380 |
+
# Embedded Client Private Key
|
| 381 |
+
<key>
|
| 382 |
+
{client_key}
|
| 383 |
+
</key>
|
| 384 |
+
|
| 385 |
+
# TLS Authentication Key (optional, for extra security)
|
| 386 |
+
# <tls-auth>
|
| 387 |
+
# -----BEGIN OpenVPN Static key V1-----
|
| 388 |
+
# [TLS-AUTH-KEY-CONTENT-WOULD-GO-HERE]
|
| 389 |
+
# -----END OpenVPN Static key V1-----
|
| 390 |
+
# </tls-auth>
|
| 391 |
+
"""
|
| 392 |
+
|
| 393 |
+
logger.info(f"Generated client configuration for {client_name}")
|
| 394 |
+
return client_config
|
| 395 |
+
|
| 396 |
+
except Exception as e:
|
| 397 |
+
logger.error(f"Error generating client config: {e}")
|
| 398 |
+
return ""
|
| 399 |
+
|
| 400 |
+
def save_client_config(self, client_name: str, config_content: str) -> bool:
|
| 401 |
+
"""Save client configuration to storage"""
|
| 402 |
+
try:
|
| 403 |
+
config_file_path = os.path.join(self.config_storage_path, f"{client_name}.ovpn")
|
| 404 |
+
with open(config_file_path, 'w') as f:
|
| 405 |
+
f.write(config_content)
|
| 406 |
+
|
| 407 |
+
logger.info(f"Saved client configuration for {client_name}")
|
| 408 |
+
return True
|
| 409 |
+
|
| 410 |
+
except Exception as e:
|
| 411 |
+
logger.error(f"Error saving client config for {client_name}: {e}")
|
| 412 |
+
return False
|
| 413 |
+
|
| 414 |
+
def load_client_config(self, client_name: str) -> str:
|
| 415 |
+
"""Load client configuration from storage"""
|
| 416 |
+
try:
|
| 417 |
+
config_file_path = os.path.join(self.config_storage_path, f"{client_name}.ovpn")
|
| 418 |
+
if not os.path.exists(config_file_path):
|
| 419 |
+
return ""
|
| 420 |
+
|
| 421 |
+
with open(config_file_path, 'r') as f:
|
| 422 |
+
config_content = f.read()
|
| 423 |
+
|
| 424 |
+
logger.info(f"Loaded client configuration for {client_name}")
|
| 425 |
+
return config_content
|
| 426 |
+
|
| 427 |
+
except Exception as e:
|
| 428 |
+
logger.error(f"Error loading client config for {client_name}: {e}")
|
| 429 |
+
return ""
|
| 430 |
+
|
| 431 |
+
def list_client_configs(self) -> List[str]:
|
| 432 |
+
"""List all stored client configurations"""
|
| 433 |
+
try:
|
| 434 |
+
config_files = []
|
| 435 |
+
if os.path.exists(self.config_storage_path):
|
| 436 |
+
for filename in os.listdir(self.config_storage_path):
|
| 437 |
+
if filename.endswith('.ovpn'):
|
| 438 |
+
client_name = filename[:-5] # Remove .ovpn extension
|
| 439 |
+
config_files.append(client_name)
|
| 440 |
+
|
| 441 |
+
return config_files
|
| 442 |
+
|
| 443 |
+
except Exception as e:
|
| 444 |
+
logger.error(f"Error listing client configs: {e}")
|
| 445 |
+
return []
|
| 446 |
+
|
| 447 |
+
def delete_client_config(self, client_name: str) -> bool:
|
| 448 |
+
"""Delete client configuration from storage"""
|
| 449 |
+
try:
|
| 450 |
+
config_file_path = os.path.join(self.config_storage_path, f"{client_name}.ovpn")
|
| 451 |
+
if os.path.exists(config_file_path):
|
| 452 |
+
os.remove(config_file_path)
|
| 453 |
+
logger.info(f"Deleted client configuration for {client_name}")
|
| 454 |
+
return True
|
| 455 |
+
else:
|
| 456 |
+
logger.warning(f"Client configuration for {client_name} not found")
|
| 457 |
+
return False
|
| 458 |
+
|
| 459 |
+
except Exception as e:
|
| 460 |
+
logger.error(f"Error deleting client config for {client_name}: {e}")
|
| 461 |
+
return False
|
| 462 |
+
|
| 463 |
+
def generate_and_save_client_config(self, client_name: str, server_ip: str) -> str:
|
| 464 |
+
"""Generate client configuration and save it to storage"""
|
| 465 |
+
try:
|
| 466 |
+
config_content = self.generate_client_config(client_name, server_ip)
|
| 467 |
+
if config_content:
|
| 468 |
+
if self.save_client_config(client_name, config_content):
|
| 469 |
+
return config_content
|
| 470 |
+
return ""
|
| 471 |
+
|
| 472 |
+
except Exception as e:
|
| 473 |
+
logger.error(f"Error generating and saving client config for {client_name}: {e}")
|
| 474 |
+
return ""
|
| 475 |
+
|
| 476 |
+
def get_statistics(self) -> Dict[str, Any]:
|
| 477 |
+
"""Get comprehensive VPN statistics"""
|
| 478 |
+
status = self.get_server_status()
|
| 479 |
+
|
| 480 |
+
return {
|
| 481 |
+
"server_status": asdict(status),
|
| 482 |
+
"connected_clients": self.get_connected_clients(),
|
| 483 |
+
"network_config": {
|
| 484 |
+
"vpn_network": str(self.vpn_network),
|
| 485 |
+
"server_ip": self.vpn_server_ip,
|
| 486 |
+
"server_port": self.vpn_port
|
| 487 |
+
},
|
| 488 |
+
"integration_status": {
|
| 489 |
+
"dhcp_integrated": self.dhcp_server is not None,
|
| 490 |
+
"nat_integrated": self.nat_engine is not None,
|
| 491 |
+
"firewall_integrated": self.firewall is not None,
|
| 492 |
+
"router_integrated": self.router is not None
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
# Global OpenVPN manager instance
|
| 497 |
+
openvpn_manager = None
|
| 498 |
+
|
| 499 |
+
def initialize_openvpn_manager(config: Dict[str, Any]) -> OpenVPNManager:
|
| 500 |
+
"""Initialize the OpenVPN manager"""
|
| 501 |
+
global openvpn_manager
|
| 502 |
+
openvpn_manager = OpenVPNManager(config)
|
| 503 |
+
return openvpn_manager
|
| 504 |
+
|
| 505 |
+
def get_openvpn_manager() -> Optional[OpenVPNManager]:
|
| 506 |
+
"""Get the global OpenVPN manager instance"""
|
| 507 |
+
return openvpn_manager
|
| 508 |
+
|
core/packet_bridge.py
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Packet Bridge Module
|
| 3 |
+
|
| 4 |
+
Handles communication with virtual clients:
|
| 5 |
+
- Accept packet streams over WebSocket/TCP
|
| 6 |
+
- Deliver response packets back to clients
|
| 7 |
+
- Frame processing (Ethernet → IPv4)
|
| 8 |
+
- Connection management
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import asyncio
|
| 12 |
+
import websockets
|
| 13 |
+
import socket
|
| 14 |
+
import threading
|
| 15 |
+
import time
|
| 16 |
+
import struct
|
| 17 |
+
from typing import Dict, List, Optional, Callable, Set, Any, Tuple
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from enum import Enum
|
| 20 |
+
import json
|
| 21 |
+
import logging
|
| 22 |
+
|
| 23 |
+
from .ip_parser import IPParser, ParsedPacket
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class BridgeType(Enum):
|
| 27 |
+
WEBSOCKET = "WEBSOCKET"
|
| 28 |
+
TCP_SOCKET = "TCP_SOCKET"
|
| 29 |
+
UDP_SOCKET = "UDP_SOCKET"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class ClientConnection:
|
| 34 |
+
"""Represents a client connection to the bridge"""
|
| 35 |
+
client_id: str
|
| 36 |
+
bridge_type: BridgeType
|
| 37 |
+
remote_address: str
|
| 38 |
+
remote_port: int
|
| 39 |
+
websocket: Optional[Any] = None # WebSocket connection
|
| 40 |
+
socket: Optional['socket.socket'] = None # TCP/UDP socket
|
| 41 |
+
connected_time: float = 0
|
| 42 |
+
last_activity: float = 0
|
| 43 |
+
packets_received: int = 0
|
| 44 |
+
packets_sent: int = 0
|
| 45 |
+
bytes_received: int = 0
|
| 46 |
+
bytes_sent: int = 0
|
| 47 |
+
is_active: bool = True
|
| 48 |
+
|
| 49 |
+
def __post_init__(self):
|
| 50 |
+
if self.connected_time == 0:
|
| 51 |
+
self.connected_time = time.time()
|
| 52 |
+
if self.last_activity == 0:
|
| 53 |
+
self.last_activity = time.time()
|
| 54 |
+
|
| 55 |
+
def update_activity(self, packet_count: int = 1, byte_count: int = 0, direction: str = 'received'):
|
| 56 |
+
"""Update connection activity"""
|
| 57 |
+
self.last_activity = time.time()
|
| 58 |
+
|
| 59 |
+
if direction == 'received':
|
| 60 |
+
self.packets_received += packet_count
|
| 61 |
+
self.bytes_received += byte_count
|
| 62 |
+
else:
|
| 63 |
+
self.packets_sent += packet_count
|
| 64 |
+
self.bytes_sent += byte_count
|
| 65 |
+
|
| 66 |
+
def to_dict(self) -> Dict:
|
| 67 |
+
"""Convert to dictionary"""
|
| 68 |
+
return {
|
| 69 |
+
'client_id': self.client_id,
|
| 70 |
+
'bridge_type': self.bridge_type.value,
|
| 71 |
+
'remote_address': self.remote_address,
|
| 72 |
+
'remote_port': self.remote_port,
|
| 73 |
+
'connected_time': self.connected_time,
|
| 74 |
+
'last_activity': self.last_activity,
|
| 75 |
+
'packets_received': self.packets_received,
|
| 76 |
+
'packets_sent': self.packets_sent,
|
| 77 |
+
'bytes_received': self.bytes_received,
|
| 78 |
+
'bytes_sent': self.bytes_sent,
|
| 79 |
+
'is_active': self.is_active,
|
| 80 |
+
'duration': time.time() - self.connected_time
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class EthernetFrame:
|
| 85 |
+
"""Ethernet frame parser"""
|
| 86 |
+
|
| 87 |
+
def __init__(self):
|
| 88 |
+
self.dest_mac = b'\x00' * 6
|
| 89 |
+
self.src_mac = b'\x00' * 6
|
| 90 |
+
self.ethertype = 0x0800 # IPv4
|
| 91 |
+
self.payload = b''
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def parse(cls, data: bytes) -> Optional['EthernetFrame']:
|
| 95 |
+
"""Parse Ethernet frame from raw bytes"""
|
| 96 |
+
if len(data) < 14: # Minimum Ethernet header size
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
frame = cls()
|
| 100 |
+
frame.dest_mac = data[0:6]
|
| 101 |
+
frame.src_mac = data[6:12]
|
| 102 |
+
frame.ethertype = struct.unpack('!H', data[12:14])[0]
|
| 103 |
+
frame.payload = data[14:]
|
| 104 |
+
|
| 105 |
+
return frame
|
| 106 |
+
|
| 107 |
+
def build(self) -> bytes:
|
| 108 |
+
"""Build Ethernet frame as bytes"""
|
| 109 |
+
header = self.dest_mac + self.src_mac + struct.pack('!H', self.ethertype)
|
| 110 |
+
return header + self.payload
|
| 111 |
+
|
| 112 |
+
def is_ipv4(self) -> bool:
|
| 113 |
+
"""Check if frame contains IPv4 packet"""
|
| 114 |
+
return self.ethertype == 0x0800
|
| 115 |
+
|
| 116 |
+
def is_arp(self) -> bool:
|
| 117 |
+
"""Check if frame contains ARP packet"""
|
| 118 |
+
return self.ethertype == 0x0806
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class PacketBridge:
|
| 122 |
+
"""Packet bridge implementation"""
|
| 123 |
+
|
| 124 |
+
def __init__(self, config: Dict):
|
| 125 |
+
self.config = config
|
| 126 |
+
self.clients: Dict[str, ClientConnection] = {}
|
| 127 |
+
self.packet_handlers: List[Callable[[ParsedPacket, str], Optional[bytes]]] = []
|
| 128 |
+
self.lock = threading.Lock()
|
| 129 |
+
|
| 130 |
+
# Configuration
|
| 131 |
+
self.websocket_host = config.get('websocket_host', '0.0.0.0')
|
| 132 |
+
self.websocket_port = config.get('websocket_port', 8765)
|
| 133 |
+
self.tcp_host = config.get('tcp_host', '0.0.0.0')
|
| 134 |
+
self.tcp_port = config.get('tcp_port', 8766)
|
| 135 |
+
self.max_clients = config.get('max_clients', 100)
|
| 136 |
+
self.client_timeout = config.get('client_timeout', 300)
|
| 137 |
+
|
| 138 |
+
# WebSocket server
|
| 139 |
+
self.websocket_server = None
|
| 140 |
+
self.tcp_server_socket = None
|
| 141 |
+
|
| 142 |
+
# Background tasks
|
| 143 |
+
self.running = False
|
| 144 |
+
self.websocket_task = None
|
| 145 |
+
self.tcp_task = None
|
| 146 |
+
self.cleanup_task = None
|
| 147 |
+
|
| 148 |
+
# Statistics
|
| 149 |
+
self.stats = {
|
| 150 |
+
'total_clients': 0,
|
| 151 |
+
'active_clients': 0,
|
| 152 |
+
'packets_processed': 0,
|
| 153 |
+
'packets_forwarded': 0,
|
| 154 |
+
'packets_dropped': 0,
|
| 155 |
+
'bytes_processed': 0,
|
| 156 |
+
'websocket_connections': 0,
|
| 157 |
+
'tcp_connections': 0,
|
| 158 |
+
'connection_errors': 0
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
# Event loop
|
| 162 |
+
self.loop = None
|
| 163 |
+
|
| 164 |
+
def add_packet_handler(self, handler: Callable[[ParsedPacket, str], Optional[bytes]]):
|
| 165 |
+
"""Add packet handler function"""
|
| 166 |
+
self.packet_handlers.append(handler)
|
| 167 |
+
|
| 168 |
+
def remove_packet_handler(self, handler: Callable[[ParsedPacket, str], Optional[bytes]]):
|
| 169 |
+
"""Remove packet handler function"""
|
| 170 |
+
if handler in self.packet_handlers:
|
| 171 |
+
self.packet_handlers.remove(handler)
|
| 172 |
+
|
| 173 |
+
def _generate_client_id(self, remote_address: str, remote_port: int) -> str:
|
| 174 |
+
"""Generate unique client ID"""
|
| 175 |
+
timestamp = int(time.time() * 1000)
|
| 176 |
+
return f"client_{remote_address}_{remote_port}_{timestamp}"
|
| 177 |
+
|
| 178 |
+
def _process_ethernet_frame(self, frame_data: bytes, client_id: str) -> Optional[bytes]:
|
| 179 |
+
"""Process Ethernet frame and extract IP packet"""
|
| 180 |
+
try:
|
| 181 |
+
# Parse Ethernet frame
|
| 182 |
+
frame = EthernetFrame.parse(frame_data)
|
| 183 |
+
if not frame or not frame.is_ipv4():
|
| 184 |
+
return None
|
| 185 |
+
|
| 186 |
+
# Parse IP packet
|
| 187 |
+
packet = IPParser.parse_packet(frame.payload)
|
| 188 |
+
self.stats['packets_processed'] += 1
|
| 189 |
+
self.stats['bytes_processed'] += len(frame_data)
|
| 190 |
+
|
| 191 |
+
# Process through packet handlers
|
| 192 |
+
response_packet = None
|
| 193 |
+
for handler in self.packet_handlers:
|
| 194 |
+
try:
|
| 195 |
+
response = handler(packet, client_id)
|
| 196 |
+
if response:
|
| 197 |
+
response_packet = response
|
| 198 |
+
break
|
| 199 |
+
except Exception as e:
|
| 200 |
+
logging.error(f"Packet handler error: {e}")
|
| 201 |
+
|
| 202 |
+
if response_packet:
|
| 203 |
+
# Wrap response in Ethernet frame
|
| 204 |
+
response_frame = EthernetFrame()
|
| 205 |
+
response_frame.dest_mac = frame.src_mac
|
| 206 |
+
response_frame.src_mac = frame.dest_mac
|
| 207 |
+
response_frame.ethertype = 0x0800
|
| 208 |
+
response_frame.payload = response_packet
|
| 209 |
+
|
| 210 |
+
self.stats['packets_forwarded'] += 1
|
| 211 |
+
return response_frame.build()
|
| 212 |
+
else:
|
| 213 |
+
self.stats['packets_dropped'] += 1
|
| 214 |
+
return None
|
| 215 |
+
|
| 216 |
+
except Exception as e:
|
| 217 |
+
logging.error(f"Error processing Ethernet frame: {e}")
|
| 218 |
+
self.stats['packets_dropped'] += 1
|
| 219 |
+
return None
|
| 220 |
+
|
| 221 |
+
async def _handle_websocket_client(self, websocket, path):
|
| 222 |
+
"""Handle WebSocket client connection"""
|
| 223 |
+
client_address = websocket.remote_address
|
| 224 |
+
client_id = self._generate_client_id(client_address[0], client_address[1])
|
| 225 |
+
|
| 226 |
+
# Create client connection
|
| 227 |
+
client = ClientConnection(
|
| 228 |
+
client_id=client_id,
|
| 229 |
+
bridge_type=BridgeType.WEBSOCKET,
|
| 230 |
+
remote_address=client_address[0],
|
| 231 |
+
remote_port=client_address[1],
|
| 232 |
+
websocket=websocket
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
with self.lock:
|
| 236 |
+
if len(self.clients) >= self.max_clients:
|
| 237 |
+
await websocket.close(code=1013, reason="Too many clients")
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
self.clients[client_id] = client
|
| 241 |
+
|
| 242 |
+
self.stats['total_clients'] += 1
|
| 243 |
+
self.stats['active_clients'] = len(self.clients)
|
| 244 |
+
self.stats['websocket_connections'] += 1
|
| 245 |
+
|
| 246 |
+
logging.info(f"WebSocket client connected: {client_id} from {client_address}")
|
| 247 |
+
|
| 248 |
+
try:
|
| 249 |
+
async for message in websocket:
|
| 250 |
+
if isinstance(message, bytes):
|
| 251 |
+
# Binary message - treat as Ethernet frame
|
| 252 |
+
client.update_activity(1, len(message), 'received')
|
| 253 |
+
|
| 254 |
+
response = self._process_ethernet_frame(message, client_id)
|
| 255 |
+
if response:
|
| 256 |
+
await websocket.send(response)
|
| 257 |
+
client.update_activity(1, len(response), 'sent')
|
| 258 |
+
|
| 259 |
+
elif isinstance(message, str):
|
| 260 |
+
# Text message - treat as control message
|
| 261 |
+
try:
|
| 262 |
+
control_msg = json.loads(message)
|
| 263 |
+
await self._handle_control_message(client, control_msg)
|
| 264 |
+
except json.JSONDecodeError:
|
| 265 |
+
logging.warning(f"Invalid control message from {client_id}: {message}")
|
| 266 |
+
|
| 267 |
+
except websockets.exceptions.ConnectionClosed:
|
| 268 |
+
logging.info(f"WebSocket client disconnected: {client_id}")
|
| 269 |
+
except Exception as e:
|
| 270 |
+
logging.error(f"WebSocket client error: {e}")
|
| 271 |
+
self.stats['connection_errors'] += 1
|
| 272 |
+
|
| 273 |
+
finally:
|
| 274 |
+
# Clean up client
|
| 275 |
+
with self.lock:
|
| 276 |
+
if client_id in self.clients:
|
| 277 |
+
self.clients[client_id].is_active = False
|
| 278 |
+
del self.clients[client_id]
|
| 279 |
+
|
| 280 |
+
self.stats['active_clients'] = len(self.clients)
|
| 281 |
+
|
| 282 |
+
async def _handle_control_message(self, client: ClientConnection, message: Dict):
|
| 283 |
+
"""Handle control message from client"""
|
| 284 |
+
msg_type = message.get('type')
|
| 285 |
+
|
| 286 |
+
if msg_type == 'ping':
|
| 287 |
+
# Respond to ping
|
| 288 |
+
response = {'type': 'pong', 'timestamp': time.time()}
|
| 289 |
+
await client.websocket.send(json.dumps(response))
|
| 290 |
+
|
| 291 |
+
elif msg_type == 'stats':
|
| 292 |
+
# Send client statistics
|
| 293 |
+
response = {
|
| 294 |
+
'type': 'stats',
|
| 295 |
+
'client_stats': client.to_dict(),
|
| 296 |
+
'bridge_stats': self.get_stats()
|
| 297 |
+
}
|
| 298 |
+
await client.websocket.send(json.dumps(response))
|
| 299 |
+
|
| 300 |
+
elif msg_type == 'config':
|
| 301 |
+
# Handle configuration updates
|
| 302 |
+
config_data = message.get('data', {})
|
| 303 |
+
# Process configuration updates here
|
| 304 |
+
response = {'type': 'config_ack', 'status': 'ok'}
|
| 305 |
+
await client.websocket.send(json.dumps(response))
|
| 306 |
+
|
| 307 |
+
def _handle_tcp_client(self, client_socket: socket.socket, client_address: Tuple[str, int]):
|
| 308 |
+
"""Handle TCP client connection"""
|
| 309 |
+
client_id = self._generate_client_id(client_address[0], client_address[1])
|
| 310 |
+
|
| 311 |
+
# Create client connection
|
| 312 |
+
client = ClientConnection(
|
| 313 |
+
client_id=client_id,
|
| 314 |
+
bridge_type=BridgeType.TCP_SOCKET,
|
| 315 |
+
remote_address=client_address[0],
|
| 316 |
+
remote_port=client_address[1],
|
| 317 |
+
socket=client_socket
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
with self.lock:
|
| 321 |
+
if len(self.clients) >= self.max_clients:
|
| 322 |
+
client_socket.close()
|
| 323 |
+
return
|
| 324 |
+
|
| 325 |
+
self.clients[client_id] = client
|
| 326 |
+
|
| 327 |
+
self.stats['total_clients'] += 1
|
| 328 |
+
self.stats['active_clients'] = len(self.clients)
|
| 329 |
+
self.stats['tcp_connections'] += 1
|
| 330 |
+
|
| 331 |
+
logging.info(f"TCP client connected: {client_id} from {client_address}")
|
| 332 |
+
|
| 333 |
+
try:
|
| 334 |
+
client_socket.settimeout(self.client_timeout)
|
| 335 |
+
|
| 336 |
+
while client.is_active:
|
| 337 |
+
try:
|
| 338 |
+
# Read frame length (4 bytes)
|
| 339 |
+
length_data = client_socket.recv(4)
|
| 340 |
+
if not length_data:
|
| 341 |
+
break
|
| 342 |
+
|
| 343 |
+
frame_length = struct.unpack('!I', length_data)[0]
|
| 344 |
+
if frame_length > 65536: # Sanity check
|
| 345 |
+
break
|
| 346 |
+
|
| 347 |
+
# Read frame data
|
| 348 |
+
frame_data = b''
|
| 349 |
+
while len(frame_data) < frame_length:
|
| 350 |
+
chunk = client_socket.recv(frame_length - len(frame_data))
|
| 351 |
+
if not chunk:
|
| 352 |
+
break
|
| 353 |
+
frame_data += chunk
|
| 354 |
+
|
| 355 |
+
if len(frame_data) != frame_length:
|
| 356 |
+
break
|
| 357 |
+
|
| 358 |
+
client.update_activity(1, len(frame_data), 'received')
|
| 359 |
+
|
| 360 |
+
# Process frame
|
| 361 |
+
response = self._process_ethernet_frame(frame_data, client_id)
|
| 362 |
+
if response:
|
| 363 |
+
# Send response with length prefix
|
| 364 |
+
response_length = struct.pack('!I', len(response))
|
| 365 |
+
client_socket.send(response_length + response)
|
| 366 |
+
client.update_activity(1, len(response), 'sent')
|
| 367 |
+
|
| 368 |
+
except socket.timeout:
|
| 369 |
+
continue
|
| 370 |
+
except Exception as e:
|
| 371 |
+
logging.error(f"TCP client error: {e}")
|
| 372 |
+
break
|
| 373 |
+
|
| 374 |
+
except Exception as e:
|
| 375 |
+
logging.error(f"TCP client handler error: {e}")
|
| 376 |
+
self.stats['connection_errors'] += 1
|
| 377 |
+
|
| 378 |
+
finally:
|
| 379 |
+
# Clean up client
|
| 380 |
+
try:
|
| 381 |
+
client_socket.close()
|
| 382 |
+
except:
|
| 383 |
+
pass
|
| 384 |
+
|
| 385 |
+
with self.lock:
|
| 386 |
+
if client_id in self.clients:
|
| 387 |
+
self.clients[client_id].is_active = False
|
| 388 |
+
del self.clients[client_id]
|
| 389 |
+
|
| 390 |
+
self.stats['active_clients'] = len(self.clients)
|
| 391 |
+
logging.info(f"TCP client disconnected: {client_id}")
|
| 392 |
+
|
| 393 |
+
def _tcp_server_loop(self):
|
| 394 |
+
"""TCP server loop"""
|
| 395 |
+
try:
|
| 396 |
+
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 397 |
+
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 398 |
+
self.tcp_server_socket.bind((self.tcp_host, self.tcp_port))
|
| 399 |
+
self.tcp_server_socket.listen(10)
|
| 400 |
+
|
| 401 |
+
logging.info(f"TCP bridge server listening on {self.tcp_host}:{self.tcp_port}")
|
| 402 |
+
|
| 403 |
+
while self.running:
|
| 404 |
+
try:
|
| 405 |
+
client_socket, client_address = self.tcp_server_socket.accept()
|
| 406 |
+
|
| 407 |
+
# Handle client in separate thread
|
| 408 |
+
client_thread = threading.Thread(
|
| 409 |
+
target=self._handle_tcp_client,
|
| 410 |
+
args=(client_socket, client_address),
|
| 411 |
+
daemon=True
|
| 412 |
+
)
|
| 413 |
+
client_thread.start()
|
| 414 |
+
|
| 415 |
+
except socket.error as e:
|
| 416 |
+
if self.running:
|
| 417 |
+
logging.error(f"TCP server error: {e}")
|
| 418 |
+
time.sleep(1)
|
| 419 |
+
|
| 420 |
+
except Exception as e:
|
| 421 |
+
logging.error(f"TCP server loop error: {e}")
|
| 422 |
+
|
| 423 |
+
finally:
|
| 424 |
+
if self.tcp_server_socket:
|
| 425 |
+
self.tcp_server_socket.close()
|
| 426 |
+
|
| 427 |
+
def _cleanup_loop(self):
|
| 428 |
+
"""Background cleanup loop"""
|
| 429 |
+
while self.running:
|
| 430 |
+
try:
|
| 431 |
+
current_time = time.time()
|
| 432 |
+
expired_clients = []
|
| 433 |
+
|
| 434 |
+
with self.lock:
|
| 435 |
+
for client_id, client in self.clients.items():
|
| 436 |
+
# Mark inactive clients for removal
|
| 437 |
+
if current_time - client.last_activity > self.client_timeout:
|
| 438 |
+
expired_clients.append(client_id)
|
| 439 |
+
|
| 440 |
+
# Clean up expired clients
|
| 441 |
+
for client_id in expired_clients:
|
| 442 |
+
with self.lock:
|
| 443 |
+
if client_id in self.clients:
|
| 444 |
+
client = self.clients[client_id]
|
| 445 |
+
client.is_active = False
|
| 446 |
+
|
| 447 |
+
# Close connections
|
| 448 |
+
if client.websocket:
|
| 449 |
+
try:
|
| 450 |
+
asyncio.run_coroutine_threadsafe(
|
| 451 |
+
client.websocket.close(),
|
| 452 |
+
self.loop
|
| 453 |
+
)
|
| 454 |
+
except:
|
| 455 |
+
pass
|
| 456 |
+
|
| 457 |
+
if client.socket:
|
| 458 |
+
try:
|
| 459 |
+
client.socket.close()
|
| 460 |
+
except:
|
| 461 |
+
pass
|
| 462 |
+
|
| 463 |
+
del self.clients[client_id]
|
| 464 |
+
logging.info(f"Cleaned up expired client: {client_id}")
|
| 465 |
+
|
| 466 |
+
self.stats['active_clients'] = len(self.clients)
|
| 467 |
+
|
| 468 |
+
time.sleep(30) # Cleanup every 30 seconds
|
| 469 |
+
|
| 470 |
+
except Exception as e:
|
| 471 |
+
logging.error(f"Cleanup loop error: {e}")
|
| 472 |
+
time.sleep(5)
|
| 473 |
+
|
| 474 |
+
def send_packet_to_client(self, client_id: str, packet_data: bytes) -> bool:
|
| 475 |
+
"""Send packet to specific client"""
|
| 476 |
+
with self.lock:
|
| 477 |
+
client = self.clients.get(client_id)
|
| 478 |
+
|
| 479 |
+
if not client or not client.is_active:
|
| 480 |
+
return False
|
| 481 |
+
|
| 482 |
+
try:
|
| 483 |
+
if client.bridge_type == BridgeType.WEBSOCKET:
|
| 484 |
+
# Send via WebSocket
|
| 485 |
+
if client.websocket:
|
| 486 |
+
asyncio.run_coroutine_threadsafe(
|
| 487 |
+
client.websocket.send(packet_data),
|
| 488 |
+
self.loop
|
| 489 |
+
)
|
| 490 |
+
client.update_activity(1, len(packet_data), 'sent')
|
| 491 |
+
return True
|
| 492 |
+
|
| 493 |
+
elif client.bridge_type == BridgeType.TCP_SOCKET:
|
| 494 |
+
# Send via TCP socket with length prefix
|
| 495 |
+
if client.socket:
|
| 496 |
+
length_prefix = struct.pack('!I', len(packet_data))
|
| 497 |
+
client.socket.send(length_prefix + packet_data)
|
| 498 |
+
client.update_activity(1, len(packet_data), 'sent')
|
| 499 |
+
return True
|
| 500 |
+
|
| 501 |
+
except Exception as e:
|
| 502 |
+
logging.error(f"Failed to send packet to client {client_id}: {e}")
|
| 503 |
+
# Mark client as inactive
|
| 504 |
+
client.is_active = False
|
| 505 |
+
|
| 506 |
+
return False
|
| 507 |
+
|
| 508 |
+
def broadcast_packet(self, packet_data: bytes, exclude_client: Optional[str] = None) -> int:
|
| 509 |
+
"""Broadcast packet to all clients"""
|
| 510 |
+
sent_count = 0
|
| 511 |
+
|
| 512 |
+
with self.lock:
|
| 513 |
+
client_ids = list(self.clients.keys())
|
| 514 |
+
|
| 515 |
+
for client_id in client_ids:
|
| 516 |
+
if client_id != exclude_client:
|
| 517 |
+
if self.send_packet_to_client(client_id, packet_data):
|
| 518 |
+
sent_count += 1
|
| 519 |
+
|
| 520 |
+
return sent_count
|
| 521 |
+
|
| 522 |
+
def get_clients(self) -> Dict[str, Dict]:
|
| 523 |
+
"""Get all connected clients"""
|
| 524 |
+
with self.lock:
|
| 525 |
+
return {
|
| 526 |
+
client_id: client.to_dict()
|
| 527 |
+
for client_id, client in self.clients.items()
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
def get_client(self, client_id: str) -> Optional[Dict]:
|
| 531 |
+
"""Get specific client"""
|
| 532 |
+
with self.lock:
|
| 533 |
+
client = self.clients.get(client_id)
|
| 534 |
+
return client.to_dict() if client else None
|
| 535 |
+
|
| 536 |
+
def disconnect_client(self, client_id: str) -> bool:
|
| 537 |
+
"""Disconnect specific client"""
|
| 538 |
+
with self.lock:
|
| 539 |
+
client = self.clients.get(client_id)
|
| 540 |
+
if not client:
|
| 541 |
+
return False
|
| 542 |
+
|
| 543 |
+
client.is_active = False
|
| 544 |
+
|
| 545 |
+
# Close connection
|
| 546 |
+
if client.websocket:
|
| 547 |
+
try:
|
| 548 |
+
asyncio.run_coroutine_threadsafe(
|
| 549 |
+
client.websocket.close(),
|
| 550 |
+
self.loop
|
| 551 |
+
)
|
| 552 |
+
except:
|
| 553 |
+
pass
|
| 554 |
+
|
| 555 |
+
if client.socket:
|
| 556 |
+
try:
|
| 557 |
+
client.socket.close()
|
| 558 |
+
except:
|
| 559 |
+
pass
|
| 560 |
+
|
| 561 |
+
del self.clients[client_id]
|
| 562 |
+
self.stats['active_clients'] = len(self.clients)
|
| 563 |
+
|
| 564 |
+
return True
|
| 565 |
+
|
| 566 |
+
def get_stats(self) -> Dict:
|
| 567 |
+
"""Get bridge statistics"""
|
| 568 |
+
with self.lock:
|
| 569 |
+
stats = self.stats.copy()
|
| 570 |
+
stats['active_clients'] = len(self.clients)
|
| 571 |
+
|
| 572 |
+
return stats
|
| 573 |
+
|
| 574 |
+
def reset_stats(self):
|
| 575 |
+
"""Reset bridge statistics"""
|
| 576 |
+
self.stats = {
|
| 577 |
+
'total_clients': 0,
|
| 578 |
+
'active_clients': len(self.clients),
|
| 579 |
+
'packets_processed': 0,
|
| 580 |
+
'packets_forwarded': 0,
|
| 581 |
+
'packets_dropped': 0,
|
| 582 |
+
'bytes_processed': 0,
|
| 583 |
+
'websocket_connections': 0,
|
| 584 |
+
'tcp_connections': 0,
|
| 585 |
+
'connection_errors': 0
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
async def start_websocket_server(self):
|
| 589 |
+
"""Start WebSocket server"""
|
| 590 |
+
try:
|
| 591 |
+
self.websocket_server = await websockets.serve(
|
| 592 |
+
self._handle_websocket_client,
|
| 593 |
+
self.websocket_host,
|
| 594 |
+
self.websocket_port,
|
| 595 |
+
max_size=1024*1024, # 1MB max message size
|
| 596 |
+
ping_interval=30,
|
| 597 |
+
ping_timeout=10
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
logging.info(f"WebSocket bridge server started on {self.websocket_host}:{self.websocket_port}")
|
| 601 |
+
|
| 602 |
+
# Keep server running
|
| 603 |
+
await self.websocket_server.wait_closed()
|
| 604 |
+
|
| 605 |
+
except Exception as e:
|
| 606 |
+
logging.error(f"WebSocket server error: {e}")
|
| 607 |
+
|
| 608 |
+
def start(self):
|
| 609 |
+
"""Start packet bridge"""
|
| 610 |
+
self.running = True
|
| 611 |
+
|
| 612 |
+
# Start event loop
|
| 613 |
+
self.loop = asyncio.new_event_loop()
|
| 614 |
+
asyncio.set_event_loop(self.loop)
|
| 615 |
+
|
| 616 |
+
# Start WebSocket server in a separate thread
|
| 617 |
+
websocket_thread = threading.Thread(target=self._run_websocket_server_in_thread, daemon=True)
|
| 618 |
+
websocket_thread.start()
|
| 619 |
+
|
| 620 |
+
# Start TCP server in separate thread
|
| 621 |
+
tcp_thread = threading.Thread(target=self._tcp_server_loop, daemon=True)
|
| 622 |
+
tcp_thread.start()
|
| 623 |
+
|
| 624 |
+
# Start cleanup thread
|
| 625 |
+
cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
|
| 626 |
+
cleanup_thread.start()
|
| 627 |
+
|
| 628 |
+
logging.info("Packet bridge started")
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
def stop(self):
|
| 633 |
+
"""Stop packet bridge"""
|
| 634 |
+
self.running = False
|
| 635 |
+
|
| 636 |
+
# Close WebSocket server
|
| 637 |
+
if self.websocket_server:
|
| 638 |
+
self.websocket_server.close()
|
| 639 |
+
|
| 640 |
+
# Close TCP server
|
| 641 |
+
if self.tcp_server_socket:
|
| 642 |
+
self.tcp_server_socket.close()
|
| 643 |
+
|
| 644 |
+
# Disconnect all clients
|
| 645 |
+
with self.lock:
|
| 646 |
+
client_ids = list(self.clients.keys())
|
| 647 |
+
|
| 648 |
+
for client_id in client_ids:
|
| 649 |
+
self.disconnect_client(client_id)
|
| 650 |
+
|
| 651 |
+
# Stop event loop
|
| 652 |
+
if self.loop and not self.loop.is_closed():
|
| 653 |
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
| 654 |
+
|
| 655 |
+
logging.info("Packet bridge stopped")
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def _run_websocket_server_in_thread(self):
|
| 660 |
+
"""Run the WebSocket server in a separate thread with its own event loop."""
|
| 661 |
+
asyncio.set_event_loop(self.loop)
|
| 662 |
+
self.loop.run_until_complete(self.start_websocket_server())
|
| 663 |
+
|
| 664 |
+
|
core/session_tracker.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Session Tracker Module
|
| 3 |
+
|
| 4 |
+
Manages and tracks all network sessions across the virtual ISP stack:
|
| 5 |
+
- Unified session management across all modules
|
| 6 |
+
- Session lifecycle tracking
|
| 7 |
+
- Performance metrics and analytics
|
| 8 |
+
- Session correlation and debugging
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import time
|
| 12 |
+
import threading
|
| 13 |
+
import uuid
|
| 14 |
+
from typing import Dict, List, Optional, Set, Any, Tuple
|
| 15 |
+
from dataclasses import dataclass, field
|
| 16 |
+
from enum import Enum
|
| 17 |
+
import json
|
| 18 |
+
|
| 19 |
+
from .dhcp_server import DHCPLease
|
| 20 |
+
from .nat_engine import NATSession
|
| 21 |
+
from .tcp_engine import TCPConnection
|
| 22 |
+
from .socket_translator import SocketConnection
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SessionType(Enum):
|
| 26 |
+
DHCP_LEASE = "DHCP_LEASE"
|
| 27 |
+
NAT_SESSION = "NAT_SESSION"
|
| 28 |
+
TCP_CONNECTION = "TCP_CONNECTION"
|
| 29 |
+
SOCKET_CONNECTION = "SOCKET_CONNECTION"
|
| 30 |
+
BRIDGE_CLIENT = "BRIDGE_CLIENT"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class SessionState(Enum):
|
| 34 |
+
INITIALIZING = "INITIALIZING"
|
| 35 |
+
ACTIVE = "ACTIVE"
|
| 36 |
+
IDLE = "IDLE"
|
| 37 |
+
CLOSING = "CLOSING"
|
| 38 |
+
CLOSED = "CLOSED"
|
| 39 |
+
ERROR = "ERROR"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class SessionMetrics:
|
| 44 |
+
"""Session performance metrics"""
|
| 45 |
+
bytes_in: int = 0
|
| 46 |
+
bytes_out: int = 0
|
| 47 |
+
packets_in: int = 0
|
| 48 |
+
packets_out: int = 0
|
| 49 |
+
errors: int = 0
|
| 50 |
+
retransmits: int = 0
|
| 51 |
+
rtt_samples: List[float] = field(default_factory=list)
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def total_bytes(self) -> int:
|
| 55 |
+
return self.bytes_in + self.bytes_out
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def total_packets(self) -> int:
|
| 59 |
+
return self.packets_in + self.packets_out
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def average_rtt(self) -> float:
|
| 63 |
+
return sum(self.rtt_samples) / len(self.rtt_samples) if self.rtt_samples else 0.0
|
| 64 |
+
|
| 65 |
+
def update_bytes(self, bytes_in: int = 0, bytes_out: int = 0):
|
| 66 |
+
"""Update byte counters"""
|
| 67 |
+
self.bytes_in += bytes_in
|
| 68 |
+
self.bytes_out += bytes_out
|
| 69 |
+
|
| 70 |
+
def update_packets(self, packets_in: int = 0, packets_out: int = 0):
|
| 71 |
+
"""Update packet counters"""
|
| 72 |
+
self.packets_in += packets_in
|
| 73 |
+
self.packets_out += packets_out
|
| 74 |
+
|
| 75 |
+
def add_rtt_sample(self, rtt: float):
|
| 76 |
+
"""Add RTT sample"""
|
| 77 |
+
self.rtt_samples.append(rtt)
|
| 78 |
+
# Keep only last 100 samples
|
| 79 |
+
if len(self.rtt_samples) > 100:
|
| 80 |
+
self.rtt_samples = self.rtt_samples[-100:]
|
| 81 |
+
|
| 82 |
+
def to_dict(self) -> Dict:
|
| 83 |
+
"""Convert to dictionary"""
|
| 84 |
+
return {
|
| 85 |
+
'bytes_in': self.bytes_in,
|
| 86 |
+
'bytes_out': self.bytes_out,
|
| 87 |
+
'packets_in': self.packets_in,
|
| 88 |
+
'packets_out': self.packets_out,
|
| 89 |
+
'total_bytes': self.total_bytes,
|
| 90 |
+
'total_packets': self.total_packets,
|
| 91 |
+
'errors': self.errors,
|
| 92 |
+
'retransmits': self.retransmits,
|
| 93 |
+
'average_rtt': self.average_rtt,
|
| 94 |
+
'rtt_samples_count': len(self.rtt_samples)
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@dataclass
|
| 99 |
+
class UnifiedSession:
|
| 100 |
+
"""Unified session representation"""
|
| 101 |
+
session_id: str
|
| 102 |
+
session_type: SessionType
|
| 103 |
+
state: SessionState
|
| 104 |
+
created_time: float
|
| 105 |
+
last_activity: float
|
| 106 |
+
|
| 107 |
+
# Session identifiers
|
| 108 |
+
virtual_ip: Optional[str] = None
|
| 109 |
+
virtual_port: Optional[int] = None
|
| 110 |
+
real_ip: Optional[str] = None
|
| 111 |
+
real_port: Optional[int] = None
|
| 112 |
+
protocol: Optional[str] = None
|
| 113 |
+
|
| 114 |
+
# Related sessions (for correlation)
|
| 115 |
+
related_sessions: Set[str] = field(default_factory=set)
|
| 116 |
+
parent_session: Optional[str] = None
|
| 117 |
+
child_sessions: Set[str] = field(default_factory=set)
|
| 118 |
+
|
| 119 |
+
# Metrics
|
| 120 |
+
metrics: SessionMetrics = field(default_factory=SessionMetrics)
|
| 121 |
+
|
| 122 |
+
# Additional data
|
| 123 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 124 |
+
|
| 125 |
+
def __post_init__(self):
|
| 126 |
+
if not self.session_id:
|
| 127 |
+
self.session_id = str(uuid.uuid4())
|
| 128 |
+
if self.created_time == 0:
|
| 129 |
+
self.created_time = time.time()
|
| 130 |
+
if self.last_activity == 0:
|
| 131 |
+
self.last_activity = time.time()
|
| 132 |
+
|
| 133 |
+
def update_activity(self):
|
| 134 |
+
"""Update last activity timestamp"""
|
| 135 |
+
self.last_activity = time.time()
|
| 136 |
+
|
| 137 |
+
def add_related_session(self, session_id: str):
|
| 138 |
+
"""Add related session"""
|
| 139 |
+
self.related_sessions.add(session_id)
|
| 140 |
+
|
| 141 |
+
def add_child_session(self, session_id: str):
|
| 142 |
+
"""Add child session"""
|
| 143 |
+
self.child_sessions.add(session_id)
|
| 144 |
+
|
| 145 |
+
def set_parent_session(self, session_id: str):
|
| 146 |
+
"""Set parent session"""
|
| 147 |
+
self.parent_session = session_id
|
| 148 |
+
|
| 149 |
+
@property
|
| 150 |
+
def duration(self) -> float:
|
| 151 |
+
"""Get session duration in seconds"""
|
| 152 |
+
return time.time() - self.created_time
|
| 153 |
+
|
| 154 |
+
@property
|
| 155 |
+
def idle_time(self) -> float:
|
| 156 |
+
"""Get idle time in seconds"""
|
| 157 |
+
return time.time() - self.last_activity
|
| 158 |
+
|
| 159 |
+
def to_dict(self) -> Dict:
|
| 160 |
+
"""Convert to dictionary"""
|
| 161 |
+
return {
|
| 162 |
+
'session_id': self.session_id,
|
| 163 |
+
'session_type': self.session_type.value,
|
| 164 |
+
'state': self.state.value,
|
| 165 |
+
'created_time': self.created_time,
|
| 166 |
+
'last_activity': self.last_activity,
|
| 167 |
+
'duration': self.duration,
|
| 168 |
+
'idle_time': self.idle_time,
|
| 169 |
+
'virtual_ip': self.virtual_ip,
|
| 170 |
+
'virtual_port': self.virtual_port,
|
| 171 |
+
'real_ip': self.real_ip,
|
| 172 |
+
'real_port': self.real_port,
|
| 173 |
+
'protocol': self.protocol,
|
| 174 |
+
'related_sessions': list(self.related_sessions),
|
| 175 |
+
'parent_session': self.parent_session,
|
| 176 |
+
'child_sessions': list(self.child_sessions),
|
| 177 |
+
'metrics': self.metrics.to_dict(),
|
| 178 |
+
'metadata': self.metadata
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class SessionTracker:
|
| 183 |
+
"""Unified session tracker"""
|
| 184 |
+
|
| 185 |
+
def __init__(self, config: Dict):
|
| 186 |
+
self.config = config
|
| 187 |
+
self.sessions: Dict[str, UnifiedSession] = {}
|
| 188 |
+
self.session_index: Dict[Tuple[str, str], Set[str]] = {} # (type, key) -> session_ids
|
| 189 |
+
self.lock = threading.Lock()
|
| 190 |
+
|
| 191 |
+
# Configuration
|
| 192 |
+
self.max_sessions = config.get('max_sessions', 10000)
|
| 193 |
+
self.session_timeout = config.get('session_timeout', 3600)
|
| 194 |
+
self.cleanup_interval = config.get('cleanup_interval', 300)
|
| 195 |
+
self.metrics_retention = config.get('metrics_retention', 86400) # 24 hours
|
| 196 |
+
|
| 197 |
+
# Statistics
|
| 198 |
+
self.stats = {
|
| 199 |
+
'total_sessions': 0,
|
| 200 |
+
'active_sessions': 0,
|
| 201 |
+
'expired_sessions': 0,
|
| 202 |
+
'session_types': {t.value: 0 for t in SessionType},
|
| 203 |
+
'session_states': {s.value: 0 for s in SessionState},
|
| 204 |
+
'cleanup_runs': 0,
|
| 205 |
+
'correlations_created': 0
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
# Background tasks
|
| 209 |
+
self.running = False
|
| 210 |
+
self.cleanup_thread = None
|
| 211 |
+
|
| 212 |
+
def _generate_session_key(self, session_type: SessionType, **kwargs) -> str:
|
| 213 |
+
"""Generate session key for indexing"""
|
| 214 |
+
if session_type == SessionType.DHCP_LEASE:
|
| 215 |
+
return f"dhcp_{kwargs.get('mac_address', 'unknown')}"
|
| 216 |
+
elif session_type == SessionType.NAT_SESSION:
|
| 217 |
+
return f"nat_{kwargs.get('virtual_ip', '')}_{kwargs.get('virtual_port', 0)}_{kwargs.get('protocol', '')}"
|
| 218 |
+
elif session_type == SessionType.TCP_CONNECTION:
|
| 219 |
+
return f"tcp_{kwargs.get('local_ip', '')}_{kwargs.get('local_port', 0)}_{kwargs.get('remote_ip', '')}_{kwargs.get('remote_port', 0)}"
|
| 220 |
+
elif session_type == SessionType.SOCKET_CONNECTION:
|
| 221 |
+
return f"socket_{kwargs.get('connection_id', 'unknown')}"
|
| 222 |
+
elif session_type == SessionType.BRIDGE_CLIENT:
|
| 223 |
+
return f"bridge_{kwargs.get('client_id', 'unknown')}"
|
| 224 |
+
else:
|
| 225 |
+
return f"unknown_{time.time()}"
|
| 226 |
+
|
| 227 |
+
def _add_to_index(self, session: UnifiedSession):
|
| 228 |
+
"""Add session to search index"""
|
| 229 |
+
# Index by type
|
| 230 |
+
type_key = (session.session_type.value, 'all')
|
| 231 |
+
if type_key not in self.session_index:
|
| 232 |
+
self.session_index[type_key] = set()
|
| 233 |
+
self.session_index[type_key].add(session.session_id)
|
| 234 |
+
|
| 235 |
+
# Index by IP addresses
|
| 236 |
+
if session.virtual_ip:
|
| 237 |
+
ip_key = ('virtual_ip', session.virtual_ip)
|
| 238 |
+
if ip_key not in self.session_index:
|
| 239 |
+
self.session_index[ip_key] = set()
|
| 240 |
+
self.session_index[ip_key].add(session.session_id)
|
| 241 |
+
|
| 242 |
+
if session.real_ip:
|
| 243 |
+
ip_key = ('real_ip', session.real_ip)
|
| 244 |
+
if ip_key not in self.session_index:
|
| 245 |
+
self.session_index[ip_key] = set()
|
| 246 |
+
self.session_index[ip_key].add(session.session_id)
|
| 247 |
+
|
| 248 |
+
# Index by protocol
|
| 249 |
+
if session.protocol:
|
| 250 |
+
proto_key = ('protocol', session.protocol)
|
| 251 |
+
if proto_key not in self.session_index:
|
| 252 |
+
self.session_index[proto_key] = set()
|
| 253 |
+
self.session_index[proto_key].add(session.session_id)
|
| 254 |
+
|
| 255 |
+
def _remove_from_index(self, session: UnifiedSession):
|
| 256 |
+
"""Remove session from search index"""
|
| 257 |
+
for key, session_set in self.session_index.items():
|
| 258 |
+
session_set.discard(session.session_id)
|
| 259 |
+
|
| 260 |
+
def create_session(self, session_type: SessionType, **kwargs) -> str:
|
| 261 |
+
"""Create new session"""
|
| 262 |
+
with self.lock:
|
| 263 |
+
# Check session limit
|
| 264 |
+
if len(self.sessions) >= self.max_sessions:
|
| 265 |
+
# Remove oldest expired session
|
| 266 |
+
self._cleanup_expired_sessions()
|
| 267 |
+
if len(self.sessions) >= self.max_sessions:
|
| 268 |
+
return None
|
| 269 |
+
|
| 270 |
+
# Create session
|
| 271 |
+
session = UnifiedSession(
|
| 272 |
+
session_id=kwargs.get('session_id', str(uuid.uuid4())),
|
| 273 |
+
session_type=session_type,
|
| 274 |
+
state=SessionState.INITIALIZING,
|
| 275 |
+
virtual_ip=kwargs.get('virtual_ip'),
|
| 276 |
+
virtual_port=kwargs.get('virtual_port'),
|
| 277 |
+
real_ip=kwargs.get('real_ip'),
|
| 278 |
+
real_port=kwargs.get('real_port'),
|
| 279 |
+
protocol=kwargs.get('protocol'),
|
| 280 |
+
metadata=kwargs.get('metadata', {})
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# Add to sessions
|
| 284 |
+
self.sessions[session.session_id] = session
|
| 285 |
+
self._add_to_index(session)
|
| 286 |
+
|
| 287 |
+
# Update statistics
|
| 288 |
+
self.stats['total_sessions'] += 1
|
| 289 |
+
self.stats['active_sessions'] = len(self.sessions)
|
| 290 |
+
self.stats['session_types'][session_type.value] += 1
|
| 291 |
+
self.stats['session_states'][SessionState.INITIALIZING.value] += 1
|
| 292 |
+
|
| 293 |
+
return session.session_id
|
| 294 |
+
|
| 295 |
+
def update_session(self, session_id: str, **kwargs) -> bool:
|
| 296 |
+
"""Update session"""
|
| 297 |
+
with self.lock:
|
| 298 |
+
session = self.sessions.get(session_id)
|
| 299 |
+
if not session:
|
| 300 |
+
return False
|
| 301 |
+
|
| 302 |
+
# Update fields
|
| 303 |
+
old_state = session.state
|
| 304 |
+
|
| 305 |
+
for key, value in kwargs.items():
|
| 306 |
+
if hasattr(session, key):
|
| 307 |
+
setattr(session, key, value)
|
| 308 |
+
|
| 309 |
+
session.update_activity()
|
| 310 |
+
|
| 311 |
+
# Update state statistics
|
| 312 |
+
if 'state' in kwargs and kwargs['state'] != old_state:
|
| 313 |
+
self.stats['session_states'][old_state.value] -= 1
|
| 314 |
+
self.stats['session_states'][kwargs['state'].value] += 1
|
| 315 |
+
|
| 316 |
+
return True
|
| 317 |
+
|
| 318 |
+
def close_session(self, session_id: str, reason: str = "") -> bool:
|
| 319 |
+
"""Close session"""
|
| 320 |
+
with self.lock:
|
| 321 |
+
session = self.sessions.get(session_id)
|
| 322 |
+
if not session:
|
| 323 |
+
return False
|
| 324 |
+
|
| 325 |
+
old_state = session.state
|
| 326 |
+
session.state = SessionState.CLOSED
|
| 327 |
+
session.update_activity()
|
| 328 |
+
|
| 329 |
+
if reason:
|
| 330 |
+
session.metadata['close_reason'] = reason
|
| 331 |
+
|
| 332 |
+
# Update statistics
|
| 333 |
+
self.stats['session_states'][old_state.value] -= 1
|
| 334 |
+
self.stats['session_states'][SessionState.CLOSED.value] += 1
|
| 335 |
+
|
| 336 |
+
return True
|
| 337 |
+
|
| 338 |
+
def remove_session(self, session_id: str) -> bool:
|
| 339 |
+
"""Remove session completely"""
|
| 340 |
+
with self.lock:
|
| 341 |
+
session = self.sessions.get(session_id)
|
| 342 |
+
if not session:
|
| 343 |
+
return False
|
| 344 |
+
|
| 345 |
+
# Remove from index
|
| 346 |
+
self._remove_from_index(session)
|
| 347 |
+
|
| 348 |
+
# Remove from sessions
|
| 349 |
+
del self.sessions[session_id]
|
| 350 |
+
|
| 351 |
+
# Update statistics
|
| 352 |
+
self.stats['active_sessions'] = len(self.sessions)
|
| 353 |
+
self.stats['session_types'][session.session_type.value] -= 1
|
| 354 |
+
self.stats['session_states'][session.state.value] -= 1
|
| 355 |
+
|
| 356 |
+
return True
|
| 357 |
+
|
| 358 |
+
def get_session(self, session_id: str) -> Optional[UnifiedSession]:
|
| 359 |
+
"""Get session by ID"""
|
| 360 |
+
with self.lock:
|
| 361 |
+
return self.sessions.get(session_id)
|
| 362 |
+
|
| 363 |
+
def find_sessions(self, **criteria) -> List[UnifiedSession]:
|
| 364 |
+
"""Find sessions by criteria"""
|
| 365 |
+
with self.lock:
|
| 366 |
+
matching_sessions = []
|
| 367 |
+
|
| 368 |
+
# Use index if possible
|
| 369 |
+
if 'session_type' in criteria:
|
| 370 |
+
type_key = (criteria['session_type'].value if isinstance(criteria['session_type'], SessionType) else criteria['session_type'], 'all')
|
| 371 |
+
candidate_ids = self.session_index.get(type_key, set())
|
| 372 |
+
elif 'virtual_ip' in criteria:
|
| 373 |
+
ip_key = ('virtual_ip', criteria['virtual_ip'])
|
| 374 |
+
candidate_ids = self.session_index.get(ip_key, set())
|
| 375 |
+
elif 'real_ip' in criteria:
|
| 376 |
+
ip_key = ('real_ip', criteria['real_ip'])
|
| 377 |
+
candidate_ids = self.session_index.get(ip_key, set())
|
| 378 |
+
elif 'protocol' in criteria:
|
| 379 |
+
proto_key = ('protocol', criteria['protocol'])
|
| 380 |
+
candidate_ids = self.session_index.get(proto_key, set())
|
| 381 |
+
else:
|
| 382 |
+
candidate_ids = set(self.sessions.keys())
|
| 383 |
+
|
| 384 |
+
# Filter candidates
|
| 385 |
+
for session_id in candidate_ids:
|
| 386 |
+
session = self.sessions.get(session_id)
|
| 387 |
+
if not session:
|
| 388 |
+
continue
|
| 389 |
+
|
| 390 |
+
match = True
|
| 391 |
+
for key, value in criteria.items():
|
| 392 |
+
if hasattr(session, key):
|
| 393 |
+
session_value = getattr(session, key)
|
| 394 |
+
if isinstance(value, (SessionType, SessionState)):
|
| 395 |
+
if session_value != value:
|
| 396 |
+
match = False
|
| 397 |
+
break
|
| 398 |
+
elif session_value != value:
|
| 399 |
+
match = False
|
| 400 |
+
break
|
| 401 |
+
else:
|
| 402 |
+
match = False
|
| 403 |
+
break
|
| 404 |
+
|
| 405 |
+
if match:
|
| 406 |
+
matching_sessions.append(session)
|
| 407 |
+
|
| 408 |
+
return matching_sessions
|
| 409 |
+
|
| 410 |
+
def correlate_sessions(self, session_id1: str, session_id2: str, relationship: str = 'related') -> bool:
|
| 411 |
+
"""Create correlation between sessions"""
|
| 412 |
+
with self.lock:
|
| 413 |
+
session1 = self.sessions.get(session_id1)
|
| 414 |
+
session2 = self.sessions.get(session_id2)
|
| 415 |
+
|
| 416 |
+
if not session1 or not session2:
|
| 417 |
+
return False
|
| 418 |
+
|
| 419 |
+
if relationship == 'parent_child':
|
| 420 |
+
session1.add_child_session(session_id2)
|
| 421 |
+
session2.set_parent_session(session_id1)
|
| 422 |
+
else:
|
| 423 |
+
session1.add_related_session(session_id2)
|
| 424 |
+
session2.add_related_session(session_id1)
|
| 425 |
+
|
| 426 |
+
self.stats['correlations_created'] += 1
|
| 427 |
+
return True
|
| 428 |
+
|
| 429 |
+
def update_metrics(self, session_id: str, **metrics) -> bool:
|
| 430 |
+
"""Update session metrics"""
|
| 431 |
+
with self.lock:
|
| 432 |
+
session = self.sessions.get(session_id)
|
| 433 |
+
if not session:
|
| 434 |
+
return False
|
| 435 |
+
|
| 436 |
+
session.update_activity()
|
| 437 |
+
|
| 438 |
+
# Update metrics
|
| 439 |
+
if 'bytes_in' in metrics or 'bytes_out' in metrics:
|
| 440 |
+
session.metrics.update_bytes(
|
| 441 |
+
metrics.get('bytes_in', 0),
|
| 442 |
+
metrics.get('bytes_out', 0)
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
if 'packets_in' in metrics or 'packets_out' in metrics:
|
| 446 |
+
session.metrics.update_packets(
|
| 447 |
+
metrics.get('packets_in', 0),
|
| 448 |
+
metrics.get('packets_out', 0)
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
if 'rtt' in metrics:
|
| 452 |
+
session.metrics.add_rtt_sample(metrics['rtt'])
|
| 453 |
+
|
| 454 |
+
if 'errors' in metrics:
|
| 455 |
+
session.metrics.errors += metrics['errors']
|
| 456 |
+
|
| 457 |
+
if 'retransmits' in metrics:
|
| 458 |
+
session.metrics.retransmits += metrics['retransmits']
|
| 459 |
+
|
| 460 |
+
return True
|
| 461 |
+
|
| 462 |
+
def _cleanup_expired_sessions(self):
|
| 463 |
+
"""Clean up expired sessions"""
|
| 464 |
+
current_time = time.time()
|
| 465 |
+
expired_sessions = []
|
| 466 |
+
|
| 467 |
+
for session_id, session in self.sessions.items():
|
| 468 |
+
# Check if session is expired
|
| 469 |
+
if (session.state == SessionState.CLOSED and
|
| 470 |
+
current_time - session.last_activity > self.cleanup_interval):
|
| 471 |
+
expired_sessions.append(session_id)
|
| 472 |
+
elif (session.state != SessionState.CLOSED and
|
| 473 |
+
current_time - session.last_activity > self.session_timeout):
|
| 474 |
+
expired_sessions.append(session_id)
|
| 475 |
+
|
| 476 |
+
# Remove expired sessions
|
| 477 |
+
for session_id in expired_sessions:
|
| 478 |
+
self.remove_session(session_id)
|
| 479 |
+
self.stats['expired_sessions'] += 1
|
| 480 |
+
|
| 481 |
+
def _cleanup_loop(self):
|
| 482 |
+
"""Background cleanup loop"""
|
| 483 |
+
while self.running:
|
| 484 |
+
try:
|
| 485 |
+
with self.lock:
|
| 486 |
+
self._cleanup_expired_sessions()
|
| 487 |
+
self.stats['cleanup_runs'] += 1
|
| 488 |
+
|
| 489 |
+
time.sleep(self.cleanup_interval)
|
| 490 |
+
|
| 491 |
+
except Exception as e:
|
| 492 |
+
print(f"Session tracker cleanup error: {e}")
|
| 493 |
+
time.sleep(60)
|
| 494 |
+
|
| 495 |
+
def get_sessions(self, limit: int = 100, offset: int = 0, **filters) -> List[Dict]:
|
| 496 |
+
"""Get sessions with pagination and filtering"""
|
| 497 |
+
with self.lock:
|
| 498 |
+
if filters:
|
| 499 |
+
sessions = self.find_sessions(**filters)
|
| 500 |
+
else:
|
| 501 |
+
sessions = list(self.sessions.values())
|
| 502 |
+
|
| 503 |
+
# Sort by last activity (most recent first)
|
| 504 |
+
sessions.sort(key=lambda s: s.last_activity, reverse=True)
|
| 505 |
+
|
| 506 |
+
# Apply pagination
|
| 507 |
+
paginated_sessions = sessions[offset:offset + limit]
|
| 508 |
+
|
| 509 |
+
return [session.to_dict() for session in paginated_sessions]
|
| 510 |
+
|
| 511 |
+
def get_session_summary(self) -> Dict:
|
| 512 |
+
"""Get session summary statistics"""
|
| 513 |
+
with self.lock:
|
| 514 |
+
summary = {
|
| 515 |
+
'total_sessions': len(self.sessions),
|
| 516 |
+
'by_type': {},
|
| 517 |
+
'by_state': {},
|
| 518 |
+
'by_protocol': {},
|
| 519 |
+
'active_sessions_by_age': {
|
| 520 |
+
'last_hour': 0,
|
| 521 |
+
'last_day': 0,
|
| 522 |
+
'older': 0
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
current_time = time.time()
|
| 527 |
+
hour_ago = current_time - 3600
|
| 528 |
+
day_ago = current_time - 86400
|
| 529 |
+
|
| 530 |
+
for session in self.sessions.values():
|
| 531 |
+
# Count by type
|
| 532 |
+
session_type = session.session_type.value
|
| 533 |
+
summary['by_type'][session_type] = summary['by_type'].get(session_type, 0) + 1
|
| 534 |
+
|
| 535 |
+
# Count by state
|
| 536 |
+
session_state = session.state.value
|
| 537 |
+
summary['by_state'][session_state] = summary['by_state'].get(session_state, 0) + 1
|
| 538 |
+
|
| 539 |
+
# Count by protocol
|
| 540 |
+
if session.protocol:
|
| 541 |
+
summary['by_protocol'][session.protocol] = summary['by_protocol'].get(session.protocol, 0) + 1
|
| 542 |
+
|
| 543 |
+
# Count by age
|
| 544 |
+
if session.last_activity > hour_ago:
|
| 545 |
+
summary['active_sessions_by_age']['last_hour'] += 1
|
| 546 |
+
elif session.last_activity > day_ago:
|
| 547 |
+
summary['active_sessions_by_age']['last_day'] += 1
|
| 548 |
+
else:
|
| 549 |
+
summary['active_sessions_by_age']['older'] += 1
|
| 550 |
+
|
| 551 |
+
return summary
|
| 552 |
+
|
| 553 |
+
def get_stats(self) -> Dict:
|
| 554 |
+
"""Get tracker statistics"""
|
| 555 |
+
with self.lock:
|
| 556 |
+
stats = self.stats.copy()
|
| 557 |
+
stats['active_sessions'] = len(self.sessions)
|
| 558 |
+
|
| 559 |
+
return stats
|
| 560 |
+
|
| 561 |
+
def reset_stats(self):
|
| 562 |
+
"""Reset statistics"""
|
| 563 |
+
self.stats = {
|
| 564 |
+
'total_sessions': len(self.sessions),
|
| 565 |
+
'active_sessions': len(self.sessions),
|
| 566 |
+
'expired_sessions': 0,
|
| 567 |
+
'session_types': {t.value: 0 for t in SessionType},
|
| 568 |
+
'session_states': {s.value: 0 for s in SessionState},
|
| 569 |
+
'cleanup_runs': 0,
|
| 570 |
+
'correlations_created': 0
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
# Recalculate current counts
|
| 574 |
+
with self.lock:
|
| 575 |
+
for session in self.sessions.values():
|
| 576 |
+
self.stats['session_types'][session.session_type.value] += 1
|
| 577 |
+
self.stats['session_states'][session.state.value] += 1
|
| 578 |
+
|
| 579 |
+
def export_sessions(self, format: str = 'json') -> str:
|
| 580 |
+
"""Export sessions data"""
|
| 581 |
+
with self.lock:
|
| 582 |
+
sessions_data = [session.to_dict() for session in self.sessions.values()]
|
| 583 |
+
|
| 584 |
+
if format == 'json':
|
| 585 |
+
return json.dumps(sessions_data, indent=2, default=str)
|
| 586 |
+
else:
|
| 587 |
+
raise ValueError(f"Unsupported export format: {format}")
|
| 588 |
+
|
| 589 |
+
def start(self):
|
| 590 |
+
"""Start session tracker"""
|
| 591 |
+
self.running = True
|
| 592 |
+
self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
|
| 593 |
+
self.cleanup_thread.start()
|
| 594 |
+
print("Session tracker started")
|
| 595 |
+
|
| 596 |
+
def stop(self):
|
| 597 |
+
"""Stop session tracker"""
|
| 598 |
+
self.running = False
|
| 599 |
+
if self.cleanup_thread:
|
| 600 |
+
self.cleanup_thread.join()
|
| 601 |
+
print("Session tracker stopped")
|
| 602 |
+
|
core/socket_translator.py
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Socket Translator Module
|
| 3 |
+
|
| 4 |
+
Bridges virtual connections to real host sockets:
|
| 5 |
+
- Map virtual connections to host sockets/HTTP clients
|
| 6 |
+
- Bidirectional data streaming
|
| 7 |
+
- Connection lifecycle management
|
| 8 |
+
- Protocol translation (TCP/UDP to host sockets)
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import socket
|
| 12 |
+
import threading
|
| 13 |
+
import time
|
| 14 |
+
import asyncio
|
| 15 |
+
import aiohttp
|
| 16 |
+
import ssl
|
| 17 |
+
from typing import Dict, Optional, Callable, Tuple, Any
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from enum import Enum
|
| 20 |
+
import urllib.parse
|
| 21 |
+
import json
|
| 22 |
+
|
| 23 |
+
from .tcp_engine import TCPConnection
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ConnectionType(Enum):
|
| 27 |
+
TCP_SOCKET = "TCP_SOCKET"
|
| 28 |
+
UDP_SOCKET = "UDP_SOCKET"
|
| 29 |
+
HTTP_CLIENT = "HTTP_CLIENT"
|
| 30 |
+
HTTPS_CLIENT = "HTTPS_CLIENT"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class SocketConnection:
|
| 35 |
+
"""Represents a socket connection"""
|
| 36 |
+
connection_id: str
|
| 37 |
+
connection_type: ConnectionType
|
| 38 |
+
virtual_connection: Optional[TCPConnection]
|
| 39 |
+
host_socket: Optional[socket.socket]
|
| 40 |
+
remote_host: str
|
| 41 |
+
remote_port: int
|
| 42 |
+
created_time: float
|
| 43 |
+
last_activity: float
|
| 44 |
+
bytes_sent: int = 0
|
| 45 |
+
bytes_received: int = 0
|
| 46 |
+
is_connected: bool = False
|
| 47 |
+
error_count: int = 0
|
| 48 |
+
|
| 49 |
+
def update_activity(self, bytes_transferred: int = 0, direction: str = 'sent'):
|
| 50 |
+
"""Update connection activity"""
|
| 51 |
+
self.last_activity = time.time()
|
| 52 |
+
if direction == 'sent':
|
| 53 |
+
self.bytes_sent += bytes_transferred
|
| 54 |
+
else:
|
| 55 |
+
self.bytes_received += bytes_transferred
|
| 56 |
+
|
| 57 |
+
def to_dict(self) -> Dict:
|
| 58 |
+
"""Convert to dictionary"""
|
| 59 |
+
return {
|
| 60 |
+
'connection_id': self.connection_id,
|
| 61 |
+
'connection_type': self.connection_type.value,
|
| 62 |
+
'remote_host': self.remote_host,
|
| 63 |
+
'remote_port': self.remote_port,
|
| 64 |
+
'created_time': self.created_time,
|
| 65 |
+
'last_activity': self.last_activity,
|
| 66 |
+
'bytes_sent': self.bytes_sent,
|
| 67 |
+
'bytes_received': self.bytes_received,
|
| 68 |
+
'is_connected': self.is_connected,
|
| 69 |
+
'error_count': self.error_count,
|
| 70 |
+
'duration': time.time() - self.created_time
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class HTTPRequest:
|
| 75 |
+
"""Represents an HTTP request"""
|
| 76 |
+
|
| 77 |
+
def __init__(self, method: str = 'GET', path: str = '/', headers: Dict[str, str] = None, body: bytes = b''):
|
| 78 |
+
self.method = method.upper()
|
| 79 |
+
self.path = path
|
| 80 |
+
self.headers = headers or {}
|
| 81 |
+
self.body = body
|
| 82 |
+
self.version = 'HTTP/1.1'
|
| 83 |
+
|
| 84 |
+
@classmethod
|
| 85 |
+
def parse(cls, data: bytes) -> Optional['HTTPRequest']:
|
| 86 |
+
"""Parse HTTP request from raw data"""
|
| 87 |
+
try:
|
| 88 |
+
lines = data.decode('utf-8', errors='ignore').split('\r\n')
|
| 89 |
+
if not lines:
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
# Parse request line
|
| 93 |
+
request_line = lines[0].split(' ')
|
| 94 |
+
if len(request_line) < 3:
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
method, path, version = request_line[0], request_line[1], request_line[2]
|
| 98 |
+
|
| 99 |
+
# Parse headers
|
| 100 |
+
headers = {}
|
| 101 |
+
body_start = 1
|
| 102 |
+
for i, line in enumerate(lines[1:], 1):
|
| 103 |
+
if line == '':
|
| 104 |
+
body_start = i + 1
|
| 105 |
+
break
|
| 106 |
+
if ':' in line:
|
| 107 |
+
key, value = line.split(':', 1)
|
| 108 |
+
headers[key.strip().lower()] = value.strip()
|
| 109 |
+
|
| 110 |
+
# Parse body
|
| 111 |
+
body_lines = lines[body_start:]
|
| 112 |
+
body = '\r\n'.join(body_lines).encode('utf-8')
|
| 113 |
+
|
| 114 |
+
return cls(method, path, headers, body)
|
| 115 |
+
|
| 116 |
+
except Exception:
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
def to_bytes(self) -> bytes:
|
| 120 |
+
"""Convert to raw HTTP request"""
|
| 121 |
+
request_line = f"{self.method} {self.path} {self.version}\r\n"
|
| 122 |
+
|
| 123 |
+
# Add default headers
|
| 124 |
+
if 'host' not in self.headers:
|
| 125 |
+
self.headers['host'] = 'localhost'
|
| 126 |
+
if 'user-agent' not in self.headers:
|
| 127 |
+
self.headers['user-agent'] = 'VirtualISP/1.0'
|
| 128 |
+
if self.body and 'content-length' not in self.headers:
|
| 129 |
+
self.headers['content-length'] = str(len(self.body))
|
| 130 |
+
|
| 131 |
+
# Build headers
|
| 132 |
+
header_lines = []
|
| 133 |
+
for key, value in self.headers.items():
|
| 134 |
+
header_lines.append(f"{key}: {value}\r\n")
|
| 135 |
+
|
| 136 |
+
# Combine all parts
|
| 137 |
+
request_data = request_line + ''.join(header_lines) + '\r\n'
|
| 138 |
+
return request_data.encode('utf-8') + self.body
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class HTTPResponse:
|
| 142 |
+
"""Represents an HTTP response"""
|
| 143 |
+
|
| 144 |
+
def __init__(self, status_code: int = 200, reason: str = 'OK', headers: Dict[str, str] = None, body: bytes = b''):
|
| 145 |
+
self.status_code = status_code
|
| 146 |
+
self.reason = reason
|
| 147 |
+
self.headers = headers or {}
|
| 148 |
+
self.body = body
|
| 149 |
+
self.version = 'HTTP/1.1'
|
| 150 |
+
|
| 151 |
+
@classmethod
|
| 152 |
+
def parse(cls, data: bytes) -> Optional['HTTPResponse']:
|
| 153 |
+
"""Parse HTTP response from raw data"""
|
| 154 |
+
try:
|
| 155 |
+
lines = data.decode('utf-8', errors='ignore').split('\r\n')
|
| 156 |
+
if not lines:
|
| 157 |
+
return None
|
| 158 |
+
|
| 159 |
+
# Parse status line
|
| 160 |
+
status_line = lines[0].split(' ', 2)
|
| 161 |
+
if len(status_line) < 3:
|
| 162 |
+
return None
|
| 163 |
+
|
| 164 |
+
version, status_code, reason = status_line[0], int(status_line[1]), status_line[2]
|
| 165 |
+
|
| 166 |
+
# Parse headers
|
| 167 |
+
headers = {}
|
| 168 |
+
body_start = 1
|
| 169 |
+
for i, line in enumerate(lines[1:], 1):
|
| 170 |
+
if line == '':
|
| 171 |
+
body_start = i + 1
|
| 172 |
+
break
|
| 173 |
+
if ':' in line:
|
| 174 |
+
key, value = line.split(':', 1)
|
| 175 |
+
headers[key.strip().lower()] = value.strip()
|
| 176 |
+
|
| 177 |
+
# Parse body
|
| 178 |
+
body_lines = lines[body_start:]
|
| 179 |
+
body = '\r\n'.join(body_lines).encode('utf-8')
|
| 180 |
+
|
| 181 |
+
return cls(status_code, reason, headers, body)
|
| 182 |
+
|
| 183 |
+
except Exception:
|
| 184 |
+
return None
|
| 185 |
+
|
| 186 |
+
def to_bytes(self) -> bytes:
|
| 187 |
+
"""Convert to raw HTTP response"""
|
| 188 |
+
status_line = f"{self.version} {self.status_code} {self.reason}\r\n"
|
| 189 |
+
|
| 190 |
+
# Add default headers
|
| 191 |
+
if 'content-length' not in self.headers and self.body:
|
| 192 |
+
self.headers['content-length'] = str(len(self.body))
|
| 193 |
+
if 'server' not in self.headers:
|
| 194 |
+
self.headers['server'] = 'VirtualISP/1.0'
|
| 195 |
+
|
| 196 |
+
# Build headers
|
| 197 |
+
header_lines = []
|
| 198 |
+
for key, value in self.headers.items():
|
| 199 |
+
header_lines.append(f"{key}: {value}\r\n")
|
| 200 |
+
|
| 201 |
+
# Combine all parts
|
| 202 |
+
response_data = status_line + ''.join(header_lines) + '\r\n'
|
| 203 |
+
return response_data.encode('utf-8') + self.body
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class SocketTranslator:
|
| 207 |
+
"""Socket translator implementation"""
|
| 208 |
+
|
| 209 |
+
def __init__(self, config: Dict):
|
| 210 |
+
self.config = config
|
| 211 |
+
self.connections: Dict[str, SocketConnection] = {}
|
| 212 |
+
self.lock = threading.Lock()
|
| 213 |
+
|
| 214 |
+
# Configuration
|
| 215 |
+
self.connect_timeout = config.get('connect_timeout', 10)
|
| 216 |
+
self.read_timeout = config.get('read_timeout', 30)
|
| 217 |
+
self.max_connections = config.get('max_connections', 1000)
|
| 218 |
+
self.buffer_size = config.get('buffer_size', 8192)
|
| 219 |
+
|
| 220 |
+
# HTTP client session
|
| 221 |
+
self.http_session = None
|
| 222 |
+
self.loop = None
|
| 223 |
+
|
| 224 |
+
# Statistics
|
| 225 |
+
self.stats = {
|
| 226 |
+
'total_connections': 0,
|
| 227 |
+
'active_connections': 0,
|
| 228 |
+
'failed_connections': 0,
|
| 229 |
+
'bytes_transferred': 0,
|
| 230 |
+
'http_requests': 0,
|
| 231 |
+
'tcp_connections': 0,
|
| 232 |
+
'udp_connections': 0
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
# Background tasks
|
| 236 |
+
self.running = False
|
| 237 |
+
self.cleanup_thread = None
|
| 238 |
+
|
| 239 |
+
async def _init_http_session(self):
|
| 240 |
+
"""Initialize HTTP client session"""
|
| 241 |
+
connector = aiohttp.TCPConnector(
|
| 242 |
+
limit=100,
|
| 243 |
+
limit_per_host=10,
|
| 244 |
+
ttl_dns_cache=300,
|
| 245 |
+
use_dns_cache=True,
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
timeout = aiohttp.ClientTimeout(
|
| 249 |
+
total=self.read_timeout,
|
| 250 |
+
connect=self.connect_timeout
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
self.http_session = aiohttp.ClientSession(
|
| 254 |
+
connector=connector,
|
| 255 |
+
timeout=timeout,
|
| 256 |
+
headers={'User-Agent': 'VirtualISP/1.0'}
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def _is_http_request(self, data: bytes) -> bool:
|
| 260 |
+
"""Check if data looks like an HTTP request"""
|
| 261 |
+
try:
|
| 262 |
+
first_line = data.split(b'\r\n')[0].decode('utf-8', errors='ignore')
|
| 263 |
+
methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH', 'TRACE']
|
| 264 |
+
return any(first_line.startswith(method + ' ') for method in methods)
|
| 265 |
+
except:
|
| 266 |
+
return False
|
| 267 |
+
|
| 268 |
+
def _determine_connection_type(self, remote_host: str, remote_port: int, data: bytes = b'') -> ConnectionType:
|
| 269 |
+
"""Determine the appropriate connection type"""
|
| 270 |
+
# Check for HTTP/HTTPS based on port and data
|
| 271 |
+
if remote_port == 80 or (data and self._is_http_request(data)):
|
| 272 |
+
return ConnectionType.HTTP_CLIENT
|
| 273 |
+
elif remote_port == 443:
|
| 274 |
+
return ConnectionType.HTTPS_CLIENT
|
| 275 |
+
else:
|
| 276 |
+
return ConnectionType.TCP_SOCKET
|
| 277 |
+
|
| 278 |
+
def create_connection(self, virtual_conn: TCPConnection, remote_host: str, remote_port: int,
|
| 279 |
+
initial_data: bytes = b'') -> Optional[SocketConnection]:
|
| 280 |
+
"""Create a new socket connection"""
|
| 281 |
+
connection_id = f"{virtual_conn.connection_id}->{remote_host}:{remote_port}"
|
| 282 |
+
|
| 283 |
+
# Check connection limit
|
| 284 |
+
with self.lock:
|
| 285 |
+
if len(self.connections) >= self.max_connections:
|
| 286 |
+
return None
|
| 287 |
+
|
| 288 |
+
# Determine connection type
|
| 289 |
+
conn_type = self._determine_connection_type(remote_host, remote_port, initial_data)
|
| 290 |
+
|
| 291 |
+
# Create socket connection
|
| 292 |
+
socket_conn = SocketConnection(
|
| 293 |
+
connection_id=connection_id,
|
| 294 |
+
connection_type=conn_type,
|
| 295 |
+
virtual_connection=virtual_conn,
|
| 296 |
+
host_socket=None,
|
| 297 |
+
remote_host=remote_host,
|
| 298 |
+
remote_port=remote_port,
|
| 299 |
+
created_time=time.time(),
|
| 300 |
+
last_activity=time.time()
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
with self.lock:
|
| 304 |
+
self.connections[connection_id] = socket_conn
|
| 305 |
+
|
| 306 |
+
# Establish connection based on type
|
| 307 |
+
if conn_type in [ConnectionType.HTTP_CLIENT, ConnectionType.HTTPS_CLIENT]:
|
| 308 |
+
success = self._create_http_connection(socket_conn, initial_data)
|
| 309 |
+
else:
|
| 310 |
+
success = self._create_tcp_connection(socket_conn, initial_data)
|
| 311 |
+
|
| 312 |
+
if success:
|
| 313 |
+
self.stats['total_connections'] += 1
|
| 314 |
+
self.stats['active_connections'] = len(self.connections)
|
| 315 |
+
|
| 316 |
+
if conn_type in [ConnectionType.HTTP_CLIENT, ConnectionType.HTTPS_CLIENT]:
|
| 317 |
+
self.stats['http_requests'] += 1
|
| 318 |
+
else:
|
| 319 |
+
self.stats['tcp_connections'] += 1
|
| 320 |
+
else:
|
| 321 |
+
self.stats['failed_connections'] += 1
|
| 322 |
+
with self.lock:
|
| 323 |
+
if connection_id in self.connections:
|
| 324 |
+
del self.connections[connection_id]
|
| 325 |
+
return None
|
| 326 |
+
|
| 327 |
+
return socket_conn
|
| 328 |
+
|
| 329 |
+
def _create_tcp_connection(self, socket_conn: SocketConnection, initial_data: bytes) -> bool:
|
| 330 |
+
"""Create TCP socket connection"""
|
| 331 |
+
try:
|
| 332 |
+
# Create socket
|
| 333 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 334 |
+
sock.settimeout(self.connect_timeout)
|
| 335 |
+
|
| 336 |
+
# Connect
|
| 337 |
+
sock.connect((socket_conn.remote_host, socket_conn.remote_port))
|
| 338 |
+
sock.settimeout(self.read_timeout)
|
| 339 |
+
|
| 340 |
+
socket_conn.host_socket = sock
|
| 341 |
+
socket_conn.is_connected = True
|
| 342 |
+
|
| 343 |
+
# Send initial data if any
|
| 344 |
+
if initial_data:
|
| 345 |
+
sock.send(initial_data)
|
| 346 |
+
socket_conn.update_activity(len(initial_data), 'sent')
|
| 347 |
+
|
| 348 |
+
# Start background thread for receiving data
|
| 349 |
+
thread = threading.Thread(
|
| 350 |
+
target=self._tcp_receive_loop,
|
| 351 |
+
args=(socket_conn,),
|
| 352 |
+
daemon=True
|
| 353 |
+
)
|
| 354 |
+
thread.start()
|
| 355 |
+
|
| 356 |
+
return True
|
| 357 |
+
|
| 358 |
+
except Exception as e:
|
| 359 |
+
print(f"Failed to create TCP connection to {socket_conn.remote_host}:{socket_conn.remote_port}: {e}")
|
| 360 |
+
socket_conn.error_count += 1
|
| 361 |
+
return False
|
| 362 |
+
|
| 363 |
+
def _create_http_connection(self, socket_conn: SocketConnection, initial_data: bytes) -> bool:
|
| 364 |
+
"""Create HTTP connection"""
|
| 365 |
+
try:
|
| 366 |
+
# Parse HTTP request
|
| 367 |
+
http_request = HTTPRequest.parse(initial_data)
|
| 368 |
+
if not http_request:
|
| 369 |
+
return False
|
| 370 |
+
|
| 371 |
+
# Set host header
|
| 372 |
+
http_request.headers['host'] = socket_conn.remote_host
|
| 373 |
+
|
| 374 |
+
# Start async HTTP request
|
| 375 |
+
if self.loop and not self.loop.is_closed():
|
| 376 |
+
asyncio.run_coroutine_threadsafe(
|
| 377 |
+
self._handle_http_request(socket_conn, http_request),
|
| 378 |
+
self.loop
|
| 379 |
+
)
|
| 380 |
+
else:
|
| 381 |
+
# Fallback to sync HTTP handling
|
| 382 |
+
return self._handle_http_request_sync(socket_conn, http_request)
|
| 383 |
+
|
| 384 |
+
return True
|
| 385 |
+
|
| 386 |
+
except Exception as e:
|
| 387 |
+
print(f"Failed to create HTTP connection to {socket_conn.remote_host}:{socket_conn.remote_port}: {e}")
|
| 388 |
+
socket_conn.error_count += 1
|
| 389 |
+
return False
|
| 390 |
+
|
| 391 |
+
async def _handle_http_request(self, socket_conn: SocketConnection, http_request: HTTPRequest):
|
| 392 |
+
"""Handle HTTP request asynchronously"""
|
| 393 |
+
try:
|
| 394 |
+
if not self.http_session:
|
| 395 |
+
await self._init_http_session()
|
| 396 |
+
|
| 397 |
+
# Build URL
|
| 398 |
+
scheme = 'https' if socket_conn.connection_type == ConnectionType.HTTPS_CLIENT else 'http'
|
| 399 |
+
url = f"{scheme}://{socket_conn.remote_host}:{socket_conn.remote_port}{http_request.path}"
|
| 400 |
+
|
| 401 |
+
# Make request
|
| 402 |
+
async with self.http_session.request(
|
| 403 |
+
method=http_request.method,
|
| 404 |
+
url=url,
|
| 405 |
+
headers=http_request.headers,
|
| 406 |
+
data=http_request.body
|
| 407 |
+
) as response:
|
| 408 |
+
# Read response
|
| 409 |
+
response_body = await response.read()
|
| 410 |
+
|
| 411 |
+
# Create HTTP response
|
| 412 |
+
http_response = HTTPResponse(
|
| 413 |
+
status_code=response.status,
|
| 414 |
+
reason=response.reason or 'OK',
|
| 415 |
+
headers=dict(response.headers),
|
| 416 |
+
body=response_body
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
# Send response back to virtual connection
|
| 420 |
+
response_data = http_response.to_bytes()
|
| 421 |
+
if socket_conn.virtual_connection and socket_conn.virtual_connection.on_data_received:
|
| 422 |
+
socket_conn.virtual_connection.on_data_received(response_data)
|
| 423 |
+
|
| 424 |
+
socket_conn.update_activity(len(response_data), 'received')
|
| 425 |
+
self.stats['bytes_transferred'] += len(response_data)
|
| 426 |
+
|
| 427 |
+
except Exception as e:
|
| 428 |
+
print(f"HTTP request failed: {e}")
|
| 429 |
+
socket_conn.error_count += 1
|
| 430 |
+
|
| 431 |
+
# Send error response
|
| 432 |
+
error_response = HTTPResponse(
|
| 433 |
+
status_code=500,
|
| 434 |
+
reason='Internal Server Error',
|
| 435 |
+
body=f"Error: {str(e)}".encode('utf-8')
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
response_data = error_response.to_bytes()
|
| 439 |
+
if socket_conn.virtual_connection and socket_conn.virtual_connection.on_data_received:
|
| 440 |
+
socket_conn.virtual_connection.on_data_received(response_data)
|
| 441 |
+
|
| 442 |
+
def _handle_http_request_sync(self, socket_conn: SocketConnection, http_request: HTTPRequest) -> bool:
|
| 443 |
+
"""Handle HTTP request synchronously (fallback)"""
|
| 444 |
+
try:
|
| 445 |
+
# Use urllib for sync HTTP requests
|
| 446 |
+
scheme = 'https' if socket_conn.connection_type == ConnectionType.HTTPS_CLIENT else 'http'
|
| 447 |
+
url = f"{scheme}://{socket_conn.remote_host}:{socket_conn.remote_port}{http_request.path}"
|
| 448 |
+
|
| 449 |
+
import urllib.request
|
| 450 |
+
import urllib.error
|
| 451 |
+
|
| 452 |
+
# Create request
|
| 453 |
+
req = urllib.request.Request(
|
| 454 |
+
url,
|
| 455 |
+
data=http_request.body if http_request.body else None,
|
| 456 |
+
headers=http_request.headers,
|
| 457 |
+
method=http_request.method
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Make request
|
| 461 |
+
with urllib.request.urlopen(req, timeout=self.read_timeout) as response:
|
| 462 |
+
response_body = response.read()
|
| 463 |
+
|
| 464 |
+
# Create HTTP response
|
| 465 |
+
http_response = HTTPResponse(
|
| 466 |
+
status_code=response.getcode(),
|
| 467 |
+
reason='OK',
|
| 468 |
+
headers=dict(response.headers),
|
| 469 |
+
body=response_body
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
# Send response back to virtual connection
|
| 473 |
+
response_data = http_response.to_bytes()
|
| 474 |
+
if socket_conn.virtual_connection and socket_conn.virtual_connection.on_data_received:
|
| 475 |
+
socket_conn.virtual_connection.on_data_received(response_data)
|
| 476 |
+
|
| 477 |
+
socket_conn.update_activity(len(response_data), 'received')
|
| 478 |
+
self.stats['bytes_transferred'] += len(response_data)
|
| 479 |
+
|
| 480 |
+
return True
|
| 481 |
+
|
| 482 |
+
except Exception as e:
|
| 483 |
+
print(f"Sync HTTP request failed: {e}")
|
| 484 |
+
socket_conn.error_count += 1
|
| 485 |
+
return False
|
| 486 |
+
|
| 487 |
+
def _tcp_receive_loop(self, socket_conn: SocketConnection):
|
| 488 |
+
"""Background loop for receiving TCP data"""
|
| 489 |
+
sock = socket_conn.host_socket
|
| 490 |
+
if not sock:
|
| 491 |
+
return
|
| 492 |
+
|
| 493 |
+
try:
|
| 494 |
+
while socket_conn.is_connected:
|
| 495 |
+
try:
|
| 496 |
+
data = sock.recv(self.buffer_size)
|
| 497 |
+
if not data:
|
| 498 |
+
break
|
| 499 |
+
|
| 500 |
+
# Forward data to virtual connection
|
| 501 |
+
if socket_conn.virtual_connection and socket_conn.virtual_connection.on_data_received:
|
| 502 |
+
socket_conn.virtual_connection.on_data_received(data)
|
| 503 |
+
|
| 504 |
+
socket_conn.update_activity(len(data), 'received')
|
| 505 |
+
self.stats['bytes_transferred'] += len(data)
|
| 506 |
+
|
| 507 |
+
except socket.timeout:
|
| 508 |
+
continue
|
| 509 |
+
except Exception as e:
|
| 510 |
+
print(f"TCP receive error: {e}")
|
| 511 |
+
break
|
| 512 |
+
|
| 513 |
+
finally:
|
| 514 |
+
self._close_connection(socket_conn.connection_id)
|
| 515 |
+
|
| 516 |
+
def send_data(self, connection_id: str, data: bytes) -> bool:
|
| 517 |
+
"""Send data through socket connection"""
|
| 518 |
+
with self.lock:
|
| 519 |
+
socket_conn = self.connections.get(connection_id)
|
| 520 |
+
|
| 521 |
+
if not socket_conn or not socket_conn.is_connected:
|
| 522 |
+
return False
|
| 523 |
+
|
| 524 |
+
try:
|
| 525 |
+
if socket_conn.connection_type in [ConnectionType.HTTP_CLIENT, ConnectionType.HTTPS_CLIENT]:
|
| 526 |
+
# For HTTP connections, treat as new request
|
| 527 |
+
return self._create_http_connection(socket_conn, data)
|
| 528 |
+
else:
|
| 529 |
+
# TCP connection
|
| 530 |
+
if socket_conn.host_socket:
|
| 531 |
+
socket_conn.host_socket.send(data)
|
| 532 |
+
socket_conn.update_activity(len(data), 'sent')
|
| 533 |
+
self.stats['bytes_transferred'] += len(data)
|
| 534 |
+
return True
|
| 535 |
+
|
| 536 |
+
except Exception as e:
|
| 537 |
+
print(f"Failed to send data: {e}")
|
| 538 |
+
socket_conn.error_count += 1
|
| 539 |
+
self._close_connection(connection_id)
|
| 540 |
+
|
| 541 |
+
return False
|
| 542 |
+
|
| 543 |
+
def _close_connection(self, connection_id: str):
|
| 544 |
+
"""Close socket connection"""
|
| 545 |
+
with self.lock:
|
| 546 |
+
socket_conn = self.connections.get(connection_id)
|
| 547 |
+
if not socket_conn:
|
| 548 |
+
return
|
| 549 |
+
|
| 550 |
+
# Close socket
|
| 551 |
+
if socket_conn.host_socket:
|
| 552 |
+
try:
|
| 553 |
+
socket_conn.host_socket.close()
|
| 554 |
+
except:
|
| 555 |
+
pass
|
| 556 |
+
|
| 557 |
+
socket_conn.is_connected = False
|
| 558 |
+
|
| 559 |
+
# Remove from connections
|
| 560 |
+
del self.connections[connection_id]
|
| 561 |
+
|
| 562 |
+
self.stats['active_connections'] = len(self.connections)
|
| 563 |
+
|
| 564 |
+
def close_connection(self, connection_id: str) -> bool:
|
| 565 |
+
"""Manually close connection"""
|
| 566 |
+
self._close_connection(connection_id)
|
| 567 |
+
return True
|
| 568 |
+
|
| 569 |
+
def get_connection(self, connection_id: str) -> Optional[SocketConnection]:
|
| 570 |
+
"""Get socket connection"""
|
| 571 |
+
with self.lock:
|
| 572 |
+
return self.connections.get(connection_id)
|
| 573 |
+
|
| 574 |
+
def get_connections(self) -> Dict[str, Dict]:
|
| 575 |
+
"""Get all socket connections"""
|
| 576 |
+
with self.lock:
|
| 577 |
+
return {
|
| 578 |
+
conn_id: conn.to_dict()
|
| 579 |
+
for conn_id, conn in self.connections.items()
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
def get_stats(self) -> Dict:
|
| 583 |
+
"""Get socket translator statistics"""
|
| 584 |
+
with self.lock:
|
| 585 |
+
stats = self.stats.copy()
|
| 586 |
+
stats['active_connections'] = len(self.connections)
|
| 587 |
+
|
| 588 |
+
return stats
|
| 589 |
+
|
| 590 |
+
def _cleanup_loop(self):
|
| 591 |
+
"""Background cleanup loop"""
|
| 592 |
+
while self.running:
|
| 593 |
+
try:
|
| 594 |
+
current_time = time.time()
|
| 595 |
+
expired_connections = []
|
| 596 |
+
|
| 597 |
+
with self.lock:
|
| 598 |
+
for conn_id, conn in self.connections.items():
|
| 599 |
+
# Close connections that have been inactive too long
|
| 600 |
+
if current_time - conn.last_activity > self.read_timeout * 2:
|
| 601 |
+
expired_connections.append(conn_id)
|
| 602 |
+
|
| 603 |
+
for conn_id in expired_connections:
|
| 604 |
+
self._close_connection(conn_id)
|
| 605 |
+
|
| 606 |
+
time.sleep(30) # Cleanup every 30 seconds
|
| 607 |
+
|
| 608 |
+
except Exception as e:
|
| 609 |
+
print(f"Socket translator cleanup error: {e}")
|
| 610 |
+
time.sleep(5)
|
| 611 |
+
|
| 612 |
+
def start(self):
|
| 613 |
+
"""Start socket translator"""
|
| 614 |
+
self.running = True
|
| 615 |
+
|
| 616 |
+
# Start event loop for async HTTP
|
| 617 |
+
try:
|
| 618 |
+
self.loop = asyncio.new_event_loop()
|
| 619 |
+
asyncio.set_event_loop(self.loop)
|
| 620 |
+
|
| 621 |
+
# Start cleanup thread
|
| 622 |
+
self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
|
| 623 |
+
self.cleanup_thread.start()
|
| 624 |
+
|
| 625 |
+
print("Socket translator started")
|
| 626 |
+
except Exception as e:
|
| 627 |
+
print(f"Failed to start socket translator: {e}")
|
| 628 |
+
|
| 629 |
+
def stop(self):
|
| 630 |
+
"""Stop socket translator"""
|
| 631 |
+
self.running = False
|
| 632 |
+
|
| 633 |
+
# Close all connections
|
| 634 |
+
with self.lock:
|
| 635 |
+
connection_ids = list(self.connections.keys())
|
| 636 |
+
|
| 637 |
+
for conn_id in connection_ids:
|
| 638 |
+
self._close_connection(conn_id)
|
| 639 |
+
|
| 640 |
+
# Close HTTP session
|
| 641 |
+
if self.http_session:
|
| 642 |
+
asyncio.run_coroutine_threadsafe(self.http_session.close(), self.loop)
|
| 643 |
+
|
| 644 |
+
# Close event loop
|
| 645 |
+
if self.loop and not self.loop.is_closed():
|
| 646 |
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
| 647 |
+
|
| 648 |
+
# Wait for cleanup thread
|
| 649 |
+
if self.cleanup_thread:
|
| 650 |
+
self.cleanup_thread.join()
|
| 651 |
+
|
| 652 |
+
print("Socket translator stopped")
|
| 653 |
+
|
core/tcp_engine.py
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TCP Engine Module
|
| 3 |
+
|
| 4 |
+
Implements a complete TCP state machine in user-space:
|
| 5 |
+
- Full TCP state machine (SYN, SYN-ACK, ESTABLISHED, FIN, RST)
|
| 6 |
+
- Sequence and acknowledgment number tracking
|
| 7 |
+
- Sliding window implementation
|
| 8 |
+
- Retransmission and timeout handling
|
| 9 |
+
- Congestion control
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import time
|
| 13 |
+
import threading
|
| 14 |
+
import random
|
| 15 |
+
from typing import Dict, List, Optional, Tuple, Callable
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from enum import Enum
|
| 18 |
+
from collections import deque
|
| 19 |
+
|
| 20 |
+
from .ip_parser import TCPHeader, IPv4Header, IPParser
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class TCPState(Enum):
|
| 24 |
+
CLOSED = "CLOSED"
|
| 25 |
+
LISTEN = "LISTEN"
|
| 26 |
+
SYN_SENT = "SYN_SENT"
|
| 27 |
+
SYN_RECEIVED = "SYN_RECEIVED"
|
| 28 |
+
ESTABLISHED = "ESTABLISHED"
|
| 29 |
+
FIN_WAIT_1 = "FIN_WAIT_1"
|
| 30 |
+
FIN_WAIT_2 = "FIN_WAIT_2"
|
| 31 |
+
CLOSE_WAIT = "CLOSE_WAIT"
|
| 32 |
+
CLOSING = "CLOSING"
|
| 33 |
+
LAST_ACK = "LAST_ACK"
|
| 34 |
+
TIME_WAIT = "TIME_WAIT"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class TCPSegment:
|
| 39 |
+
"""Represents a TCP segment"""
|
| 40 |
+
seq_num: int
|
| 41 |
+
ack_num: int
|
| 42 |
+
flags: int
|
| 43 |
+
window: int
|
| 44 |
+
data: bytes
|
| 45 |
+
timestamp: float = field(default_factory=time.time)
|
| 46 |
+
retransmit_count: int = 0
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def data_length(self) -> int:
|
| 50 |
+
"""Get data length"""
|
| 51 |
+
return len(self.data)
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def seq_end(self) -> int:
|
| 55 |
+
"""Get sequence number after this segment"""
|
| 56 |
+
length = self.data_length
|
| 57 |
+
# SYN and FIN consume one sequence number
|
| 58 |
+
if self.flags & 0x02: # SYN
|
| 59 |
+
length += 1
|
| 60 |
+
if self.flags & 0x01: # FIN
|
| 61 |
+
length += 1
|
| 62 |
+
return self.seq_num + length
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@dataclass
|
| 66 |
+
class TCPConnection:
|
| 67 |
+
"""Represents a TCP connection state"""
|
| 68 |
+
# Connection identification
|
| 69 |
+
local_ip: str
|
| 70 |
+
local_port: int
|
| 71 |
+
remote_ip: str
|
| 72 |
+
remote_port: int
|
| 73 |
+
|
| 74 |
+
# State
|
| 75 |
+
state: TCPState = TCPState.CLOSED
|
| 76 |
+
|
| 77 |
+
# Sequence numbers
|
| 78 |
+
local_seq: int = 0
|
| 79 |
+
local_ack: int = 0
|
| 80 |
+
remote_seq: int = 0
|
| 81 |
+
remote_ack: int = 0
|
| 82 |
+
initial_seq: int = 0
|
| 83 |
+
|
| 84 |
+
# Window management
|
| 85 |
+
local_window: int = 65535
|
| 86 |
+
remote_window: int = 65535
|
| 87 |
+
window_scale: int = 0
|
| 88 |
+
|
| 89 |
+
# Buffers
|
| 90 |
+
send_buffer: deque = field(default_factory=deque)
|
| 91 |
+
recv_buffer: deque = field(default_factory=deque)
|
| 92 |
+
out_of_order_buffer: Dict[int, bytes] = field(default_factory=dict)
|
| 93 |
+
|
| 94 |
+
# Retransmission
|
| 95 |
+
unacked_segments: Dict[int, TCPSegment] = field(default_factory=dict)
|
| 96 |
+
retransmit_timer: Optional[float] = None
|
| 97 |
+
rto: float = 1.0 # Retransmission timeout
|
| 98 |
+
srtt: float = 0.0 # Smoothed round-trip time
|
| 99 |
+
rttvar: float = 0.0 # Round-trip time variation
|
| 100 |
+
|
| 101 |
+
# Congestion control
|
| 102 |
+
cwnd: int = 1 # Congestion window (in MSS units)
|
| 103 |
+
ssthresh: int = 65535 # Slow start threshold
|
| 104 |
+
mss: int = 1460 # Maximum segment size
|
| 105 |
+
|
| 106 |
+
# Timers
|
| 107 |
+
last_activity: float = field(default_factory=time.time)
|
| 108 |
+
time_wait_start: Optional[float] = None
|
| 109 |
+
|
| 110 |
+
# Callbacks
|
| 111 |
+
on_data_received: Optional[Callable[[bytes], None]] = None
|
| 112 |
+
on_connection_closed: Optional[Callable[[], None]] = None
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def connection_id(self) -> str:
|
| 116 |
+
"""Get unique connection identifier"""
|
| 117 |
+
return f"{self.local_ip}:{self.local_port}-{self.remote_ip}:{self.remote_port}"
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def is_established(self) -> bool:
|
| 121 |
+
"""Check if connection is established"""
|
| 122 |
+
return self.state == TCPState.ESTABLISHED
|
| 123 |
+
|
| 124 |
+
@property
|
| 125 |
+
def can_send_data(self) -> bool:
|
| 126 |
+
"""Check if connection can send data"""
|
| 127 |
+
return self.state in [TCPState.ESTABLISHED, TCPState.CLOSE_WAIT]
|
| 128 |
+
|
| 129 |
+
@property
|
| 130 |
+
def effective_window(self) -> int:
|
| 131 |
+
"""Get effective send window"""
|
| 132 |
+
return min(self.remote_window, self.cwnd * self.mss)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class TCPEngine:
|
| 136 |
+
"""TCP state machine implementation"""
|
| 137 |
+
|
| 138 |
+
def __init__(self, config: Dict):
|
| 139 |
+
self.config = config
|
| 140 |
+
self.connections: Dict[str, TCPConnection] = {}
|
| 141 |
+
self.listening_ports: Dict[int, Callable] = {} # port -> accept callback
|
| 142 |
+
self.lock = threading.Lock()
|
| 143 |
+
self.running = False
|
| 144 |
+
self.timer_thread = None
|
| 145 |
+
|
| 146 |
+
# Default configuration
|
| 147 |
+
self.default_mss = config.get('mss', 1460)
|
| 148 |
+
self.default_window = config.get('initial_window', 65535)
|
| 149 |
+
self.max_retries = config.get('max_retries', 3)
|
| 150 |
+
self.connection_timeout = config.get('timeout', 300)
|
| 151 |
+
self.time_wait_timeout = config.get('time_wait_timeout', 120)
|
| 152 |
+
|
| 153 |
+
def _generate_isn(self) -> int:
|
| 154 |
+
"""Generate Initial Sequence Number"""
|
| 155 |
+
return random.randint(0, 0xFFFFFFFF)
|
| 156 |
+
|
| 157 |
+
def _get_connection_key(self, local_ip: str, local_port: int, remote_ip: str, remote_port: int) -> str:
|
| 158 |
+
"""Get connection key"""
|
| 159 |
+
return f"{local_ip}:{local_port}-{remote_ip}:{remote_port}"
|
| 160 |
+
|
| 161 |
+
def _create_tcp_segment(self, conn: TCPConnection, flags: int, data: bytes = b'') -> TCPSegment:
|
| 162 |
+
"""Create TCP segment"""
|
| 163 |
+
segment = TCPSegment(
|
| 164 |
+
seq_num=conn.local_seq,
|
| 165 |
+
ack_num=conn.local_ack,
|
| 166 |
+
flags=flags,
|
| 167 |
+
window=conn.local_window,
|
| 168 |
+
data=data
|
| 169 |
+
)
|
| 170 |
+
return segment
|
| 171 |
+
|
| 172 |
+
def _build_tcp_packet(self, conn: TCPConnection, segment: TCPSegment) -> bytes:
|
| 173 |
+
"""Build complete TCP packet"""
|
| 174 |
+
# Create IP header
|
| 175 |
+
ip_header = IPv4Header(
|
| 176 |
+
protocol=6, # TCP
|
| 177 |
+
source_ip=conn.local_ip,
|
| 178 |
+
dest_ip=conn.remote_ip,
|
| 179 |
+
ttl=64
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# Create TCP header
|
| 183 |
+
tcp_header = TCPHeader(
|
| 184 |
+
source_port=conn.local_port,
|
| 185 |
+
dest_port=conn.remote_port,
|
| 186 |
+
seq_num=segment.seq_num,
|
| 187 |
+
ack_num=segment.ack_num,
|
| 188 |
+
flags=segment.flags,
|
| 189 |
+
window_size=segment.window
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# Build packet
|
| 193 |
+
return IPParser.build_packet(ip_header, tcp_header, segment.data)
|
| 194 |
+
|
| 195 |
+
def _update_rto(self, conn: TCPConnection, rtt: float):
|
| 196 |
+
"""Update retransmission timeout using RFC 6298"""
|
| 197 |
+
if conn.srtt == 0:
|
| 198 |
+
# First RTT measurement
|
| 199 |
+
conn.srtt = rtt
|
| 200 |
+
conn.rttvar = rtt / 2
|
| 201 |
+
else:
|
| 202 |
+
# Subsequent measurements
|
| 203 |
+
alpha = 0.125
|
| 204 |
+
beta = 0.25
|
| 205 |
+
conn.rttvar = (1 - beta) * conn.rttvar + beta * abs(conn.srtt - rtt)
|
| 206 |
+
conn.srtt = (1 - alpha) * conn.srtt + alpha * rtt
|
| 207 |
+
|
| 208 |
+
# Calculate RTO
|
| 209 |
+
conn.rto = max(1.0, conn.srtt + 4 * conn.rttvar)
|
| 210 |
+
conn.rto = min(conn.rto, 60.0) # Cap at 60 seconds
|
| 211 |
+
|
| 212 |
+
def _update_congestion_window(self, conn: TCPConnection, acked_bytes: int):
|
| 213 |
+
"""Update congestion window (simplified congestion control)"""
|
| 214 |
+
if conn.cwnd < conn.ssthresh:
|
| 215 |
+
# Slow start
|
| 216 |
+
conn.cwnd += 1
|
| 217 |
+
else:
|
| 218 |
+
# Congestion avoidance
|
| 219 |
+
conn.cwnd += max(1, conn.mss * conn.mss // conn.cwnd)
|
| 220 |
+
|
| 221 |
+
def _handle_retransmission(self, conn: TCPConnection):
|
| 222 |
+
"""Handle segment retransmission"""
|
| 223 |
+
current_time = time.time()
|
| 224 |
+
|
| 225 |
+
# Find segments that need retransmission
|
| 226 |
+
to_retransmit = []
|
| 227 |
+
for seq_num, segment in conn.unacked_segments.items():
|
| 228 |
+
if current_time - segment.timestamp > conn.rto:
|
| 229 |
+
if segment.retransmit_count < self.max_retries:
|
| 230 |
+
to_retransmit.append(segment)
|
| 231 |
+
else:
|
| 232 |
+
# Max retries exceeded, close connection
|
| 233 |
+
self._close_connection(conn, reset=True)
|
| 234 |
+
return
|
| 235 |
+
|
| 236 |
+
# Retransmit segments
|
| 237 |
+
for segment in to_retransmit:
|
| 238 |
+
segment.retransmit_count += 1
|
| 239 |
+
segment.timestamp = current_time
|
| 240 |
+
|
| 241 |
+
# Exponential backoff
|
| 242 |
+
conn.rto = min(conn.rto * 2, 60.0)
|
| 243 |
+
|
| 244 |
+
# Congestion control: reduce window
|
| 245 |
+
conn.ssthresh = max(conn.cwnd // 2, 2)
|
| 246 |
+
conn.cwnd = 1
|
| 247 |
+
|
| 248 |
+
# Send retransmitted segment
|
| 249 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 250 |
+
self._send_packet(packet)
|
| 251 |
+
|
| 252 |
+
def _send_packet(self, packet: bytes):
|
| 253 |
+
"""Send packet (to be implemented by integration layer)"""
|
| 254 |
+
# This will be connected to the packet bridge
|
| 255 |
+
pass
|
| 256 |
+
|
| 257 |
+
def _close_connection(self, conn: TCPConnection, reset: bool = False):
|
| 258 |
+
"""Close connection"""
|
| 259 |
+
if reset:
|
| 260 |
+
# Send RST
|
| 261 |
+
segment = self._create_tcp_segment(conn, 0x04) # RST flag
|
| 262 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 263 |
+
self._send_packet(packet)
|
| 264 |
+
conn.state = TCPState.CLOSED
|
| 265 |
+
else:
|
| 266 |
+
# Normal close
|
| 267 |
+
if conn.state == TCPState.ESTABLISHED:
|
| 268 |
+
# Send FIN
|
| 269 |
+
segment = self._create_tcp_segment(conn, 0x01) # FIN flag
|
| 270 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 271 |
+
self._send_packet(packet)
|
| 272 |
+
conn.local_seq += 1
|
| 273 |
+
conn.state = TCPState.FIN_WAIT_1
|
| 274 |
+
|
| 275 |
+
# Cleanup if closed
|
| 276 |
+
if conn.state == TCPState.CLOSED:
|
| 277 |
+
if conn.on_connection_closed:
|
| 278 |
+
conn.on_connection_closed()
|
| 279 |
+
|
| 280 |
+
with self.lock:
|
| 281 |
+
if conn.connection_id in self.connections:
|
| 282 |
+
del self.connections[conn.connection_id]
|
| 283 |
+
|
| 284 |
+
def listen(self, port: int, accept_callback: Callable):
|
| 285 |
+
"""Listen on port for incoming connections"""
|
| 286 |
+
with self.lock:
|
| 287 |
+
self.listening_ports[port] = accept_callback
|
| 288 |
+
|
| 289 |
+
def connect(self, local_ip: str, local_port: int, remote_ip: str, remote_port: int) -> Optional[TCPConnection]:
|
| 290 |
+
"""Initiate outbound connection"""
|
| 291 |
+
conn_key = self._get_connection_key(local_ip, local_port, remote_ip, remote_port)
|
| 292 |
+
|
| 293 |
+
# Create connection
|
| 294 |
+
conn = TCPConnection(
|
| 295 |
+
local_ip=local_ip,
|
| 296 |
+
local_port=local_port,
|
| 297 |
+
remote_ip=remote_ip,
|
| 298 |
+
remote_port=remote_port,
|
| 299 |
+
state=TCPState.SYN_SENT,
|
| 300 |
+
local_seq=self._generate_isn(),
|
| 301 |
+
mss=self.default_mss,
|
| 302 |
+
local_window=self.default_window
|
| 303 |
+
)
|
| 304 |
+
conn.initial_seq = conn.local_seq
|
| 305 |
+
|
| 306 |
+
with self.lock:
|
| 307 |
+
self.connections[conn_key] = conn
|
| 308 |
+
|
| 309 |
+
# Send SYN
|
| 310 |
+
segment = self._create_tcp_segment(conn, 0x02) # SYN flag
|
| 311 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 312 |
+
self._send_packet(packet)
|
| 313 |
+
|
| 314 |
+
# Track unacked segment
|
| 315 |
+
conn.unacked_segments[conn.local_seq] = segment
|
| 316 |
+
conn.local_seq += 1
|
| 317 |
+
conn.retransmit_timer = time.time()
|
| 318 |
+
|
| 319 |
+
return conn
|
| 320 |
+
|
| 321 |
+
def send_data(self, conn: TCPConnection, data: bytes) -> bool:
|
| 322 |
+
"""Send data on established connection"""
|
| 323 |
+
if not conn.can_send_data:
|
| 324 |
+
return False
|
| 325 |
+
|
| 326 |
+
# Add to send buffer
|
| 327 |
+
conn.send_buffer.append(data)
|
| 328 |
+
|
| 329 |
+
# Try to send immediately
|
| 330 |
+
self._try_send_data(conn)
|
| 331 |
+
|
| 332 |
+
return True
|
| 333 |
+
|
| 334 |
+
def _try_send_data(self, conn: TCPConnection):
|
| 335 |
+
"""Try to send buffered data"""
|
| 336 |
+
while conn.send_buffer and len(conn.unacked_segments) * conn.mss < conn.effective_window:
|
| 337 |
+
data = conn.send_buffer.popleft()
|
| 338 |
+
|
| 339 |
+
# Split data if larger than MSS
|
| 340 |
+
while data:
|
| 341 |
+
chunk = data[:conn.mss]
|
| 342 |
+
data = data[conn.mss:]
|
| 343 |
+
|
| 344 |
+
# Create and send segment
|
| 345 |
+
segment = self._create_tcp_segment(conn, 0x18, chunk) # PSH+ACK flags
|
| 346 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 347 |
+
self._send_packet(packet)
|
| 348 |
+
|
| 349 |
+
# Track unacked segment
|
| 350 |
+
conn.unacked_segments[conn.local_seq] = segment
|
| 351 |
+
conn.local_seq += len(chunk)
|
| 352 |
+
|
| 353 |
+
if not data:
|
| 354 |
+
break
|
| 355 |
+
|
| 356 |
+
def process_packet(self, packet_data: bytes) -> bool:
|
| 357 |
+
"""Process incoming TCP packet"""
|
| 358 |
+
try:
|
| 359 |
+
# Parse packet
|
| 360 |
+
parsed = IPParser.parse_packet(packet_data)
|
| 361 |
+
if not isinstance(parsed.transport_header, TCPHeader):
|
| 362 |
+
return False
|
| 363 |
+
|
| 364 |
+
ip_header = parsed.ip_header
|
| 365 |
+
tcp_header = parsed.transport_header
|
| 366 |
+
payload = parsed.payload
|
| 367 |
+
|
| 368 |
+
# Find or create connection
|
| 369 |
+
conn_key = self._get_connection_key(
|
| 370 |
+
ip_header.dest_ip, tcp_header.dest_port,
|
| 371 |
+
ip_header.source_ip, tcp_header.source_port
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
with self.lock:
|
| 375 |
+
conn = self.connections.get(conn_key)
|
| 376 |
+
|
| 377 |
+
# Handle new connection (SYN to listening port)
|
| 378 |
+
if not conn and tcp_header.syn and not tcp_header.ack:
|
| 379 |
+
if tcp_header.dest_port in self.listening_ports:
|
| 380 |
+
conn = self._handle_new_connection(ip_header, tcp_header)
|
| 381 |
+
if conn:
|
| 382 |
+
self.connections[conn_key] = conn
|
| 383 |
+
|
| 384 |
+
if not conn:
|
| 385 |
+
# Send RST for unknown connection
|
| 386 |
+
self._send_rst(ip_header, tcp_header)
|
| 387 |
+
return False
|
| 388 |
+
|
| 389 |
+
# Process segment
|
| 390 |
+
return self._process_segment(conn, tcp_header, payload)
|
| 391 |
+
|
| 392 |
+
except Exception as e:
|
| 393 |
+
print(f"Error processing TCP packet: {e}")
|
| 394 |
+
return False
|
| 395 |
+
|
| 396 |
+
def _handle_new_connection(self, ip_header: IPv4Header, tcp_header: TCPHeader) -> Optional[TCPConnection]:
|
| 397 |
+
"""Handle new incoming connection"""
|
| 398 |
+
accept_callback = self.listening_ports.get(tcp_header.dest_port)
|
| 399 |
+
if not accept_callback:
|
| 400 |
+
return None
|
| 401 |
+
|
| 402 |
+
# Create connection
|
| 403 |
+
conn = TCPConnection(
|
| 404 |
+
local_ip=ip_header.dest_ip,
|
| 405 |
+
local_port=tcp_header.dest_port,
|
| 406 |
+
remote_ip=ip_header.source_ip,
|
| 407 |
+
remote_port=tcp_header.source_port,
|
| 408 |
+
state=TCPState.SYN_RECEIVED,
|
| 409 |
+
local_seq=self._generate_isn(),
|
| 410 |
+
remote_seq=tcp_header.seq_num,
|
| 411 |
+
local_ack=tcp_header.seq_num + 1,
|
| 412 |
+
mss=self.default_mss,
|
| 413 |
+
local_window=self.default_window
|
| 414 |
+
)
|
| 415 |
+
conn.initial_seq = conn.local_seq
|
| 416 |
+
|
| 417 |
+
# Send SYN-ACK
|
| 418 |
+
segment = self._create_tcp_segment(conn, 0x12) # SYN+ACK flags
|
| 419 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 420 |
+
self._send_packet(packet)
|
| 421 |
+
|
| 422 |
+
# Track unacked segment
|
| 423 |
+
conn.unacked_segments[conn.local_seq] = segment
|
| 424 |
+
conn.local_seq += 1
|
| 425 |
+
conn.retransmit_timer = time.time()
|
| 426 |
+
|
| 427 |
+
# Call accept callback
|
| 428 |
+
accept_callback(conn)
|
| 429 |
+
|
| 430 |
+
return conn
|
| 431 |
+
|
| 432 |
+
def _process_segment(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 433 |
+
"""Process TCP segment based on connection state"""
|
| 434 |
+
conn.last_activity = time.time()
|
| 435 |
+
|
| 436 |
+
# Handle RST
|
| 437 |
+
if tcp_header.rst:
|
| 438 |
+
conn.state = TCPState.CLOSED
|
| 439 |
+
self._close_connection(conn)
|
| 440 |
+
return True
|
| 441 |
+
|
| 442 |
+
# State machine
|
| 443 |
+
if conn.state == TCPState.SYN_SENT:
|
| 444 |
+
return self._handle_syn_sent(conn, tcp_header, payload)
|
| 445 |
+
elif conn.state == TCPState.SYN_RECEIVED:
|
| 446 |
+
return self._handle_syn_received(conn, tcp_header, payload)
|
| 447 |
+
elif conn.state == TCPState.ESTABLISHED:
|
| 448 |
+
return self._handle_established(conn, tcp_header, payload)
|
| 449 |
+
elif conn.state == TCPState.FIN_WAIT_1:
|
| 450 |
+
return self._handle_fin_wait_1(conn, tcp_header, payload)
|
| 451 |
+
elif conn.state == TCPState.FIN_WAIT_2:
|
| 452 |
+
return self._handle_fin_wait_2(conn, tcp_header, payload)
|
| 453 |
+
elif conn.state == TCPState.CLOSE_WAIT:
|
| 454 |
+
return self._handle_close_wait(conn, tcp_header, payload)
|
| 455 |
+
elif conn.state == TCPState.CLOSING:
|
| 456 |
+
return self._handle_closing(conn, tcp_header, payload)
|
| 457 |
+
elif conn.state == TCPState.LAST_ACK:
|
| 458 |
+
return self._handle_last_ack(conn, tcp_header, payload)
|
| 459 |
+
elif conn.state == TCPState.TIME_WAIT:
|
| 460 |
+
return self._handle_time_wait(conn, tcp_header, payload)
|
| 461 |
+
|
| 462 |
+
return False
|
| 463 |
+
|
| 464 |
+
def _handle_syn_sent(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 465 |
+
"""Handle segment in SYN_SENT state"""
|
| 466 |
+
if tcp_header.syn and tcp_header.ack:
|
| 467 |
+
# SYN-ACK received
|
| 468 |
+
if tcp_header.ack_num == conn.local_seq:
|
| 469 |
+
conn.remote_seq = tcp_header.seq_num
|
| 470 |
+
conn.local_ack = tcp_header.seq_num + 1
|
| 471 |
+
conn.remote_window = tcp_header.window_size
|
| 472 |
+
|
| 473 |
+
# Remove SYN from unacked segments
|
| 474 |
+
if conn.local_seq - 1 in conn.unacked_segments:
|
| 475 |
+
del conn.unacked_segments[conn.local_seq - 1]
|
| 476 |
+
|
| 477 |
+
# Send ACK
|
| 478 |
+
segment = self._create_tcp_segment(conn, 0x10) # ACK flag
|
| 479 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 480 |
+
self._send_packet(packet)
|
| 481 |
+
|
| 482 |
+
conn.state = TCPState.ESTABLISHED
|
| 483 |
+
return True
|
| 484 |
+
|
| 485 |
+
return False
|
| 486 |
+
|
| 487 |
+
def _handle_syn_received(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 488 |
+
"""Handle segment in SYN_RECEIVED state"""
|
| 489 |
+
if tcp_header.ack and tcp_header.ack_num == conn.local_seq:
|
| 490 |
+
# ACK for our SYN-ACK
|
| 491 |
+
conn.remote_window = tcp_header.window_size
|
| 492 |
+
|
| 493 |
+
# Remove SYN-ACK from unacked segments
|
| 494 |
+
if conn.local_seq - 1 in conn.unacked_segments:
|
| 495 |
+
del conn.unacked_segments[conn.local_seq - 1]
|
| 496 |
+
|
| 497 |
+
conn.state = TCPState.ESTABLISHED
|
| 498 |
+
return True
|
| 499 |
+
|
| 500 |
+
return False
|
| 501 |
+
|
| 502 |
+
def _handle_established(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 503 |
+
"""Handle segment in ESTABLISHED state"""
|
| 504 |
+
# Handle ACK
|
| 505 |
+
if tcp_header.ack:
|
| 506 |
+
self._process_ack(conn, tcp_header.ack_num)
|
| 507 |
+
|
| 508 |
+
# Handle data
|
| 509 |
+
if payload and tcp_header.seq_num == conn.local_ack:
|
| 510 |
+
conn.local_ack += len(payload)
|
| 511 |
+
|
| 512 |
+
# Deliver data
|
| 513 |
+
if conn.on_data_received:
|
| 514 |
+
conn.on_data_received(payload)
|
| 515 |
+
|
| 516 |
+
# Send ACK
|
| 517 |
+
segment = self._create_tcp_segment(conn, 0x10) # ACK flag
|
| 518 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 519 |
+
self._send_packet(packet)
|
| 520 |
+
|
| 521 |
+
# Handle FIN
|
| 522 |
+
if tcp_header.fin:
|
| 523 |
+
conn.local_ack += 1
|
| 524 |
+
|
| 525 |
+
# Send ACK
|
| 526 |
+
segment = self._create_tcp_segment(conn, 0x10) # ACK flag
|
| 527 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 528 |
+
self._send_packet(packet)
|
| 529 |
+
|
| 530 |
+
conn.state = TCPState.CLOSE_WAIT
|
| 531 |
+
|
| 532 |
+
return True
|
| 533 |
+
|
| 534 |
+
def _handle_fin_wait_1(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 535 |
+
"""Handle segment in FIN_WAIT_1 state"""
|
| 536 |
+
if tcp_header.ack:
|
| 537 |
+
self._process_ack(conn, tcp_header.ack_num)
|
| 538 |
+
if not conn.unacked_segments: # Our FIN was ACKed
|
| 539 |
+
conn.state = TCPState.FIN_WAIT_2
|
| 540 |
+
|
| 541 |
+
if tcp_header.fin:
|
| 542 |
+
conn.local_ack += 1
|
| 543 |
+
|
| 544 |
+
# Send ACK
|
| 545 |
+
segment = self._create_tcp_segment(conn, 0x10) # ACK flag
|
| 546 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 547 |
+
self._send_packet(packet)
|
| 548 |
+
|
| 549 |
+
if conn.state == TCPState.FIN_WAIT_2:
|
| 550 |
+
conn.state = TCPState.TIME_WAIT
|
| 551 |
+
conn.time_wait_start = time.time()
|
| 552 |
+
else:
|
| 553 |
+
conn.state = TCPState.CLOSING
|
| 554 |
+
|
| 555 |
+
return True
|
| 556 |
+
|
| 557 |
+
def _handle_fin_wait_2(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 558 |
+
"""Handle segment in FIN_WAIT_2 state"""
|
| 559 |
+
if tcp_header.fin:
|
| 560 |
+
conn.local_ack += 1
|
| 561 |
+
|
| 562 |
+
# Send ACK
|
| 563 |
+
segment = self._create_tcp_segment(conn, 0x10) # ACK flag
|
| 564 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 565 |
+
self._send_packet(packet)
|
| 566 |
+
|
| 567 |
+
conn.state = TCPState.TIME_WAIT
|
| 568 |
+
conn.time_wait_start = time.time()
|
| 569 |
+
|
| 570 |
+
return True
|
| 571 |
+
|
| 572 |
+
def _handle_close_wait(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 573 |
+
"""Handle segment in CLOSE_WAIT state"""
|
| 574 |
+
# Application should close the connection
|
| 575 |
+
return True
|
| 576 |
+
|
| 577 |
+
def _handle_closing(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 578 |
+
"""Handle segment in CLOSING state"""
|
| 579 |
+
if tcp_header.ack:
|
| 580 |
+
self._process_ack(conn, tcp_header.ack_num)
|
| 581 |
+
if not conn.unacked_segments: # Our FIN was ACKed
|
| 582 |
+
conn.state = TCPState.TIME_WAIT
|
| 583 |
+
conn.time_wait_start = time.time()
|
| 584 |
+
|
| 585 |
+
return True
|
| 586 |
+
|
| 587 |
+
def _handle_last_ack(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 588 |
+
"""Handle segment in LAST_ACK state"""
|
| 589 |
+
if tcp_header.ack:
|
| 590 |
+
self._process_ack(conn, tcp_header.ack_num)
|
| 591 |
+
if not conn.unacked_segments: # Our FIN was ACKed
|
| 592 |
+
conn.state = TCPState.CLOSED
|
| 593 |
+
self._close_connection(conn)
|
| 594 |
+
|
| 595 |
+
return True
|
| 596 |
+
|
| 597 |
+
def _handle_time_wait(self, conn: TCPConnection, tcp_header: TCPHeader, payload: bytes) -> bool:
|
| 598 |
+
"""Handle segment in TIME_WAIT state"""
|
| 599 |
+
# Just acknowledge any segments
|
| 600 |
+
if tcp_header.seq_num == conn.local_ack:
|
| 601 |
+
segment = self._create_tcp_segment(conn, 0x10) # ACK flag
|
| 602 |
+
packet = self._build_tcp_packet(conn, segment)
|
| 603 |
+
self._send_packet(packet)
|
| 604 |
+
|
| 605 |
+
return True
|
| 606 |
+
|
| 607 |
+
def _process_ack(self, conn: TCPConnection, ack_num: int):
|
| 608 |
+
"""Process ACK and remove acknowledged segments"""
|
| 609 |
+
acked_segments = []
|
| 610 |
+
acked_bytes = 0
|
| 611 |
+
|
| 612 |
+
for seq_num, segment in list(conn.unacked_segments.items()):
|
| 613 |
+
if seq_num < ack_num:
|
| 614 |
+
acked_segments.append((seq_num, segment))
|
| 615 |
+
acked_bytes += segment.data_length
|
| 616 |
+
del conn.unacked_segments[seq_num]
|
| 617 |
+
|
| 618 |
+
# Update RTT and congestion window
|
| 619 |
+
if acked_segments:
|
| 620 |
+
# Use first acked segment for RTT calculation
|
| 621 |
+
rtt = time.time() - acked_segments[0][1].timestamp
|
| 622 |
+
self._update_rto(conn, rtt)
|
| 623 |
+
self._update_congestion_window(conn, acked_bytes)
|
| 624 |
+
|
| 625 |
+
# Try to send more data
|
| 626 |
+
self._try_send_data(conn)
|
| 627 |
+
|
| 628 |
+
def _send_rst(self, ip_header: IPv4Header, tcp_header: TCPHeader):
|
| 629 |
+
"""Send RST for unknown connection"""
|
| 630 |
+
# Create RST response
|
| 631 |
+
rst_ip = IPv4Header(
|
| 632 |
+
protocol=6,
|
| 633 |
+
source_ip=ip_header.dest_ip,
|
| 634 |
+
dest_ip=ip_header.source_ip,
|
| 635 |
+
ttl=64
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
rst_tcp = TCPHeader(
|
| 639 |
+
source_port=tcp_header.dest_port,
|
| 640 |
+
dest_port=tcp_header.source_port,
|
| 641 |
+
seq_num=tcp_header.ack_num if tcp_header.ack else 0,
|
| 642 |
+
ack_num=tcp_header.seq_num + 1 if tcp_header.syn else tcp_header.seq_num,
|
| 643 |
+
flags=0x14 if tcp_header.ack else 0x04 # RST+ACK or RST
|
| 644 |
+
)
|
| 645 |
+
|
| 646 |
+
packet = IPParser.build_packet(rst_ip, rst_tcp)
|
| 647 |
+
self._send_packet(packet)
|
| 648 |
+
|
| 649 |
+
def _timer_loop(self):
|
| 650 |
+
"""Timer loop for handling timeouts"""
|
| 651 |
+
while self.running:
|
| 652 |
+
current_time = time.time()
|
| 653 |
+
|
| 654 |
+
with self.lock:
|
| 655 |
+
connections_to_check = list(self.connections.values())
|
| 656 |
+
|
| 657 |
+
for conn in connections_to_check:
|
| 658 |
+
# Handle retransmissions
|
| 659 |
+
if conn.unacked_segments:
|
| 660 |
+
self._handle_retransmission(conn)
|
| 661 |
+
|
| 662 |
+
# Handle connection timeout
|
| 663 |
+
if current_time - conn.last_activity > self.connection_timeout:
|
| 664 |
+
self._close_connection(conn, reset=True)
|
| 665 |
+
|
| 666 |
+
# Handle TIME_WAIT timeout
|
| 667 |
+
if (conn.state == TCPState.TIME_WAIT and
|
| 668 |
+
conn.time_wait_start and
|
| 669 |
+
current_time - conn.time_wait_start > self.time_wait_timeout):
|
| 670 |
+
conn.state = TCPState.CLOSED
|
| 671 |
+
self._close_connection(conn)
|
| 672 |
+
|
| 673 |
+
time.sleep(1) # Check every second
|
| 674 |
+
|
| 675 |
+
def start(self):
|
| 676 |
+
"""Start TCP engine"""
|
| 677 |
+
self.running = True
|
| 678 |
+
self.timer_thread = threading.Thread(target=self._timer_loop, daemon=True)
|
| 679 |
+
self.timer_thread.start()
|
| 680 |
+
print("TCP engine started")
|
| 681 |
+
|
| 682 |
+
def stop(self):
|
| 683 |
+
"""Stop TCP engine"""
|
| 684 |
+
self.running = False
|
| 685 |
+
if self.timer_thread:
|
| 686 |
+
self.timer_thread.join()
|
| 687 |
+
|
| 688 |
+
# Close all connections
|
| 689 |
+
with self.lock:
|
| 690 |
+
for conn in list(self.connections.values()):
|
| 691 |
+
self._close_connection(conn, reset=True)
|
| 692 |
+
|
| 693 |
+
print("TCP engine stopped")
|
| 694 |
+
|
| 695 |
+
def get_connections(self) -> Dict[str, Dict]:
|
| 696 |
+
"""Get current connections"""
|
| 697 |
+
with self.lock:
|
| 698 |
+
return {
|
| 699 |
+
conn_id: {
|
| 700 |
+
'local_ip': conn.local_ip,
|
| 701 |
+
'local_port': conn.local_port,
|
| 702 |
+
'remote_ip': conn.remote_ip,
|
| 703 |
+
'remote_port': conn.remote_port,
|
| 704 |
+
'state': conn.state.value,
|
| 705 |
+
'local_seq': conn.local_seq,
|
| 706 |
+
'local_ack': conn.local_ack,
|
| 707 |
+
'remote_seq': conn.remote_seq,
|
| 708 |
+
'remote_ack': conn.remote_ack,
|
| 709 |
+
'window_size': conn.local_window,
|
| 710 |
+
'cwnd': conn.cwnd,
|
| 711 |
+
'unacked_segments': len(conn.unacked_segments),
|
| 712 |
+
'last_activity': conn.last_activity
|
| 713 |
+
}
|
| 714 |
+
for conn_id, conn in self.connections.items()
|
| 715 |
+
}
|
| 716 |
+
|
core/traffic_router.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Traffic Router Module
|
| 3 |
+
|
| 4 |
+
Handles routing of all client traffic through VPN for free data access using async TCP sockets.
|
| 5 |
+
Optimized for performance and scalability.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
import socket
|
| 11 |
+
import logging
|
| 12 |
+
from typing import Dict, Any, Optional
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TrafficRouter:
|
| 18 |
+
"""Manages traffic routing for VPN clients using async TCP sockets"""
|
| 19 |
+
|
| 20 |
+
def __init__(self, config: Dict[str, Any], nat_engine: Any = None):
|
| 21 |
+
self.config = config
|
| 22 |
+
self.is_running = False
|
| 23 |
+
self.vpn_host = self.config.get("vpn_host", "127.0.0.1")
|
| 24 |
+
self.vpn_port = self.config.get("vpn_port", 9000)
|
| 25 |
+
self.internet_host = self.config.get("internet_host", "0.0.0.0")
|
| 26 |
+
self.internet_port = self.config.get("internet_port", 9001)
|
| 27 |
+
self.nat_engine = nat_engine
|
| 28 |
+
self.loop = None
|
| 29 |
+
self.vpn_server = None
|
| 30 |
+
self.internet_server = None
|
| 31 |
+
self.connections = set()
|
| 32 |
+
self.stats = {
|
| 33 |
+
"total_connections": 0,
|
| 34 |
+
"active_connections": 0,
|
| 35 |
+
"bytes_forwarded": 0,
|
| 36 |
+
"errors": 0
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
async def start(self):
|
| 40 |
+
"""Start the traffic router using asyncio TCP servers"""
|
| 41 |
+
if self.is_running:
|
| 42 |
+
logger.warning("Traffic Router is already running")
|
| 43 |
+
return True
|
| 44 |
+
|
| 45 |
+
self.is_running = True
|
| 46 |
+
self.loop = asyncio.get_event_loop()
|
| 47 |
+
self.vpn_server = await asyncio.start_server(
|
| 48 |
+
lambda r, w: self._handle_connection(r, w, "VPN"),
|
| 49 |
+
self.vpn_host, self.vpn_port)
|
| 50 |
+
self.internet_server = await asyncio.start_server(
|
| 51 |
+
lambda r, w: self._handle_connection(r, w, "Internet"),
|
| 52 |
+
self.internet_host, self.internet_port)
|
| 53 |
+
|
| 54 |
+
logger.info(f"Traffic Router started on TCP endpoints: {self.vpn_host}:{self.vpn_port} and {self.internet_host}:{self.internet_port}")
|
| 55 |
+
return True
|
| 56 |
+
|
| 57 |
+
async def stop(self):
|
| 58 |
+
"""Stop the traffic router"""
|
| 59 |
+
logger.info("Stopping Traffic Router...")
|
| 60 |
+
self.is_running = False
|
| 61 |
+
if self.vpn_server:
|
| 62 |
+
self.vpn_server.close()
|
| 63 |
+
await self.vpn_server.wait_closed()
|
| 64 |
+
if self.internet_server:
|
| 65 |
+
self.internet_server.close()
|
| 66 |
+
await self.internet_server.wait_closed()
|
| 67 |
+
for conn in list(self.connections):
|
| 68 |
+
conn.close()
|
| 69 |
+
logger.info("Traffic Router stopped")
|
| 70 |
+
return True
|
| 71 |
+
|
| 72 |
+
async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, source_name: str):
|
| 73 |
+
"""Handle a new connection and forward data asynchronously."""
|
| 74 |
+
peer = writer.get_extra_info("peername")
|
| 75 |
+
logger.info(f"Accepted connection from {peer} on {source_name}")
|
| 76 |
+
self.connections.add(writer)
|
| 77 |
+
try:
|
| 78 |
+
while self.is_running:
|
| 79 |
+
data = await reader.read(4096)
|
| 80 |
+
if not data:
|
| 81 |
+
break
|
| 82 |
+
processed_data = None
|
| 83 |
+
if self.nat_engine:
|
| 84 |
+
if source_name == "VPN":
|
| 85 |
+
processed_data = self.nat_engine.process_outbound_packet(data)
|
| 86 |
+
elif source_name == "Internet":
|
| 87 |
+
processed_data = self.nat_engine.process_inbound_packet(data)
|
| 88 |
+
if processed_data:
|
| 89 |
+
await self._forward_data(processed_data, source_name)
|
| 90 |
+
elif not self.nat_engine:
|
| 91 |
+
await self._forward_data(data, source_name)
|
| 92 |
+
self.stats["bytes_forwarded"] += len(data)
|
| 93 |
+
except Exception as e:
|
| 94 |
+
self.stats["errors"] += 1
|
| 95 |
+
logger.error(f"Error in {source_name} connection: {e}")
|
| 96 |
+
finally:
|
| 97 |
+
writer.close()
|
| 98 |
+
await writer.wait_closed()
|
| 99 |
+
self.connections.discard(writer)
|
| 100 |
+
|
| 101 |
+
async def _forward_data(self, data: bytes, source_name: str):
|
| 102 |
+
"""Forward data to the opposite endpoint."""
|
| 103 |
+
# This is a placeholder for actual forwarding logic.
|
| 104 |
+
# You may want to implement connection pooling or load balancing here.
|
| 105 |
+
# For demo, just log the forwarding event.
|
| 106 |
+
logger.debug(f"Forwarded {len(data)} bytes from {source_name}")
|
| 107 |
+
|
| 108 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 109 |
+
"""Get traffic router statistics"""
|
| 110 |
+
return {
|
| 111 |
+
"is_running": self.is_running,
|
| 112 |
+
"vpn_host": self.vpn_host,
|
| 113 |
+
"vpn_port": self.vpn_port,
|
| 114 |
+
"internet_host": self.internet_host,
|
| 115 |
+
"internet_port": self.internet_port,
|
| 116 |
+
"total_connections": self.stats["total_connections"],
|
| 117 |
+
"active_connections": len(self.connections),
|
| 118 |
+
"bytes_forwarded": self.stats["bytes_forwarded"],
|
| 119 |
+
"errors": self.stats["errors"]
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def set_components(self, nat_engine: Any = None):
|
| 128 |
+
"""Set references to other components for inter-operation."""
|
| 129 |
+
if nat_engine:
|
| 130 |
+
self.nat_engine = nat_engine
|
| 131 |
+
|
| 132 |
+
|
core/virtual_router.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Virtual Router Module
|
| 3 |
+
|
| 4 |
+
Implements packet routing between virtual clients and external internet:
|
| 5 |
+
- Maintain routing table for virtual network
|
| 6 |
+
- Forward packets based on destination IP
|
| 7 |
+
- Handle internal vs external routing decisions
|
| 8 |
+
- Support static route configuration
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import ipaddress
|
| 12 |
+
import time
|
| 13 |
+
import threading
|
| 14 |
+
from typing import Dict, List, Optional, Tuple, Set
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from enum import Enum
|
| 17 |
+
|
| 18 |
+
from .ip_parser import ParsedPacket, IPv4Header
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class RouteType(Enum):
|
| 22 |
+
DIRECT = "DIRECT" # Directly connected network
|
| 23 |
+
STATIC = "STATIC" # Static route
|
| 24 |
+
DEFAULT = "DEFAULT" # Default route
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class RouteEntry:
|
| 29 |
+
"""Represents a routing table entry"""
|
| 30 |
+
destination: str # Network in CIDR notation (e.g., "10.0.0.0/24")
|
| 31 |
+
gateway: Optional[str] # Next hop IP (None for direct routes)
|
| 32 |
+
interface: str # Interface name or identifier
|
| 33 |
+
metric: int # Route metric (lower is preferred)
|
| 34 |
+
route_type: RouteType
|
| 35 |
+
created_time: float
|
| 36 |
+
last_used: Optional[float] = None
|
| 37 |
+
use_count: int = 0
|
| 38 |
+
|
| 39 |
+
def __post_init__(self):
|
| 40 |
+
if self.created_time == 0:
|
| 41 |
+
self.created_time = time.time()
|
| 42 |
+
|
| 43 |
+
def record_use(self):
|
| 44 |
+
"""Record route usage"""
|
| 45 |
+
self.use_count += 1
|
| 46 |
+
self.last_used = time.time()
|
| 47 |
+
|
| 48 |
+
def matches_destination(self, ip: str) -> bool:
|
| 49 |
+
"""Check if this route matches the destination IP"""
|
| 50 |
+
try:
|
| 51 |
+
network = ipaddress.ip_network(self.destination, strict=False)
|
| 52 |
+
return ipaddress.ip_address(ip) in network
|
| 53 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 54 |
+
return False
|
| 55 |
+
|
| 56 |
+
def to_dict(self) -> Dict:
|
| 57 |
+
"""Convert route to dictionary"""
|
| 58 |
+
return {
|
| 59 |
+
'destination': self.destination,
|
| 60 |
+
'gateway': self.gateway,
|
| 61 |
+
'interface': self.interface,
|
| 62 |
+
'metric': self.metric,
|
| 63 |
+
'route_type': self.route_type.value,
|
| 64 |
+
'created_time': self.created_time,
|
| 65 |
+
'last_used': self.last_used,
|
| 66 |
+
'use_count': self.use_count
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass
|
| 71 |
+
class Interface:
|
| 72 |
+
"""Represents a network interface"""
|
| 73 |
+
name: str
|
| 74 |
+
ip_address: str
|
| 75 |
+
netmask: str
|
| 76 |
+
network: str # Network in CIDR notation
|
| 77 |
+
enabled: bool = True
|
| 78 |
+
mtu: int = 1500
|
| 79 |
+
created_time: float = 0
|
| 80 |
+
|
| 81 |
+
def __post_init__(self):
|
| 82 |
+
if self.created_time == 0:
|
| 83 |
+
self.created_time = time.time()
|
| 84 |
+
|
| 85 |
+
# Calculate network if not provided
|
| 86 |
+
if not self.network:
|
| 87 |
+
try:
|
| 88 |
+
interface_network = ipaddress.ip_interface(f"{self.ip_address}/{self.netmask}")
|
| 89 |
+
self.network = str(interface_network.network)
|
| 90 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 91 |
+
self.network = "0.0.0.0/0"
|
| 92 |
+
|
| 93 |
+
def is_local_address(self, ip: str) -> bool:
|
| 94 |
+
"""Check if IP address belongs to this interface's network"""
|
| 95 |
+
try:
|
| 96 |
+
network = ipaddress.ip_network(self.network, strict=False)
|
| 97 |
+
return ipaddress.ip_address(ip) in network
|
| 98 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
def to_dict(self) -> Dict:
|
| 102 |
+
"""Convert interface to dictionary"""
|
| 103 |
+
return {
|
| 104 |
+
'name': self.name,
|
| 105 |
+
'ip_address': self.ip_address,
|
| 106 |
+
'netmask': self.netmask,
|
| 107 |
+
'network': self.network,
|
| 108 |
+
'enabled': self.enabled,
|
| 109 |
+
'mtu': self.mtu,
|
| 110 |
+
'created_time': self.created_time
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class VirtualRouter:
|
| 115 |
+
"""Virtual router implementation"""
|
| 116 |
+
|
| 117 |
+
def __init__(self, config: Dict):
|
| 118 |
+
self.config = config
|
| 119 |
+
self.routing_table: List[RouteEntry] = []
|
| 120 |
+
self.interfaces: Dict[str, Interface] = {}
|
| 121 |
+
self.arp_table: Dict[str, str] = {} # IP -> MAC mapping
|
| 122 |
+
self.lock = threading.Lock()
|
| 123 |
+
|
| 124 |
+
# Router configuration
|
| 125 |
+
self.router_id = config.get('router_id', 'virtual-router-1')
|
| 126 |
+
self.default_gateway = config.get('default_gateway')
|
| 127 |
+
|
| 128 |
+
# Statistics
|
| 129 |
+
self.stats = {
|
| 130 |
+
'packets_routed': 0,
|
| 131 |
+
'packets_dropped': 0,
|
| 132 |
+
'route_lookups': 0,
|
| 133 |
+
'arp_requests': 0,
|
| 134 |
+
'arp_replies': 0,
|
| 135 |
+
'routing_errors': 0
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
# Initialize interfaces and routes
|
| 139 |
+
self._initialize_interfaces()
|
| 140 |
+
self._initialize_routes()
|
| 141 |
+
|
| 142 |
+
def _initialize_interfaces(self):
|
| 143 |
+
"""Initialize network interfaces from configuration"""
|
| 144 |
+
interfaces_config = self.config.get('interfaces', [])
|
| 145 |
+
|
| 146 |
+
for iface_config in interfaces_config:
|
| 147 |
+
interface = Interface(
|
| 148 |
+
name=iface_config['name'],
|
| 149 |
+
ip_address=iface_config['ip_address'],
|
| 150 |
+
netmask=iface_config.get('netmask', '255.255.255.0'),
|
| 151 |
+
network=iface_config.get('network'),
|
| 152 |
+
enabled=iface_config.get('enabled', True),
|
| 153 |
+
mtu=iface_config.get('mtu', 1500)
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
with self.lock:
|
| 157 |
+
self.interfaces[interface.name] = interface
|
| 158 |
+
|
| 159 |
+
# Add direct route for interface network
|
| 160 |
+
self.add_route(
|
| 161 |
+
destination=interface.network,
|
| 162 |
+
gateway=None,
|
| 163 |
+
interface=interface.name,
|
| 164 |
+
metric=0,
|
| 165 |
+
route_type=RouteType.DIRECT
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
def _initialize_routes(self):
|
| 169 |
+
"""Initialize static routes from configuration"""
|
| 170 |
+
routes_config = self.config.get('static_routes', [])
|
| 171 |
+
|
| 172 |
+
for route_config in routes_config:
|
| 173 |
+
self.add_route(
|
| 174 |
+
destination=route_config['destination'],
|
| 175 |
+
gateway=route_config.get('gateway'),
|
| 176 |
+
interface=route_config['interface'],
|
| 177 |
+
metric=route_config.get('metric', 10),
|
| 178 |
+
route_type=RouteType.STATIC
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Add default route if configured
|
| 182 |
+
if self.default_gateway:
|
| 183 |
+
# Find interface for default gateway
|
| 184 |
+
default_interface = None
|
| 185 |
+
for interface in self.interfaces.values():
|
| 186 |
+
if interface.is_local_address(self.default_gateway):
|
| 187 |
+
default_interface = interface.name
|
| 188 |
+
break
|
| 189 |
+
|
| 190 |
+
if default_interface:
|
| 191 |
+
self.add_route(
|
| 192 |
+
destination="0.0.0.0/0",
|
| 193 |
+
gateway=self.default_gateway,
|
| 194 |
+
interface=default_interface,
|
| 195 |
+
metric=100,
|
| 196 |
+
route_type=RouteType.DEFAULT
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
def add_interface(self, name: str, ip_address: str, netmask: str = "255.255.255.0",
|
| 200 |
+
network: Optional[str] = None, mtu: int = 1500) -> bool:
|
| 201 |
+
"""Add network interface"""
|
| 202 |
+
with self.lock:
|
| 203 |
+
if name in self.interfaces:
|
| 204 |
+
return False
|
| 205 |
+
|
| 206 |
+
interface = Interface(
|
| 207 |
+
name=name,
|
| 208 |
+
ip_address=ip_address,
|
| 209 |
+
netmask=netmask,
|
| 210 |
+
network=network,
|
| 211 |
+
mtu=mtu
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
self.interfaces[name] = interface
|
| 215 |
+
|
| 216 |
+
# Add direct route for interface network
|
| 217 |
+
self.add_route(
|
| 218 |
+
destination=interface.network,
|
| 219 |
+
gateway=None,
|
| 220 |
+
interface=name,
|
| 221 |
+
metric=0,
|
| 222 |
+
route_type=RouteType.DIRECT
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
return True
|
| 226 |
+
|
| 227 |
+
def remove_interface(self, name: str) -> bool:
|
| 228 |
+
"""Remove network interface"""
|
| 229 |
+
with self.lock:
|
| 230 |
+
if name not in self.interfaces:
|
| 231 |
+
return False
|
| 232 |
+
|
| 233 |
+
# Remove interface
|
| 234 |
+
del self.interfaces[name]
|
| 235 |
+
|
| 236 |
+
# Remove routes associated with this interface
|
| 237 |
+
self.routing_table = [
|
| 238 |
+
route for route in self.routing_table
|
| 239 |
+
if route.interface != name
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
return True
|
| 243 |
+
|
| 244 |
+
def enable_interface(self, name: str) -> bool:
|
| 245 |
+
"""Enable network interface"""
|
| 246 |
+
with self.lock:
|
| 247 |
+
if name in self.interfaces:
|
| 248 |
+
self.interfaces[name].enabled = True
|
| 249 |
+
return True
|
| 250 |
+
return False
|
| 251 |
+
|
| 252 |
+
def disable_interface(self, name: str) -> bool:
|
| 253 |
+
"""Disable network interface"""
|
| 254 |
+
with self.lock:
|
| 255 |
+
if name in self.interfaces:
|
| 256 |
+
self.interfaces[name].enabled = False
|
| 257 |
+
return True
|
| 258 |
+
return False
|
| 259 |
+
|
| 260 |
+
def add_route(self, destination: str, gateway: Optional[str], interface: str,
|
| 261 |
+
metric: int = 10, route_type: RouteType = RouteType.STATIC) -> bool:
|
| 262 |
+
"""Add route to routing table"""
|
| 263 |
+
try:
|
| 264 |
+
# Validate destination network
|
| 265 |
+
ipaddress.ip_network(destination, strict=False)
|
| 266 |
+
|
| 267 |
+
# Validate gateway if provided
|
| 268 |
+
if gateway:
|
| 269 |
+
ipaddress.ip_address(gateway)
|
| 270 |
+
|
| 271 |
+
route = RouteEntry(
|
| 272 |
+
destination=destination,
|
| 273 |
+
gateway=gateway,
|
| 274 |
+
interface=interface,
|
| 275 |
+
metric=metric,
|
| 276 |
+
route_type=route_type,
|
| 277 |
+
created_time=time.time()
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
with self.lock:
|
| 281 |
+
# Check if interface exists
|
| 282 |
+
if interface not in self.interfaces:
|
| 283 |
+
return False
|
| 284 |
+
|
| 285 |
+
# Remove existing route with same destination and interface
|
| 286 |
+
self.routing_table = [
|
| 287 |
+
r for r in self.routing_table
|
| 288 |
+
if not (r.destination == destination and r.interface == interface)
|
| 289 |
+
]
|
| 290 |
+
|
| 291 |
+
# Add new route
|
| 292 |
+
self.routing_table.append(route)
|
| 293 |
+
|
| 294 |
+
# Sort by metric (lower metric = higher priority)
|
| 295 |
+
self.routing_table.sort(key=lambda r: (r.metric, r.created_time))
|
| 296 |
+
|
| 297 |
+
return True
|
| 298 |
+
|
| 299 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 300 |
+
return False
|
| 301 |
+
|
| 302 |
+
def remove_route(self, destination: str, interface: str) -> bool:
|
| 303 |
+
"""Remove route from routing table"""
|
| 304 |
+
with self.lock:
|
| 305 |
+
original_count = len(self.routing_table)
|
| 306 |
+
self.routing_table = [
|
| 307 |
+
route for route in self.routing_table
|
| 308 |
+
if not (route.destination == destination and route.interface == interface)
|
| 309 |
+
]
|
| 310 |
+
return len(self.routing_table) < original_count
|
| 311 |
+
|
| 312 |
+
def lookup_route(self, destination_ip: str) -> Optional[RouteEntry]:
|
| 313 |
+
"""Look up route for destination IP"""
|
| 314 |
+
self.stats['route_lookups'] += 1
|
| 315 |
+
|
| 316 |
+
with self.lock:
|
| 317 |
+
# Find all matching routes
|
| 318 |
+
matching_routes = []
|
| 319 |
+
for route in self.routing_table:
|
| 320 |
+
# Skip disabled interfaces
|
| 321 |
+
interface = self.interfaces.get(route.interface)
|
| 322 |
+
if not interface or not interface.enabled:
|
| 323 |
+
continue
|
| 324 |
+
|
| 325 |
+
if route.matches_destination(destination_ip):
|
| 326 |
+
matching_routes.append(route)
|
| 327 |
+
|
| 328 |
+
if not matching_routes:
|
| 329 |
+
self.stats['routing_errors'] += 1
|
| 330 |
+
return None
|
| 331 |
+
|
| 332 |
+
# Sort by specificity (longest prefix match) and then by metric
|
| 333 |
+
def route_priority(route):
|
| 334 |
+
try:
|
| 335 |
+
network = ipaddress.ip_network(route.destination, strict=False)
|
| 336 |
+
return (-network.prefixlen, route.metric, route.created_time)
|
| 337 |
+
except:
|
| 338 |
+
return (0, route.metric, route.created_time)
|
| 339 |
+
|
| 340 |
+
matching_routes.sort(key=route_priority)
|
| 341 |
+
best_route = matching_routes[0]
|
| 342 |
+
best_route.record_use()
|
| 343 |
+
|
| 344 |
+
return best_route
|
| 345 |
+
|
| 346 |
+
def route_packet(self, packet: ParsedPacket) -> Optional[Tuple[str, str]]:
|
| 347 |
+
"""Route packet and return (next_hop_ip, interface)"""
|
| 348 |
+
self.stats['packets_routed'] += 1
|
| 349 |
+
|
| 350 |
+
destination_ip = packet.ip_header.dest_ip
|
| 351 |
+
|
| 352 |
+
# Look up route
|
| 353 |
+
route = self.lookup_route(destination_ip)
|
| 354 |
+
if not route:
|
| 355 |
+
self.stats['packets_dropped'] += 1
|
| 356 |
+
return None
|
| 357 |
+
|
| 358 |
+
# Determine next hop
|
| 359 |
+
if route.gateway:
|
| 360 |
+
next_hop = route.gateway
|
| 361 |
+
else:
|
| 362 |
+
# Direct route - destination is next hop
|
| 363 |
+
next_hop = destination_ip
|
| 364 |
+
|
| 365 |
+
return (next_hop, route.interface)
|
| 366 |
+
|
| 367 |
+
def is_local_destination(self, ip: str) -> bool:
|
| 368 |
+
"""Check if IP is a local destination (belongs to router interfaces)"""
|
| 369 |
+
with self.lock:
|
| 370 |
+
for interface in self.interfaces.values():
|
| 371 |
+
if interface.ip_address == ip:
|
| 372 |
+
return True
|
| 373 |
+
return False
|
| 374 |
+
|
| 375 |
+
def is_local_network(self, ip: str) -> bool:
|
| 376 |
+
"""Check if IP belongs to any local network"""
|
| 377 |
+
with self.lock:
|
| 378 |
+
for interface in self.interfaces.values():
|
| 379 |
+
if interface.is_local_address(ip):
|
| 380 |
+
return True
|
| 381 |
+
return False
|
| 382 |
+
|
| 383 |
+
def get_interface_for_ip(self, ip: str) -> Optional[Interface]:
|
| 384 |
+
"""Get interface that can reach the given IP"""
|
| 385 |
+
with self.lock:
|
| 386 |
+
for interface in self.interfaces.values():
|
| 387 |
+
if interface.enabled and interface.is_local_address(ip):
|
| 388 |
+
return interface
|
| 389 |
+
return None
|
| 390 |
+
|
| 391 |
+
def add_arp_entry(self, ip: str, mac: str):
|
| 392 |
+
"""Add ARP table entry"""
|
| 393 |
+
with self.lock:
|
| 394 |
+
self.arp_table[ip] = mac
|
| 395 |
+
|
| 396 |
+
def get_arp_entry(self, ip: str) -> Optional[str]:
|
| 397 |
+
"""Get MAC address from ARP table"""
|
| 398 |
+
with self.lock:
|
| 399 |
+
return self.arp_table.get(ip)
|
| 400 |
+
|
| 401 |
+
def remove_arp_entry(self, ip: str) -> bool:
|
| 402 |
+
"""Remove ARP table entry"""
|
| 403 |
+
with self.lock:
|
| 404 |
+
if ip in self.arp_table:
|
| 405 |
+
del self.arp_table[ip]
|
| 406 |
+
return True
|
| 407 |
+
return False
|
| 408 |
+
|
| 409 |
+
def clear_arp_table(self):
|
| 410 |
+
"""Clear ARP table"""
|
| 411 |
+
with self.lock:
|
| 412 |
+
self.arp_table.clear()
|
| 413 |
+
|
| 414 |
+
def get_routing_table(self) -> List[Dict]:
|
| 415 |
+
"""Get routing table"""
|
| 416 |
+
with self.lock:
|
| 417 |
+
return [route.to_dict() for route in self.routing_table]
|
| 418 |
+
|
| 419 |
+
def get_interfaces(self) -> Dict[str, Dict]:
|
| 420 |
+
"""Get network interfaces"""
|
| 421 |
+
with self.lock:
|
| 422 |
+
return {
|
| 423 |
+
name: interface.to_dict()
|
| 424 |
+
for name, interface in self.interfaces.items()
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
def get_arp_table(self) -> Dict[str, str]:
|
| 428 |
+
"""Get ARP table"""
|
| 429 |
+
with self.lock:
|
| 430 |
+
return self.arp_table.copy()
|
| 431 |
+
|
| 432 |
+
def get_stats(self) -> Dict:
|
| 433 |
+
"""Get router statistics"""
|
| 434 |
+
with self.lock:
|
| 435 |
+
stats = self.stats.copy()
|
| 436 |
+
stats['total_routes'] = len(self.routing_table)
|
| 437 |
+
stats['total_interfaces'] = len(self.interfaces)
|
| 438 |
+
stats['enabled_interfaces'] = sum(1 for iface in self.interfaces.values() if iface.enabled)
|
| 439 |
+
stats['arp_entries'] = len(self.arp_table)
|
| 440 |
+
|
| 441 |
+
return stats
|
| 442 |
+
|
| 443 |
+
def reset_stats(self):
|
| 444 |
+
"""Reset router statistics"""
|
| 445 |
+
self.stats = {
|
| 446 |
+
'packets_routed': 0,
|
| 447 |
+
'packets_dropped': 0,
|
| 448 |
+
'route_lookups': 0,
|
| 449 |
+
'arp_requests': 0,
|
| 450 |
+
'arp_replies': 0,
|
| 451 |
+
'routing_errors': 0
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
# Reset route usage statistics
|
| 455 |
+
with self.lock:
|
| 456 |
+
for route in self.routing_table:
|
| 457 |
+
route.use_count = 0
|
| 458 |
+
route.last_used = None
|
| 459 |
+
|
| 460 |
+
def flush_routes(self, route_type: Optional[RouteType] = None):
|
| 461 |
+
"""Flush routes of specified type (or all if None)"""
|
| 462 |
+
with self.lock:
|
| 463 |
+
if route_type:
|
| 464 |
+
self.routing_table = [
|
| 465 |
+
route for route in self.routing_table
|
| 466 |
+
if route.route_type != route_type
|
| 467 |
+
]
|
| 468 |
+
else:
|
| 469 |
+
self.routing_table.clear()
|
| 470 |
+
|
| 471 |
+
def export_config(self) -> Dict:
|
| 472 |
+
"""Export router configuration"""
|
| 473 |
+
return {
|
| 474 |
+
'router_id': self.router_id,
|
| 475 |
+
'default_gateway': self.default_gateway,
|
| 476 |
+
'interfaces': [
|
| 477 |
+
{
|
| 478 |
+
'name': iface.name,
|
| 479 |
+
'ip_address': iface.ip_address,
|
| 480 |
+
'netmask': iface.netmask,
|
| 481 |
+
'network': iface.network,
|
| 482 |
+
'enabled': iface.enabled,
|
| 483 |
+
'mtu': iface.mtu
|
| 484 |
+
}
|
| 485 |
+
for iface in self.interfaces.values()
|
| 486 |
+
],
|
| 487 |
+
'static_routes': [
|
| 488 |
+
{
|
| 489 |
+
'destination': route.destination,
|
| 490 |
+
'gateway': route.gateway,
|
| 491 |
+
'interface': route.interface,
|
| 492 |
+
'metric': route.metric
|
| 493 |
+
}
|
| 494 |
+
for route in self.routing_table
|
| 495 |
+
if route.route_type == RouteType.STATIC
|
| 496 |
+
]
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
def import_config(self, config: Dict):
|
| 500 |
+
"""Import router configuration"""
|
| 501 |
+
# Clear existing configuration
|
| 502 |
+
with self.lock:
|
| 503 |
+
self.interfaces.clear()
|
| 504 |
+
self.routing_table.clear()
|
| 505 |
+
self.arp_table.clear()
|
| 506 |
+
|
| 507 |
+
# Update router settings
|
| 508 |
+
self.router_id = config.get('router_id', self.router_id)
|
| 509 |
+
self.default_gateway = config.get('default_gateway', self.default_gateway)
|
| 510 |
+
|
| 511 |
+
# Reinitialize from new config
|
| 512 |
+
self.config.update(config)
|
| 513 |
+
self._initialize_interfaces()
|
| 514 |
+
self._initialize_routes()
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
class RouterUtils:
|
| 518 |
+
"""Utility functions for router operations"""
|
| 519 |
+
|
| 520 |
+
@staticmethod
|
| 521 |
+
def ip_to_int(ip: str) -> int:
|
| 522 |
+
"""Convert IP address to integer"""
|
| 523 |
+
return int(ipaddress.ip_address(ip))
|
| 524 |
+
|
| 525 |
+
@staticmethod
|
| 526 |
+
def int_to_ip(ip_int: int) -> str:
|
| 527 |
+
"""Convert integer to IP address"""
|
| 528 |
+
return str(ipaddress.ip_address(ip_int))
|
| 529 |
+
|
| 530 |
+
@staticmethod
|
| 531 |
+
def calculate_network(ip: str, netmask: str) -> str:
|
| 532 |
+
"""Calculate network address from IP and netmask"""
|
| 533 |
+
try:
|
| 534 |
+
interface = ipaddress.ip_interface(f"{ip}/{netmask}")
|
| 535 |
+
return str(interface.network)
|
| 536 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 537 |
+
return "0.0.0.0/0"
|
| 538 |
+
|
| 539 |
+
@staticmethod
|
| 540 |
+
def is_private_ip(ip: str) -> bool:
|
| 541 |
+
"""Check if IP address is private"""
|
| 542 |
+
try:
|
| 543 |
+
ip_obj = ipaddress.ip_address(ip)
|
| 544 |
+
return ip_obj.is_private
|
| 545 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 546 |
+
return False
|
| 547 |
+
|
| 548 |
+
@staticmethod
|
| 549 |
+
def is_multicast_ip(ip: str) -> bool:
|
| 550 |
+
"""Check if IP address is multicast"""
|
| 551 |
+
try:
|
| 552 |
+
ip_obj = ipaddress.ip_address(ip)
|
| 553 |
+
return ip_obj.is_multicast
|
| 554 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 555 |
+
return False
|
| 556 |
+
|
| 557 |
+
@staticmethod
|
| 558 |
+
def validate_cidr(cidr: str) -> bool:
|
| 559 |
+
"""Validate CIDR notation"""
|
| 560 |
+
try:
|
| 561 |
+
ipaddress.ip_network(cidr, strict=False)
|
| 562 |
+
return True
|
| 563 |
+
except (ipaddress.AddressValueError, ValueError):
|
| 564 |
+
return False
|
| 565 |
+
|
database/app.db
ADDED
|
Binary file (45.1 kB). View file
|
|
|
flask_app.log
ADDED
|
Binary file (2.14 kB). View file
|
|
|
main.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
# DON\'T CHANGE THIS !!!
|
| 4 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
| 5 |
+
|
| 6 |
+
from flask import Flask, send_from_directory
|
| 7 |
+
from models.user import db
|
| 8 |
+
from routes.user import user_bp
|
| 9 |
+
from routes.auth import auth_bp
|
| 10 |
+
from routes.isp_api import init_engines, isp_api
|
| 11 |
+
from core.openvpn_manager import initialize_openvpn_manager
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), 'static'))
|
| 15 |
+
app.config['SECRET_KEY'] = 'asdf#FGSgvasgf$5$WGT'
|
| 16 |
+
|
| 17 |
+
app.register_blueprint(user_bp, url_prefix='/api')
|
| 18 |
+
app.register_blueprint(isp_api, url_prefix='/api')
|
| 19 |
+
app.register_blueprint(auth_bp, url_prefix='/api')
|
| 20 |
+
|
| 21 |
+
# uncomment if you need to use database
|
| 22 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.path.dirname(__file__), 'database', 'app.db')}"
|
| 23 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 24 |
+
db.init_app(app)
|
| 25 |
+
|
| 26 |
+
with app.app_context():
|
| 27 |
+
db.create_all()
|
| 28 |
+
|
| 29 |
+
# Default configuration for engines
|
| 30 |
+
app.config["dhcp"] = {
|
| 31 |
+
"network": "10.0.0.0/24",
|
| 32 |
+
"range_start": "10.0.0.10",
|
| 33 |
+
"range_end": "10.0.0.100",
|
| 34 |
+
"lease_time": 3600,
|
| 35 |
+
"gateway": "10.0.0.1",
|
| 36 |
+
"dns_servers": ["8.8.8.8", "8.8.4.4"]
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
# Initialize engines only once, when the Flask app is not in debug mode's reloader process
|
| 40 |
+
if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
| 41 |
+
init_engines(app.config)
|
| 42 |
+
initialize_openvpn_manager(app.config)
|
| 43 |
+
|
| 44 |
+
@app.route("/auth")
|
| 45 |
+
def serve_auth():
|
| 46 |
+
return send_from_directory(app.static_folder, "auth.html")
|
| 47 |
+
|
| 48 |
+
@app.route("/dashboard")
|
| 49 |
+
def serve_dashboard():
|
| 50 |
+
return send_from_directory(app.static_folder, "dashboard.html")
|
| 51 |
+
|
| 52 |
+
@app.route("/")
|
| 53 |
+
def serve_root():
|
| 54 |
+
return send_from_directory(app.static_folder, "index.html")
|
| 55 |
+
|
| 56 |
+
@app.route('/<path:path>')
|
| 57 |
+
def serve(path):
|
| 58 |
+
static_folder_path = app.static_folder
|
| 59 |
+
if static_folder_path is None:
|
| 60 |
+
return "Static folder not configured", 404
|
| 61 |
+
|
| 62 |
+
if path != "" and os.path.exists(os.path.join(static_folder_path, path)):
|
| 63 |
+
return send_from_directory(static_folder_path, path)
|
| 64 |
+
else:
|
| 65 |
+
index_path = os.path.join(static_folder_path, 'index.html')
|
| 66 |
+
if os.path.exists(index_path):
|
| 67 |
+
return send_from_directory(static_folder_path, 'index.html')
|
| 68 |
+
else:
|
| 69 |
+
return "index.html not found", 404
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if __name__ == '__main__':
|
| 73 |
+
app.run(host='0.0.0.0', port=5000, debug=False)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
main_isp.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main ISP Application
|
| 3 |
+
|
| 4 |
+
Integrates all core modules and provides the main application entry point
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import json
|
| 10 |
+
import threading
|
| 11 |
+
import time
|
| 12 |
+
from flask import Flask
|
| 13 |
+
from flask_cors import CORS
|
| 14 |
+
|
| 15 |
+
# Add project root to path
|
| 16 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
| 17 |
+
|
| 18 |
+
# Import routes and core modules
|
| 19 |
+
from routes.isp_api import isp_api, init_engines
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def load_config():
|
| 23 |
+
"""Load configuration from file or use defaults"""
|
| 24 |
+
config_file = os.path.join(os.path.dirname(__file__), 'config.json')
|
| 25 |
+
|
| 26 |
+
default_config = {
|
| 27 |
+
"dhcp": {
|
| 28 |
+
"network": "10.0.0.0/24",
|
| 29 |
+
"range_start": "10.0.0.10",
|
| 30 |
+
"range_end": "10.0.0.100",
|
| 31 |
+
"lease_time": 3600,
|
| 32 |
+
"gateway": "10.0.0.1",
|
| 33 |
+
"dns_servers": ["8.8.8.8", "8.8.4.4"]
|
| 34 |
+
},
|
| 35 |
+
"nat": {
|
| 36 |
+
"port_range_start": 10000,
|
| 37 |
+
"port_range_end": 65535,
|
| 38 |
+
"session_timeout": 300,
|
| 39 |
+
"host_ip": "0.0.0.0"
|
| 40 |
+
},
|
| 41 |
+
"firewall": {
|
| 42 |
+
"default_policy": "ACCEPT",
|
| 43 |
+
"log_blocked": True,
|
| 44 |
+
"log_accepted": False,
|
| 45 |
+
"max_log_entries": 10000,
|
| 46 |
+
"rules": [
|
| 47 |
+
{
|
| 48 |
+
"rule_id": "allow_dhcp",
|
| 49 |
+
"priority": 1,
|
| 50 |
+
"action": "ACCEPT",
|
| 51 |
+
"direction": "BOTH",
|
| 52 |
+
"dest_port": "67,68",
|
| 53 |
+
"protocol": "UDP",
|
| 54 |
+
"description": "Allow DHCP traffic",
|
| 55 |
+
"enabled": True
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"rule_id": "allow_dns",
|
| 59 |
+
"priority": 2,
|
| 60 |
+
"action": "ACCEPT",
|
| 61 |
+
"direction": "BOTH",
|
| 62 |
+
"dest_port": "53",
|
| 63 |
+
"protocol": "UDP",
|
| 64 |
+
"description": "Allow DNS traffic",
|
| 65 |
+
"enabled": True
|
| 66 |
+
}
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
"tcp": {
|
| 70 |
+
"initial_window": 65535,
|
| 71 |
+
"max_retries": 3,
|
| 72 |
+
"timeout": 300,
|
| 73 |
+
"time_wait_timeout": 120,
|
| 74 |
+
"mss": 1460
|
| 75 |
+
},
|
| 76 |
+
"router": {
|
| 77 |
+
"router_id": "virtual-isp-router",
|
| 78 |
+
"default_gateway": "10.0.0.1",
|
| 79 |
+
"interfaces": [
|
| 80 |
+
{
|
| 81 |
+
"name": "virtual0",
|
| 82 |
+
"ip_address": "10.0.0.1",
|
| 83 |
+
"netmask": "255.255.255.0",
|
| 84 |
+
"enabled": True,
|
| 85 |
+
"mtu": 1500
|
| 86 |
+
}
|
| 87 |
+
],
|
| 88 |
+
"static_routes": []
|
| 89 |
+
},
|
| 90 |
+
"socket_translator": {
|
| 91 |
+
"connect_timeout": 10,
|
| 92 |
+
"read_timeout": 30,
|
| 93 |
+
"max_connections": 1000,
|
| 94 |
+
"buffer_size": 8192
|
| 95 |
+
},
|
| 96 |
+
"packet_bridge": {
|
| 97 |
+
"websocket_host": "0.0.0.0",
|
| 98 |
+
"websocket_port": 8765,
|
| 99 |
+
"tcp_host": "0.0.0.0",
|
| 100 |
+
"tcp_port": 8766,
|
| 101 |
+
"max_clients": 100,
|
| 102 |
+
"client_timeout": 300
|
| 103 |
+
},
|
| 104 |
+
"session_tracker": {
|
| 105 |
+
"max_sessions": 10000,
|
| 106 |
+
"session_timeout": 3600,
|
| 107 |
+
"cleanup_interval": 300,
|
| 108 |
+
"metrics_retention": 86400
|
| 109 |
+
},
|
| 110 |
+
"logger": {
|
| 111 |
+
"log_level": "INFO",
|
| 112 |
+
"log_to_file": True,
|
| 113 |
+
"log_file_path": "/tmp/virtual_isp.log",
|
| 114 |
+
"log_file_max_size": 10485760,
|
| 115 |
+
"log_file_backup_count": 5,
|
| 116 |
+
"log_to_console": True,
|
| 117 |
+
"structured_logging": True,
|
| 118 |
+
"max_memory_logs": 10000
|
| 119 |
+
},
|
| 120 |
+
"openvpn": {
|
| 121 |
+
"server_config_path": "/etc/openvpn/server/server.conf",
|
| 122 |
+
"ca_cert_path": "/etc/openvpn/server/ca.crt",
|
| 123 |
+
"server_cert_path": "/etc/openvpn/server/server.crt",
|
| 124 |
+
"server_key_path": "/etc/openvpn/server/server.key",
|
| 125 |
+
"dh_path": "/etc/openvpn/server/dh.pem",
|
| 126 |
+
"vpn_network": "10.8.0.0/24",
|
| 127 |
+
"vpn_server_ip": "10.8.0.1",
|
| 128 |
+
"vpn_port": 1194,
|
| 129 |
+
"protocol": "udp",
|
| 130 |
+
"auto_start": False,
|
| 131 |
+
"client_to_client": False,
|
| 132 |
+
"push_routes": [
|
| 133 |
+
"redirect-gateway def1 bypass-dhcp",
|
| 134 |
+
"dhcp-option DNS 8.8.8.8",
|
| 135 |
+
"dhcp-option DNS 8.8.4.4"
|
| 136 |
+
]
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
if os.path.exists(config_file):
|
| 141 |
+
try:
|
| 142 |
+
with open(config_file, 'r') as f:
|
| 143 |
+
file_config = json.load(f)
|
| 144 |
+
|
| 145 |
+
# Merge with defaults
|
| 146 |
+
def merge_config(default, override):
|
| 147 |
+
result = default.copy()
|
| 148 |
+
for key, value in override.items():
|
| 149 |
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
| 150 |
+
result[key] = merge_config(result[key], value)
|
| 151 |
+
else:
|
| 152 |
+
result[key] = value
|
| 153 |
+
return result
|
| 154 |
+
|
| 155 |
+
return merge_config(default_config, file_config)
|
| 156 |
+
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"Error loading config file: {e}")
|
| 159 |
+
print("Using default configuration")
|
| 160 |
+
return default_config
|
| 161 |
+
else:
|
| 162 |
+
# Save default config
|
| 163 |
+
try:
|
| 164 |
+
with open(config_file, 'w') as f:
|
| 165 |
+
json.dump(default_config, f, indent=2)
|
| 166 |
+
print(f"Created default configuration file: {config_file}")
|
| 167 |
+
except Exception as e:
|
| 168 |
+
print(f"Could not save default config: {e}")
|
| 169 |
+
|
| 170 |
+
return default_config
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def create_app():
|
| 174 |
+
"""Create and configure Flask application"""
|
| 175 |
+
app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), 'static'))
|
| 176 |
+
|
| 177 |
+
# Enable CORS for all routes
|
| 178 |
+
CORS(app, origins="*", allow_headers=["Content-Type", "Authorization"])
|
| 179 |
+
|
| 180 |
+
# Load configuration
|
| 181 |
+
config = load_config()
|
| 182 |
+
app.config['ISP_CONFIG'] = config
|
| 183 |
+
|
| 184 |
+
# Register blueprints
|
| 185 |
+
app.register_blueprint(isp_api, url_prefix='/api')
|
| 186 |
+
|
| 187 |
+
# Initialize engines
|
| 188 |
+
init_engines(config)
|
| 189 |
+
|
| 190 |
+
# Serve static files
|
| 191 |
+
@app.route('/', defaults={'path': ''})
|
| 192 |
+
@app.route('/<path:path>')
|
| 193 |
+
def serve_static(path):
|
| 194 |
+
static_folder_path = app.static_folder
|
| 195 |
+
if static_folder_path is None:
|
| 196 |
+
return "Static folder not configured", 404
|
| 197 |
+
|
| 198 |
+
if path != "" and os.path.exists(os.path.join(static_folder_path, path)):
|
| 199 |
+
return app.send_static_file(path)
|
| 200 |
+
else:
|
| 201 |
+
index_path = os.path.join(static_folder_path, 'index.html')
|
| 202 |
+
if os.path.exists(index_path):
|
| 203 |
+
return app.send_static_file('index.html')
|
| 204 |
+
else:
|
| 205 |
+
return """
|
| 206 |
+
<!DOCTYPE html>
|
| 207 |
+
<html>
|
| 208 |
+
<head>
|
| 209 |
+
<title>Virtual ISP Stack</title>
|
| 210 |
+
<style>
|
| 211 |
+
body { font-family: Arial, sans-serif; margin: 40px; }
|
| 212 |
+
.container { max-width: 800px; margin: 0 auto; }
|
| 213 |
+
.status { background: #f0f0f0; padding: 20px; border-radius: 5px; }
|
| 214 |
+
.api-link { color: #0066cc; text-decoration: none; }
|
| 215 |
+
.api-link:hover { text-decoration: underline; }
|
| 216 |
+
</style>
|
| 217 |
+
</head>
|
| 218 |
+
<body>
|
| 219 |
+
<div class="container">
|
| 220 |
+
<h1>Virtual ISP Stack</h1>
|
| 221 |
+
<div class="status">
|
| 222 |
+
<h2>System Status</h2>
|
| 223 |
+
<p>The Virtual ISP Stack is running successfully!</p>
|
| 224 |
+
<p><strong>API Endpoint:</strong> <a href="/api/status" class="api-link">/api/status</a></p>
|
| 225 |
+
<p><strong>System Stats:</strong> <a href="/api/stats" class="api-link">/api/stats</a></p>
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
<h2>Available API Endpoints</h2>
|
| 229 |
+
<ul>
|
| 230 |
+
<li><a href="/api/config" class="api-link">GET /api/config</a> - System configuration</li>
|
| 231 |
+
<li><a href="/api/status" class="api-link">GET /api/status</a> - System status</li>
|
| 232 |
+
<li><a href="/api/stats" class="api-link">GET /api/stats</a> - System statistics</li>
|
| 233 |
+
<li><a href="/api/dhcp/leases" class="api-link">GET /api/dhcp/leases</a> - DHCP leases</li>
|
| 234 |
+
<li><a href="/api/nat/sessions" class="api-link">GET /api/nat/sessions</a> - NAT sessions</li>
|
| 235 |
+
<li><a href="/api/firewall/rules" class="api-link">GET /api/firewall/rules</a> - Firewall rules</li>
|
| 236 |
+
<li><a href="/api/tcp/connections" class="api-link">GET /api/tcp/connections</a> - TCP connections</li>
|
| 237 |
+
<li><a href="/api/router/routes" class="api-link">GET /api/router/routes</a> - Routing table</li>
|
| 238 |
+
<li><a href="/api/bridge/clients" class="api-link">GET /api/bridge/clients</a> - Bridge clients</li>
|
| 239 |
+
<li><a href="/api/sessions" class="api-link">GET /api/sessions</a> - Session tracking</li>
|
| 240 |
+
<li><a href="/api/logs" class="api-link">GET /api/logs</a> - System logs</li>
|
| 241 |
+
</ul>
|
| 242 |
+
|
| 243 |
+
<h2>WebSocket Bridge</h2>
|
| 244 |
+
<p>WebSocket server running on port 8765 for packet bridge connections.</p>
|
| 245 |
+
<p>TCP server running on port 8766 for packet bridge connections.</p>
|
| 246 |
+
</div>
|
| 247 |
+
</body>
|
| 248 |
+
</html>
|
| 249 |
+
""", 200
|
| 250 |
+
|
| 251 |
+
return app
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def main():
|
| 255 |
+
"""Main application entry point"""
|
| 256 |
+
print("Starting Virtual ISP Stack...")
|
| 257 |
+
|
| 258 |
+
# Create Flask app
|
| 259 |
+
app = create_app()
|
| 260 |
+
|
| 261 |
+
# Start the application
|
| 262 |
+
print("Virtual ISP Stack started successfully!")
|
| 263 |
+
print("API available at: http://0.0.0.0:5000/api/")
|
| 264 |
+
print("WebSocket bridge at: ws://0.0.0.0:8765")
|
| 265 |
+
print("TCP bridge at: tcp://0.0.0.0:8766")
|
| 266 |
+
|
| 267 |
+
# Run Flask app
|
| 268 |
+
app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
if __name__ == '__main__':
|
| 272 |
+
main()
|
| 273 |
+
|
models/__pycache__/enhanced_user.cpython-311.pyc
ADDED
|
Binary file (26.2 kB). View file
|
|
|
models/__pycache__/user.cpython-311.pyc
ADDED
|
Binary file (1.3 kB). View file
|
|
|
models/enhanced_user.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced User Model with Authentication and VPN Client Management
|
| 3 |
+
|
| 4 |
+
This module provides comprehensive user management with security features,
|
| 5 |
+
VPN client management, and session tracking capabilities.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
import secrets
|
| 12 |
+
import jwt
|
| 13 |
+
import re
|
| 14 |
+
from flask import current_app
|
| 15 |
+
|
| 16 |
+
from .user import db
|
| 17 |
+
|
| 18 |
+
class User(db.Model):
|
| 19 |
+
"""Enhanced User model with authentication and VPN management"""
|
| 20 |
+
__tablename__ = 'users'
|
| 21 |
+
|
| 22 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 23 |
+
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
|
| 24 |
+
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
|
| 25 |
+
password_hash = db.Column(db.String(255), nullable=False)
|
| 26 |
+
salt = db.Column(db.String(32), nullable=False)
|
| 27 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 28 |
+
last_login = db.Column(db.DateTime)
|
| 29 |
+
is_active = db.Column(db.Boolean, default=True)
|
| 30 |
+
is_admin = db.Column(db.Boolean, default=False)
|
| 31 |
+
subscription_type = db.Column(db.String(20), default='free')
|
| 32 |
+
subscription_expires = db.Column(db.DateTime)
|
| 33 |
+
max_concurrent_connections = db.Column(db.Integer, default=1)
|
| 34 |
+
bandwidth_limit_mbps = db.Column(db.Integer, default=10)
|
| 35 |
+
email_verified = db.Column(db.Boolean, default=False)
|
| 36 |
+
email_verification_token = db.Column(db.String(64))
|
| 37 |
+
two_factor_enabled = db.Column(db.Boolean, default=False)
|
| 38 |
+
two_factor_secret = db.Column(db.String(32))
|
| 39 |
+
password_reset_token = db.Column(db.String(64))
|
| 40 |
+
password_reset_expires = db.Column(db.DateTime)
|
| 41 |
+
failed_login_attempts = db.Column(db.Integer, default=0)
|
| 42 |
+
account_locked_until = db.Column(db.DateTime)
|
| 43 |
+
|
| 44 |
+
# Relationships
|
| 45 |
+
vpn_clients = db.relationship('VPNClient', backref='user', lazy=True, cascade='all, delete-orphan')
|
| 46 |
+
vpn_sessions = db.relationship('VPNSession', backref='user', lazy=True)
|
| 47 |
+
|
| 48 |
+
def __init__(self, username, email, password=None):
|
| 49 |
+
self.username = username
|
| 50 |
+
self.email = email
|
| 51 |
+
if password:
|
| 52 |
+
self.set_password(password)
|
| 53 |
+
self.email_verification_token = secrets.token_urlsafe(32)
|
| 54 |
+
|
| 55 |
+
def set_password(self, password):
|
| 56 |
+
"""Set password with secure hashing and salt"""
|
| 57 |
+
if not self.validate_password_strength(password):
|
| 58 |
+
raise ValueError("Password does not meet security requirements")
|
| 59 |
+
|
| 60 |
+
self.salt = secrets.token_hex(16)
|
| 61 |
+
self.password_hash = generate_password_hash(password + self.salt)
|
| 62 |
+
self.failed_login_attempts = 0
|
| 63 |
+
self.account_locked_until = None
|
| 64 |
+
|
| 65 |
+
def check_password(self, password):
|
| 66 |
+
"""Verify password against hash"""
|
| 67 |
+
if self.is_account_locked():
|
| 68 |
+
return False
|
| 69 |
+
|
| 70 |
+
is_valid = check_password_hash(self.password_hash, password + self.salt)
|
| 71 |
+
|
| 72 |
+
if is_valid:
|
| 73 |
+
self.failed_login_attempts = 0
|
| 74 |
+
self.last_login = datetime.utcnow()
|
| 75 |
+
else:
|
| 76 |
+
self.failed_login_attempts += 1
|
| 77 |
+
if self.failed_login_attempts >= 5:
|
| 78 |
+
self.account_locked_until = datetime.utcnow() + timedelta(minutes=30)
|
| 79 |
+
|
| 80 |
+
return is_valid
|
| 81 |
+
|
| 82 |
+
def is_account_locked(self):
|
| 83 |
+
"""Check if account is locked due to failed login attempts"""
|
| 84 |
+
if self.account_locked_until and datetime.utcnow() < self.account_locked_until:
|
| 85 |
+
return True
|
| 86 |
+
elif self.account_locked_until and datetime.utcnow() >= self.account_locked_until:
|
| 87 |
+
# Unlock account
|
| 88 |
+
self.account_locked_until = None
|
| 89 |
+
self.failed_login_attempts = 0
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
@staticmethod
|
| 93 |
+
def validate_password_strength(password):
|
| 94 |
+
"""Validate password meets security requirements"""
|
| 95 |
+
if len(password) < 8:
|
| 96 |
+
return False
|
| 97 |
+
if not re.search(r'[A-Z]', password):
|
| 98 |
+
return False
|
| 99 |
+
if not re.search(r'[a-z]', password):
|
| 100 |
+
return False
|
| 101 |
+
if not re.search(r'\d', password):
|
| 102 |
+
return False
|
| 103 |
+
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
| 104 |
+
return False
|
| 105 |
+
return True
|
| 106 |
+
|
| 107 |
+
@staticmethod
|
| 108 |
+
def validate_email(email):
|
| 109 |
+
"""Validate email format"""
|
| 110 |
+
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
| 111 |
+
return re.match(pattern, email) is not None
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
def validate_username(username):
|
| 115 |
+
"""Validate username format"""
|
| 116 |
+
if len(username) < 3 or len(username) > 80:
|
| 117 |
+
return False
|
| 118 |
+
if not re.match(r'^[a-zA-Z0-9_-]+$', username):
|
| 119 |
+
return False
|
| 120 |
+
return True
|
| 121 |
+
|
| 122 |
+
def generate_auth_token(self, expires_in=3600):
|
| 123 |
+
"""Generate JWT authentication token"""
|
| 124 |
+
payload = {
|
| 125 |
+
'user_id': self.id,
|
| 126 |
+
'username': self.username,
|
| 127 |
+
'email': self.email,
|
| 128 |
+
'subscription_type': self.subscription_type,
|
| 129 |
+
'is_admin': self.is_admin,
|
| 130 |
+
'exp': datetime.utcnow() + timedelta(seconds=expires_in),
|
| 131 |
+
'iat': datetime.utcnow()
|
| 132 |
+
}
|
| 133 |
+
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
|
| 134 |
+
|
| 135 |
+
def generate_refresh_token(self, expires_in=2592000): # 30 days
|
| 136 |
+
"""Generate refresh token for extended sessions"""
|
| 137 |
+
payload = {
|
| 138 |
+
'user_id': self.id,
|
| 139 |
+
'type': 'refresh',
|
| 140 |
+
'exp': datetime.utcnow() + timedelta(seconds=expires_in),
|
| 141 |
+
'iat': datetime.utcnow()
|
| 142 |
+
}
|
| 143 |
+
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
|
| 144 |
+
|
| 145 |
+
@staticmethod
|
| 146 |
+
def verify_auth_token(token):
|
| 147 |
+
"""Verify JWT authentication token"""
|
| 148 |
+
try:
|
| 149 |
+
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
| 150 |
+
if payload.get('type') == 'refresh':
|
| 151 |
+
return None # Refresh tokens cannot be used for authentication
|
| 152 |
+
return User.query.get(payload['user_id'])
|
| 153 |
+
except jwt.ExpiredSignatureError:
|
| 154 |
+
return None
|
| 155 |
+
except jwt.InvalidTokenError:
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
@staticmethod
|
| 159 |
+
def verify_refresh_token(token):
|
| 160 |
+
"""Verify refresh token and return user"""
|
| 161 |
+
try:
|
| 162 |
+
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
| 163 |
+
if payload.get('type') != 'refresh':
|
| 164 |
+
return None
|
| 165 |
+
return User.query.get(payload['user_id'])
|
| 166 |
+
except jwt.ExpiredSignatureError:
|
| 167 |
+
return None
|
| 168 |
+
except jwt.InvalidTokenError:
|
| 169 |
+
return None
|
| 170 |
+
|
| 171 |
+
def generate_password_reset_token(self):
|
| 172 |
+
"""Generate password reset token"""
|
| 173 |
+
self.password_reset_token = secrets.token_urlsafe(32)
|
| 174 |
+
self.password_reset_expires = datetime.utcnow() + timedelta(hours=1)
|
| 175 |
+
return self.password_reset_token
|
| 176 |
+
|
| 177 |
+
def verify_password_reset_token(self, token):
|
| 178 |
+
"""Verify password reset token"""
|
| 179 |
+
if (self.password_reset_token == token and
|
| 180 |
+
self.password_reset_expires and
|
| 181 |
+
datetime.utcnow() < self.password_reset_expires):
|
| 182 |
+
return True
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
def reset_password(self, new_password, token):
|
| 186 |
+
"""Reset password using reset token"""
|
| 187 |
+
if not self.verify_password_reset_token(token):
|
| 188 |
+
return False
|
| 189 |
+
|
| 190 |
+
self.set_password(new_password)
|
| 191 |
+
self.password_reset_token = None
|
| 192 |
+
self.password_reset_expires = None
|
| 193 |
+
return True
|
| 194 |
+
|
| 195 |
+
def verify_email(self, token):
|
| 196 |
+
"""Verify email using verification token"""
|
| 197 |
+
if self.email_verification_token == token:
|
| 198 |
+
self.email_verified = True
|
| 199 |
+
self.email_verification_token = None
|
| 200 |
+
return True
|
| 201 |
+
return False
|
| 202 |
+
|
| 203 |
+
def can_create_vpn_client(self):
|
| 204 |
+
"""Check if user can create additional VPN clients"""
|
| 205 |
+
active_clients = len([c for c in self.vpn_clients if c.is_active])
|
| 206 |
+
|
| 207 |
+
if self.subscription_type == 'free':
|
| 208 |
+
return active_clients < 1
|
| 209 |
+
elif self.subscription_type == 'premium':
|
| 210 |
+
return active_clients < 5
|
| 211 |
+
elif self.subscription_type == 'enterprise':
|
| 212 |
+
return active_clients < 50
|
| 213 |
+
|
| 214 |
+
return False
|
| 215 |
+
|
| 216 |
+
def get_active_sessions_count(self):
|
| 217 |
+
"""Get count of active VPN sessions"""
|
| 218 |
+
return len([s for s in self.vpn_sessions if s.disconnected_at is None])
|
| 219 |
+
|
| 220 |
+
def can_connect_vpn(self):
|
| 221 |
+
"""Check if user can establish new VPN connections"""
|
| 222 |
+
active_sessions = self.get_active_sessions_count()
|
| 223 |
+
return active_sessions < self.max_concurrent_connections
|
| 224 |
+
|
| 225 |
+
def get_bandwidth_usage_today(self):
|
| 226 |
+
"""Get bandwidth usage for today"""
|
| 227 |
+
today = datetime.utcnow().date()
|
| 228 |
+
today_sessions = [s for s in self.vpn_sessions
|
| 229 |
+
if s.connected_at and s.connected_at.date() == today]
|
| 230 |
+
|
| 231 |
+
total_bytes = sum(s.bytes_received + s.bytes_sent for s in today_sessions)
|
| 232 |
+
return total_bytes
|
| 233 |
+
|
| 234 |
+
def is_subscription_active(self):
|
| 235 |
+
"""Check if subscription is active"""
|
| 236 |
+
if self.subscription_type == 'free':
|
| 237 |
+
return True
|
| 238 |
+
|
| 239 |
+
return (self.subscription_expires and
|
| 240 |
+
datetime.utcnow() < self.subscription_expires)
|
| 241 |
+
|
| 242 |
+
def to_dict(self, include_sensitive=False):
|
| 243 |
+
"""Convert user to dictionary"""
|
| 244 |
+
data = {
|
| 245 |
+
'id': self.id,
|
| 246 |
+
'username': self.username,
|
| 247 |
+
'email': self.email,
|
| 248 |
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 249 |
+
'last_login': self.last_login.isoformat() if self.last_login else None,
|
| 250 |
+
'is_active': self.is_active,
|
| 251 |
+
'subscription_type': self.subscription_type,
|
| 252 |
+
'subscription_expires': self.subscription_expires.isoformat() if self.subscription_expires else None,
|
| 253 |
+
'max_concurrent_connections': self.max_concurrent_connections,
|
| 254 |
+
'bandwidth_limit_mbps': self.bandwidth_limit_mbps,
|
| 255 |
+
'email_verified': self.email_verified,
|
| 256 |
+
'two_factor_enabled': self.two_factor_enabled,
|
| 257 |
+
'is_subscription_active': self.is_subscription_active(),
|
| 258 |
+
'active_vpn_clients': len([c for c in self.vpn_clients if c.is_active]),
|
| 259 |
+
'active_sessions': self.get_active_sessions_count(),
|
| 260 |
+
'can_create_vpn_client': self.can_create_vpn_client(),
|
| 261 |
+
'can_connect_vpn': self.can_connect_vpn()
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
if include_sensitive and (self.is_admin or include_sensitive == 'self'):
|
| 265 |
+
data.update({
|
| 266 |
+
'is_admin': self.is_admin,
|
| 267 |
+
'failed_login_attempts': self.failed_login_attempts,
|
| 268 |
+
'account_locked': self.is_account_locked(),
|
| 269 |
+
'bandwidth_usage_today': self.get_bandwidth_usage_today()
|
| 270 |
+
})
|
| 271 |
+
|
| 272 |
+
return data
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
class VPNClient(db.Model):
|
| 276 |
+
"""VPN Client configuration and management"""
|
| 277 |
+
__tablename__ = 'vpn_clients'
|
| 278 |
+
|
| 279 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 280 |
+
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
| 281 |
+
client_name = db.Column(db.String(100), nullable=False)
|
| 282 |
+
protocol = db.Column(db.String(20), nullable=False) # openvpn, ikev2, wireguard
|
| 283 |
+
certificate_serial = db.Column(db.String(50), unique=True)
|
| 284 |
+
private_key_path = db.Column(db.String(255))
|
| 285 |
+
certificate_path = db.Column(db.String(255))
|
| 286 |
+
config_file_path = db.Column(db.String(255))
|
| 287 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 288 |
+
last_connected = db.Column(db.DateTime)
|
| 289 |
+
is_active = db.Column(db.Boolean, default=True)
|
| 290 |
+
device_type = db.Column(db.String(50)) # windows, macos, linux, ios, android
|
| 291 |
+
public_key = db.Column(db.Text) # For WireGuard
|
| 292 |
+
|
| 293 |
+
# Relationships
|
| 294 |
+
sessions = db.relationship('VPNSession', backref='vpn_client', lazy=True)
|
| 295 |
+
|
| 296 |
+
def __init__(self, user_id, client_name, protocol, device_type=None):
|
| 297 |
+
self.user_id = user_id
|
| 298 |
+
self.client_name = client_name
|
| 299 |
+
self.protocol = protocol
|
| 300 |
+
self.device_type = device_type
|
| 301 |
+
|
| 302 |
+
def get_active_sessions(self):
|
| 303 |
+
"""Get active sessions for this client"""
|
| 304 |
+
return [s for s in self.sessions if s.disconnected_at is None]
|
| 305 |
+
|
| 306 |
+
def get_total_bandwidth_usage(self):
|
| 307 |
+
"""Get total bandwidth usage for this client"""
|
| 308 |
+
return sum(s.bytes_received + s.bytes_sent for s in self.sessions)
|
| 309 |
+
|
| 310 |
+
def to_dict(self):
|
| 311 |
+
"""Convert VPN client to dictionary"""
|
| 312 |
+
return {
|
| 313 |
+
'id': self.id,
|
| 314 |
+
'client_name': self.client_name,
|
| 315 |
+
'protocol': self.protocol,
|
| 316 |
+
'device_type': self.device_type,
|
| 317 |
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 318 |
+
'last_connected': self.last_connected.isoformat() if self.last_connected else None,
|
| 319 |
+
'is_active': self.is_active,
|
| 320 |
+
'certificate_serial': self.certificate_serial,
|
| 321 |
+
'active_sessions': len(self.get_active_sessions()),
|
| 322 |
+
'total_bandwidth_usage': self.get_total_bandwidth_usage()
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
class VPNSession(db.Model):
|
| 327 |
+
"""VPN Session tracking"""
|
| 328 |
+
__tablename__ = 'vpn_sessions'
|
| 329 |
+
|
| 330 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 331 |
+
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
| 332 |
+
client_id = db.Column(db.Integer, db.ForeignKey('vpn_clients.id'), nullable=False)
|
| 333 |
+
server_protocol = db.Column(db.String(20), nullable=False)
|
| 334 |
+
client_ip = db.Column(db.String(15))
|
| 335 |
+
server_ip = db.Column(db.String(15))
|
| 336 |
+
client_real_ip = db.Column(db.String(45)) # Support IPv6
|
| 337 |
+
connected_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 338 |
+
disconnected_at = db.Column(db.DateTime)
|
| 339 |
+
bytes_received = db.Column(db.BigInteger, default=0)
|
| 340 |
+
bytes_sent = db.Column(db.BigInteger, default=0)
|
| 341 |
+
session_duration = db.Column(db.Integer) # in seconds
|
| 342 |
+
disconnect_reason = db.Column(db.String(100))
|
| 343 |
+
|
| 344 |
+
def __init__(self, user_id, client_id, server_protocol, client_ip=None, server_ip=None, client_real_ip=None):
|
| 345 |
+
self.user_id = user_id
|
| 346 |
+
self.client_id = client_id
|
| 347 |
+
self.server_protocol = server_protocol
|
| 348 |
+
self.client_ip = client_ip
|
| 349 |
+
self.server_ip = server_ip
|
| 350 |
+
self.client_real_ip = client_real_ip
|
| 351 |
+
|
| 352 |
+
def disconnect(self, reason=None):
|
| 353 |
+
"""Mark session as disconnected"""
|
| 354 |
+
self.disconnected_at = datetime.utcnow()
|
| 355 |
+
self.disconnect_reason = reason
|
| 356 |
+
if self.connected_at:
|
| 357 |
+
self.session_duration = int((self.disconnected_at - self.connected_at).total_seconds())
|
| 358 |
+
|
| 359 |
+
def is_active(self):
|
| 360 |
+
"""Check if session is active"""
|
| 361 |
+
return self.disconnected_at is None
|
| 362 |
+
|
| 363 |
+
def get_duration(self):
|
| 364 |
+
"""Get session duration in seconds"""
|
| 365 |
+
if self.disconnected_at:
|
| 366 |
+
return self.session_duration
|
| 367 |
+
elif self.connected_at:
|
| 368 |
+
return int((datetime.utcnow() - self.connected_at).total_seconds())
|
| 369 |
+
return 0
|
| 370 |
+
|
| 371 |
+
def to_dict(self):
|
| 372 |
+
"""Convert VPN session to dictionary"""
|
| 373 |
+
return {
|
| 374 |
+
'id': self.id,
|
| 375 |
+
'client_id': self.client_id,
|
| 376 |
+
'server_protocol': self.server_protocol,
|
| 377 |
+
'client_ip': self.client_ip,
|
| 378 |
+
'server_ip': self.server_ip,
|
| 379 |
+
'client_real_ip': self.client_real_ip,
|
| 380 |
+
'connected_at': self.connected_at.isoformat() if self.connected_at else None,
|
| 381 |
+
'disconnected_at': self.disconnected_at.isoformat() if self.disconnected_at else None,
|
| 382 |
+
'bytes_received': self.bytes_received,
|
| 383 |
+
'bytes_sent': self.bytes_sent,
|
| 384 |
+
'session_duration': self.get_duration(),
|
| 385 |
+
'disconnect_reason': self.disconnect_reason,
|
| 386 |
+
'is_active': self.is_active()
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
class ServerConfiguration(db.Model):
|
| 391 |
+
"""VPN Server configuration management"""
|
| 392 |
+
__tablename__ = 'server_configurations'
|
| 393 |
+
|
| 394 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 395 |
+
protocol = db.Column(db.String(20), nullable=False)
|
| 396 |
+
server_name = db.Column(db.String(100), nullable=False)
|
| 397 |
+
listen_port = db.Column(db.Integer, nullable=False)
|
| 398 |
+
network_cidr = db.Column(db.String(18), nullable=False)
|
| 399 |
+
dns_servers = db.Column(db.Text) # JSON string
|
| 400 |
+
routes = db.Column(db.Text) # JSON string
|
| 401 |
+
is_active = db.Column(db.Boolean, default=True)
|
| 402 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 403 |
+
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 404 |
+
max_clients = db.Column(db.Integer, default=100)
|
| 405 |
+
|
| 406 |
+
def __init__(self, protocol, server_name, listen_port, network_cidr):
|
| 407 |
+
self.protocol = protocol
|
| 408 |
+
self.server_name = server_name
|
| 409 |
+
self.listen_port = listen_port
|
| 410 |
+
self.network_cidr = network_cidr
|
| 411 |
+
|
| 412 |
+
def to_dict(self):
|
| 413 |
+
"""Convert server configuration to dictionary"""
|
| 414 |
+
return {
|
| 415 |
+
'id': self.id,
|
| 416 |
+
'protocol': self.protocol,
|
| 417 |
+
'server_name': self.server_name,
|
| 418 |
+
'listen_port': self.listen_port,
|
| 419 |
+
'network_cidr': self.network_cidr,
|
| 420 |
+
'dns_servers': self.dns_servers,
|
| 421 |
+
'routes': self.routes,
|
| 422 |
+
'is_active': self.is_active,
|
| 423 |
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 424 |
+
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 425 |
+
'max_clients': self.max_clients
|
| 426 |
+
}
|
| 427 |
+
|
models/user.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 2 |
+
|
| 3 |
+
db = SQLAlchemy()
|
| 4 |
+
|
| 5 |
+
class User(db.Model):
|
| 6 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 7 |
+
username = db.Column(db.String(80), unique=True, nullable=False)
|
| 8 |
+
email = db.Column(db.String(120), unique=True, nullable=False)
|
| 9 |
+
|
| 10 |
+
def __repr__(self):
|
| 11 |
+
return f'<User {self.username}>'
|
| 12 |
+
|
| 13 |
+
def to_dict(self):
|
| 14 |
+
return {
|
| 15 |
+
'id': self.id,
|
| 16 |
+
'username': self.username,
|
| 17 |
+
'email': self.email
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
|
openvpn/ca.crt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIDMzCCAhugAwIBAgIUNO765P4t/yD/PnIFTMVs0Q32TJYwDQYJKoZIhvcNAQEL
|
| 3 |
+
BQAwDjEMMAoGA1UEAwwDeWVzMB4XDTI1MDgwMjAxMjkzNVoXDTM1MDczMTAxMjkz
|
| 4 |
+
NVowDjEMMAoGA1UEAwwDeWVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
| 5 |
+
AQEAtwhMGXouHnHBRd2RhdrW8sOMgqt4wDXZC0J+4UMjOX6Y7t2O1Sgw/sWhwFPk
|
| 6 |
+
QF/cMoQIvsucklPogcnzzGtv9zDkAXyVyCC27UYbg8JfWZK3ZMrt6dfEmYf4KKXm
|
| 7 |
+
D6PLn9guxzBB63dhEWx/7fd6H9C/rK/u0rOh15DQRnfEI468cmXS5uNg8ke/73+y
|
| 8 |
+
Gzb6q7ZOFByBAwM0hW0lStBaIIcxouFrIK8B72O8H+6t10K1GvgiBhKvM3cc8dpN
|
| 9 |
+
y4qvRoN/o+eXarZG7G9dfm9OFgdd9LoXPTTbO+ftFPKOq4F41PnMd2Zcyk7P3GCr
|
| 10 |
+
3oK7NbISxZ5efLpy45lgSpqKBwIDAQABo4GIMIGFMB0GA1UdDgQWBBQIi0Er30cV
|
| 11 |
+
Qzi+U/LPV4Lf3yvGIzBJBgNVHSMEQjBAgBQIi0Er30cVQzi+U/LPV4Lf3yvGI6ES
|
| 12 |
+
pBAwDjEMMAoGA1UEAwwDeWVzghQ07vrk/i3/IP8+cgVMxWzRDfZMljAMBgNVHRME
|
| 13 |
+
BTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAHzfSFbi1G7WC
|
| 14 |
+
vMSOqSv4/jlBExnz/AlLUBHhgDomIdLK8Pb3tyCD5IYkmi0NT5x6DORcOV2ow1JZ
|
| 15 |
+
o4BL7OVV+fhz3VKXEpG+s3gq5j2m+raqLtu6QKBGg7SIUZ4MLjggvAcPjsK+n8sK
|
| 16 |
+
86sAUFVTccBxJlKBShAUPSNihyWwxB4PQFvwhefNQSoID1kAB2Fzf1beMX6Gp6Lj
|
| 17 |
+
ldI6e63lpYtIbp4+2F5SxJ/hGTUx+nWbOAHPvhBfhN6sEu9G1C5KPR0cm+xxOpZ9
|
| 18 |
+
lA7y4Dea7pyVybR/b7lFquE3TReXCoLx79UNNSv8erIlsy1jh9yXDnTCk8SN1dpO
|
| 19 |
+
YwJ9U0AHXA==
|
| 20 |
+
-----END CERTIFICATE-----
|
openvpn/dh.pem
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN DH PARAMETERS-----
|
| 2 |
+
MIIBCAKCAQEAlPRBW0tYm271xYHi15JrD3JRlpvdjAm+CZoEq0ElLXvSlIKaNQls
|
| 3 |
+
ITH+KIBBX3pgbFFk03fO9ApF0kSOzycRRCuW970iCkDoFUN9y58EG+BI863FkU1h
|
| 4 |
+
3dx+c59HqdWXkzFK+SmTfKIe12alZFik5G0Xs0hkphCgPaXvWlojorjQoRfKySw3
|
| 5 |
+
VxpybKS83+l3t2ER3Z03IRvWinlnuxVAcymzeSR9hwIMJi3RmYmNmdXNel/WFAo2
|
| 6 |
+
zT5j2f2OZHtnBhvo1V92Rml+5rJksPX4lJMRNwVEnXwqVUyCQOTTiGTUjLOO2gdk
|
| 7 |
+
HLhH5teetBdKL4tFcldeIJSk3e0oWXbURwIBAg==
|
| 8 |
+
-----END DH PARAMETERS-----
|
openvpn/server.conf
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
port 1194
|
| 2 |
+
proto udp
|
| 3 |
+
dev tun
|
| 4 |
+
ca /etc/openvpn/server/ca.crt
|
| 5 |
+
cert /etc/openvpn/server/server.crt
|
| 6 |
+
key /etc/openvpn/server/server.key
|
| 7 |
+
dh /etc/openvpn/server/dh.pem
|
| 8 |
+
server 10.8.0.0 255.255.255.0
|
| 9 |
+
ifconfig-pool-persist ipp.txt
|
| 10 |
+
push "redirect-gateway def1 bypass-dhcp"
|
| 11 |
+
push "dhcp-option DNS 8.8.8.8"
|
| 12 |
+
push "dhcp-option DNS 8.8.4.4"
|
| 13 |
+
keepalive 10 120
|
| 14 |
+
cipher AES-256-CBC
|
| 15 |
+
persist-key
|
| 16 |
+
persist-tun
|
| 17 |
+
status openvpn-status.log
|
| 18 |
+
verb 3
|
| 19 |
+
explicit-exit-notify 1
|
| 20 |
+
|
| 21 |
+
|
openvpn/server.crt
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Certificate:
|
| 2 |
+
Data:
|
| 3 |
+
Version: 3 (0x2)
|
| 4 |
+
Serial Number:
|
| 5 |
+
dd:b5:29:c9:70:b2:b3:65:70:ac:0f:57:30:15:b4:2a
|
| 6 |
+
Signature Algorithm: sha256WithRSAEncryption
|
| 7 |
+
Issuer: CN=yes
|
| 8 |
+
Validity
|
| 9 |
+
Not Before: Aug 2 01:29:38 2025 GMT
|
| 10 |
+
Not After : Nov 5 01:29:38 2027 GMT
|
| 11 |
+
Subject: CN=server
|
| 12 |
+
Subject Public Key Info:
|
| 13 |
+
Public Key Algorithm: rsaEncryption
|
| 14 |
+
Public-Key: (2048 bit)
|
| 15 |
+
Modulus:
|
| 16 |
+
00:dd:9e:02:fb:e3:57:cd:51:43:36:6a:2f:30:f5:
|
| 17 |
+
a1:42:5c:16:f1:7b:4b:0a:aa:b1:34:b5:86:51:3e:
|
| 18 |
+
6b:82:2e:59:df:42:21:cf:65:14:ea:8c:93:3c:0a:
|
| 19 |
+
72:a5:2e:0f:64:1a:ec:76:52:18:b2:d3:a0:df:df:
|
| 20 |
+
19:83:7e:39:9e:f5:16:18:36:34:ae:57:cf:2c:89:
|
| 21 |
+
7c:c5:97:e3:8f:d0:83:08:7f:14:0c:74:2c:d2:95:
|
| 22 |
+
09:6e:42:99:a0:28:69:83:68:f4:9c:0e:b5:3e:08:
|
| 23 |
+
8f:d8:06:ec:d5:aa:c8:bc:19:4b:ff:e4:99:50:12:
|
| 24 |
+
67:25:d4:79:94:1f:3d:64:b2:c8:00:ea:97:c2:df:
|
| 25 |
+
b8:1c:dc:69:47:9f:59:df:03:06:5a:32:7a:fa:51:
|
| 26 |
+
96:45:9a:b7:e7:03:ef:9d:3b:94:51:9d:08:69:bb:
|
| 27 |
+
b0:3e:c8:9c:a3:a0:9c:18:aa:e9:88:ec:96:c3:71:
|
| 28 |
+
b1:f6:a7:09:ff:c0:56:b1:24:22:ab:fc:9a:c5:fc:
|
| 29 |
+
fd:67:8e:1a:86:ff:0a:5b:28:46:b4:20:93:05:b6:
|
| 30 |
+
ff:87:93:66:7d:ae:92:c4:0d:20:99:e9:c5:b8:3d:
|
| 31 |
+
41:3a:06:83:49:e5:13:2e:d6:33:94:45:6a:36:84:
|
| 32 |
+
f9:c9:61:fe:98:3a:6e:41:ed:d8:8c:f1:55:3d:6d:
|
| 33 |
+
53:fb
|
| 34 |
+
Exponent: 65537 (0x10001)
|
| 35 |
+
X509v3 extensions:
|
| 36 |
+
X509v3 Basic Constraints:
|
| 37 |
+
CA:FALSE
|
| 38 |
+
X509v3 Subject Key Identifier:
|
| 39 |
+
F4:62:12:72:49:40:C2:8A:46:5A:CB:71:BE:33:58:25:B3:E0:01:AC
|
| 40 |
+
X509v3 Authority Key Identifier:
|
| 41 |
+
keyid:08:8B:41:2B:DF:47:15:43:38:BE:53:F2:CF:57:82:DF:DF:2B:C6:23
|
| 42 |
+
DirName:/CN=yes
|
| 43 |
+
serial:34:EE:FA:E4:FE:2D:FF:20:FF:3E:72:05:4C:C5:6C:D1:0D:F6:4C:96
|
| 44 |
+
X509v3 Extended Key Usage:
|
| 45 |
+
TLS Web Server Authentication
|
| 46 |
+
X509v3 Key Usage:
|
| 47 |
+
Digital Signature, Key Encipherment
|
| 48 |
+
X509v3 Subject Alternative Name:
|
| 49 |
+
DNS:server
|
| 50 |
+
Signature Algorithm: sha256WithRSAEncryption
|
| 51 |
+
Signature Value:
|
| 52 |
+
85:f7:59:01:c2:99:23:c3:9a:99:2a:0a:bc:5d:7d:1c:e8:7c:
|
| 53 |
+
e9:23:a5:87:08:bd:45:1b:a7:a9:b7:3a:06:b6:91:86:ac:61:
|
| 54 |
+
03:ae:cd:65:80:0e:e4:81:dc:38:b3:fe:6d:6f:02:e4:9e:43:
|
| 55 |
+
95:d0:a6:38:30:53:52:14:f1:96:2a:30:69:2f:56:24:65:ba:
|
| 56 |
+
53:c0:b0:22:23:2b:18:37:a1:0c:45:07:cb:ec:a9:71:f7:96:
|
| 57 |
+
2a:d2:18:94:f0:07:18:1f:4c:d2:c5:d5:66:8f:1d:5c:08:8d:
|
| 58 |
+
02:00:d6:0d:df:fd:6e:1e:2a:47:8c:30:fd:5b:46:56:0a:5a:
|
| 59 |
+
d4:6d:d4:99:c8:94:26:36:0b:86:30:dd:cb:3a:2e:a2:f3:80:
|
| 60 |
+
0f:62:80:f8:9d:ec:98:f2:96:20:4f:46:01:ae:9d:35:7f:34:
|
| 61 |
+
21:d7:71:89:b6:7a:ce:94:7e:14:e6:bf:b6:08:44:39:24:db:
|
| 62 |
+
aa:cf:54:46:34:8f:67:6c:72:22:f1:eb:e9:94:7d:73:26:f3:
|
| 63 |
+
2f:72:fe:28:b3:cb:28:c3:4c:14:3d:c3:81:1e:8d:96:96:e5:
|
| 64 |
+
df:af:c4:0a:06:71:16:df:8f:a3:30:50:79:45:95:4c:e8:57:
|
| 65 |
+
ee:ed:38:dd:82:8e:0e:b1:2b:4d:27:2b:6f:bc:c8:1c:91:de:
|
| 66 |
+
2c:55:69:38
|
| 67 |
+
-----BEGIN CERTIFICATE-----
|
| 68 |
+
MIIDWDCCAkCgAwIBAgIRAN21KclwsrNlcKwPVzAVtCowDQYJKoZIhvcNAQELBQAw
|
| 69 |
+
DjEMMAoGA1UEAwwDeWVzMB4XDTI1MDgwMjAxMjkzOFoXDTI3MTEwNTAxMjkzOFow
|
| 70 |
+
ETEPMA0GA1UEAwwGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
| 71 |
+
AQEA3Z4C++NXzVFDNmovMPWhQlwW8XtLCqqxNLWGUT5rgi5Z30Ihz2UU6oyTPApy
|
| 72 |
+
pS4PZBrsdlIYstOg398Zg345nvUWGDY0rlfPLIl8xZfjj9CDCH8UDHQs0pUJbkKZ
|
| 73 |
+
oChpg2j0nA61PgiP2Abs1arIvBlL/+SZUBJnJdR5lB89ZLLIAOqXwt+4HNxpR59Z
|
| 74 |
+
3wMGWjJ6+lGWRZq35wPvnTuUUZ0IabuwPsico6CcGKrpiOyWw3Gx9qcJ/8BWsSQi
|
| 75 |
+
q/yaxfz9Z44ahv8KWyhGtCCTBbb/h5Nmfa6SxA0gmenFuD1BOgaDSeUTLtYzlEVq
|
| 76 |
+
NoT5yWH+mDpuQe3YjPFVPW1T+wIDAQABo4GtMIGqMAkGA1UdEwQCMAAwHQYDVR0O
|
| 77 |
+
BBYEFPRiEnJJQMKKRlrLcb4zWCWz4AGsMEkGA1UdIwRCMECAFAiLQSvfRxVDOL5T
|
| 78 |
+
8s9Xgt/fK8YjoRKkEDAOMQwwCgYDVQQDDAN5ZXOCFDTu+uT+Lf8g/z5yBUzFbNEN
|
| 79 |
+
9kyWMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAsGA1UdDwQEAwIFoDARBgNVHREECjAI
|
| 80 |
+
ggZzZXJ2ZXIwDQYJKoZIhvcNAQELBQADggEBAIX3WQHCmSPDmpkqCrxdfRzofOkj
|
| 81 |
+
pYcIvUUbp6m3Oga2kYasYQOuzWWADuSB3Diz/m1vAuSeQ5XQpjgwU1IU8ZYqMGkv
|
| 82 |
+
ViRlulPAsCIjKxg3oQxFB8vsqXH3lirSGJTwBxgfTNLF1WaPHVwIjQIA1g3f/W4e
|
| 83 |
+
KkeMMP1bRlYKWtRt1JnIlCY2C4Yw3cs6LqLzgA9igPid7JjyliBPRgGunTV/NCHX
|
| 84 |
+
cYm2es6UfhTmv7YIRDkk26rPVEY0j2dsciLx6+mUfXMm8y9y/iizyyjDTBQ9w4Ee
|
| 85 |
+
jZaW5d+vxAoGcRbfj6MwUHlFlUzoV+7tON2Cjg6xK00nK2+8yByR3ixVaTg=
|
| 86 |
+
-----END CERTIFICATE-----
|
openvpn/server.key
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN PRIVATE KEY-----
|
| 2 |
+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdngL741fNUUM2
|
| 3 |
+
ai8w9aFCXBbxe0sKqrE0tYZRPmuCLlnfQiHPZRTqjJM8CnKlLg9kGux2Uhiy06Df
|
| 4 |
+
3xmDfjme9RYYNjSuV88siXzFl+OP0IMIfxQMdCzSlQluQpmgKGmDaPScDrU+CI/Y
|
| 5 |
+
BuzVqsi8GUv/5JlQEmcl1HmUHz1kssgA6pfC37gc3GlHn1nfAwZaMnr6UZZFmrfn
|
| 6 |
+
A++dO5RRnQhpu7A+yJyjoJwYqumI7JbDcbH2pwn/wFaxJCKr/JrF/P1njhqG/wpb
|
| 7 |
+
KEa0IJMFtv+Hk2Z9rpLEDSCZ6cW4PUE6BoNJ5RMu1jOURWo2hPnJYf6YOm5B7diM
|
| 8 |
+
8VU9bVP7AgMBAAECggEATtwR0sEYtspSYPQS+9iD/AGZ9m75in+n1Ao+E/3isq28
|
| 9 |
+
tDmrn0moUjgYklZjakzEFEqSVx4qhMPSrKcORKCvb1Vl+dKcF2fOpFn+KK++Pagk
|
| 10 |
+
YGsb3ryeUIbRFsejM/79YNIBrOB89OiGCwiX0QZXLLvRs+qL9Za+1pLPenpNVd2w
|
| 11 |
+
zL+AZ8QkJZdHn1vOZt9vKRlpe8psAt64RHb+LqhYWfeLlpIUjpM5Vu9FFewMGPrw
|
| 12 |
+
n+GVCzK4ylq0pJ9bYwKI5Hw4qnJ3j5bGIumEjYBqqmef1+OTD3r/wyhTGpK9RRAu
|
| 13 |
+
WD9YGJeQx3ybzRL7Wj6k5g0dn+UA82Lh7Y8n9IoSaQKBgQDqP/BU2KapOHgFt2DE
|
| 14 |
+
WHU/+zA7/kfMJMGB5dYy8oXTxUY7WuqX9lja3rC0XuH10JTD6Q21jkTujc0T5/1B
|
| 15 |
+
4KxuX+nQP/T9b4XzVM3pKWVmHUt6wf24sbuTNxOy/Q/wC7eCnkr04CEl0vf3E56N
|
| 16 |
+
JaLG11dbpcn+9RC9FlUhlYY8QwKBgQDyMcz43915YGOQMkGVZFPvKyOy7ol4fFZv
|
| 17 |
+
VRfRoGx9CfHCIOfh9vmlUy6TR4qAQkCnkL730OsxpW3aDTe3qcAcmhiK7u5TfWrE
|
| 18 |
+
cd1WgrkymJ8hyEk6FSV0GMKrccQeEo2T95cKnk6lNXnEdNp5kx7LBQhL36fEtMXS
|
| 19 |
+
FGCcRkNp6QKBgAbm6WLmm0qDIm4wsAY5AQNomEw8OstWDemQ5xXLNYw+1Mns7Nqb
|
| 20 |
+
ZJTWWOiHnyrKAYggNsoxrfBFd1Rt0nV9dDcwVkhPih1pis3XotWK5bTzigTM8Hff
|
| 21 |
+
rMIyrj7o2+5bugV8OoMqk2903t+F0XchM8GeGLHXmbMMb3jSzqFVsYXXAoGBAII1
|
| 22 |
+
Z/99S7LPsXd6rWvFzqJMzRqLx/iw0D92viGDYBAxYnp9+myvvTO27tlbowilleEA
|
| 23 |
+
nsrY1TmRuOd8J7JkXtaBuiQnpJXaXaZTmS3DhhG/n/4nkcbaS5KJJU/LECcizl74
|
| 24 |
+
w4l/5sRHZbnLIRIvmGSJxhYUnjvQ/HGfZvldhSzRAoGBAMVTrxWedC2XeSMwjdhF
|
| 25 |
+
zeDBAp/dTMEnRaS0j3rp+4a4l7Sus1L/p8gBrJtnf/B43bNvQ5cr2jwH7Ql5cF1A
|
| 26 |
+
A7hpZ3C0trNaf6WqslJQhN8j8Cs85S/8rPGM5yAfyzKTMe0ytLUjn+XiQCqCUFcT
|
| 27 |
+
Inqx4ll7r2tlcI3aMlvN2qsd
|
| 28 |
+
-----END PRIVATE KEY-----
|
requirements.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core Flask dependencies
|
| 2 |
+
Flask==3.1.1
|
| 3 |
+
flask-cors==6.0.0
|
| 4 |
+
Flask-SQLAlchemy==3.1.1
|
| 5 |
+
Werkzeug==3.1.3
|
| 6 |
+
|
| 7 |
+
# Database
|
| 8 |
+
SQLAlchemy==2.0.41
|
| 9 |
+
|
| 10 |
+
# Async and networking
|
| 11 |
+
aiohttp==3.12.15
|
| 12 |
+
aiohappyeyeballs==2.6.1
|
| 13 |
+
aiosignal==1.4.0
|
| 14 |
+
websockets==15.0.1
|
| 15 |
+
|
| 16 |
+
# Utilities
|
| 17 |
+
attrs==25.3.0
|
| 18 |
+
blinker==1.9.0
|
| 19 |
+
click==8.2.1
|
| 20 |
+
frozenlist==1.7.0
|
| 21 |
+
greenlet==3.2.3
|
| 22 |
+
idna==3.10
|
| 23 |
+
itsdangerous==2.2.0
|
| 24 |
+
Jinja2==3.1.6
|
| 25 |
+
MarkupSafe==3.0.2
|
| 26 |
+
multidict==6.6.3
|
| 27 |
+
propcache==0.3.2
|
| 28 |
+
typing_extensions==4.14.0
|
| 29 |
+
yarl==1.20.1
|
| 30 |
+
jwt
|
| 31 |
+
# Additional dependencies for VPN management
|
| 32 |
+
psutil==5.9.8
|
| 33 |
+
|
routes/__pycache__/auth.cpython-311.pyc
ADDED
|
Binary file (30 kB). View file
|
|
|