649 lines
22 KiB
Python
649 lines
22 KiB
Python
|
#!/usr/bin/env python3
|
||
|
"""
|
||
|
Multi-Provider VPN Gateway Backend
|
||
|
Supports: Mullvad, Custom WireGuard, Imported Configs
|
||
|
With permanent killswitch protection
|
||
|
"""
|
||
|
|
||
|
from flask import Flask, request, jsonify, send_from_directory
|
||
|
from flask_cors import CORS
|
||
|
import subprocess
|
||
|
import json
|
||
|
import os
|
||
|
import re
|
||
|
import requests
|
||
|
import time
|
||
|
import yaml
|
||
|
import base64
|
||
|
from datetime import datetime
|
||
|
import threading
|
||
|
import logging
|
||
|
from pathlib import Path
|
||
|
from typing import Dict, List, Optional
|
||
|
|
||
|
app = Flask(__name__)
|
||
|
CORS(app)
|
||
|
|
||
|
# Configuration
|
||
|
CONFIG_FILE = '/opt/vpn-gateway/config.json'
|
||
|
PROVIDERS_DIR = '/opt/vpn-gateway/providers'
|
||
|
WIREGUARD_DIR = '/etc/wireguard'
|
||
|
|
||
|
# Setup logging
|
||
|
logging.basicConfig(
|
||
|
level=logging.INFO,
|
||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||
|
handlers=[
|
||
|
logging.FileHandler('/var/log/vpn-gateway.log'),
|
||
|
logging.StreamHandler()
|
||
|
]
|
||
|
)
|
||
|
|
||
|
class VPNProvider:
|
||
|
"""Base class for VPN providers"""
|
||
|
|
||
|
def __init__(self, name: str):
|
||
|
self.name = name
|
||
|
self.servers = {}
|
||
|
|
||
|
def get_servers(self) -> Dict:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def generate_config(self, server: str) -> str:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
class MullvadProvider(VPNProvider):
|
||
|
"""Mullvad VPN provider"""
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__("mullvad")
|
||
|
self.api_url = "https://api.mullvad.net/www/relays/all/"
|
||
|
self.public_key = "g+9JNZp3SvLPvBb+PzXHyOPHhqNiUdATrz1YdNEPvWo="
|
||
|
|
||
|
def get_servers(self) -> Dict:
|
||
|
try:
|
||
|
response = requests.get(self.api_url, timeout=10)
|
||
|
servers = response.json()
|
||
|
|
||
|
organized = {}
|
||
|
for server in servers:
|
||
|
if server.get('type') == 'wireguard' and server.get('active'):
|
||
|
country = server.get('country_name', 'Unknown')
|
||
|
city = server.get('city_name', 'Unknown')
|
||
|
|
||
|
if country not in organized:
|
||
|
organized[country] = {}
|
||
|
if city not in organized[country]:
|
||
|
organized[country][city] = []
|
||
|
|
||
|
organized[country][city].append({
|
||
|
'hostname': server['hostname'],
|
||
|
'ipv4': server['ipv4_addr_in'],
|
||
|
'ipv6': server.get('ipv6_addr_in'),
|
||
|
'type': 'WireGuard',
|
||
|
'provider': 'Mullvad'
|
||
|
})
|
||
|
|
||
|
self.servers = organized
|
||
|
return organized
|
||
|
|
||
|
except Exception as e:
|
||
|
logging.error(f"Failed to fetch Mullvad servers: {e}")
|
||
|
return {}
|
||
|
|
||
|
def generate_config(self, server_hostname: str) -> str:
|
||
|
server_info = self._find_server(server_hostname)
|
||
|
if not server_info:
|
||
|
raise ValueError(f"Server {server_hostname} not found")
|
||
|
|
||
|
private_key = self._get_or_generate_key()
|
||
|
|
||
|
return f"""# Mullvad WireGuard Configuration
|
||
|
# Server: {server_hostname}
|
||
|
# Provider: Mullvad
|
||
|
|
||
|
[Interface]
|
||
|
PrivateKey = {private_key}
|
||
|
Address = 10.64.0.2/32,fc00:bbbb:bbbb:bb01::2/128
|
||
|
DNS = 100.64.0.1
|
||
|
|
||
|
# PERMANENT KILLSWITCH - CANNOT BE DISABLED
|
||
|
PreUp = iptables -F OUTPUT
|
||
|
PreUp = iptables -F FORWARD
|
||
|
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
|
||
|
PostUp = iptables -I FORWARD -i eth0 -o %i -j ACCEPT
|
||
|
PostUp = iptables -t nat -A POSTROUTING -o %i -j MASQUERADE
|
||
|
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
|
||
|
PostDown = iptables -t nat -D POSTROUTING -o %i -j MASQUERADE
|
||
|
|
||
|
[Peer]
|
||
|
PublicKey = {self.public_key}
|
||
|
AllowedIPs = 0.0.0.0/0,::/0
|
||
|
Endpoint = {server_info['ipv4']}:51820
|
||
|
PersistentKeepalive = 25
|
||
|
"""
|
||
|
|
||
|
def _find_server(self, hostname: str) -> Optional[Dict]:
|
||
|
for country in self.servers.values():
|
||
|
for city in country.values():
|
||
|
for server in city:
|
||
|
if server['hostname'] == hostname:
|
||
|
return server
|
||
|
return None
|
||
|
|
||
|
def _get_or_generate_key(self) -> str:
|
||
|
key_file = f"{WIREGUARD_DIR}/mullvad_private.key"
|
||
|
if os.path.exists(key_file):
|
||
|
with open(key_file, 'r') as f:
|
||
|
return f.read().strip()
|
||
|
else:
|
||
|
private_key = subprocess.check_output(['wg', 'genkey'], text=True).strip()
|
||
|
with open(key_file, 'w') as f:
|
||
|
f.write(private_key)
|
||
|
os.chmod(key_file, 0o600)
|
||
|
return private_key
|
||
|
|
||
|
class CustomWireGuardProvider(VPNProvider):
|
||
|
"""Custom WireGuard servers provider"""
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__("custom")
|
||
|
self.config_file = f"{PROVIDERS_DIR}/custom_servers.json"
|
||
|
self.load_servers()
|
||
|
|
||
|
def load_servers(self):
|
||
|
"""Load custom servers from config file"""
|
||
|
if os.path.exists(self.config_file):
|
||
|
try:
|
||
|
with open(self.config_file, 'r') as f:
|
||
|
self.servers = json.load(f)
|
||
|
except Exception as e:
|
||
|
logging.error(f"Failed to load custom servers: {e}")
|
||
|
self.servers = {}
|
||
|
else:
|
||
|
self.servers = {}
|
||
|
|
||
|
def save_servers(self):
|
||
|
"""Save custom servers to config file"""
|
||
|
os.makedirs(PROVIDERS_DIR, exist_ok=True)
|
||
|
with open(self.config_file, 'w') as f:
|
||
|
json.dump(self.servers, f, indent=2)
|
||
|
|
||
|
def add_server(self, name: str, config: Dict) -> bool:
|
||
|
"""Add a custom WireGuard server"""
|
||
|
try:
|
||
|
location = config.get('location', 'Custom')
|
||
|
|
||
|
if location not in self.servers:
|
||
|
self.servers[location] = {}
|
||
|
|
||
|
if 'Custom' not in self.servers[location]:
|
||
|
self.servers[location]['Custom'] = []
|
||
|
|
||
|
self.servers[location]['Custom'].append({
|
||
|
'hostname': name,
|
||
|
'endpoint': config['endpoint'],
|
||
|
'public_key': config['public_key'],
|
||
|
'private_key': config.get('private_key'),
|
||
|
'address': config.get('address', '10.0.0.2/32'),
|
||
|
'dns': config.get('dns', '1.1.1.1,1.0.0.1'),
|
||
|
'allowed_ips': config.get('allowed_ips', '0.0.0.0/0,::/0'),
|
||
|
'keepalive': config.get('keepalive', 25),
|
||
|
'type': 'WireGuard',
|
||
|
'provider': 'Custom'
|
||
|
})
|
||
|
|
||
|
self.save_servers()
|
||
|
return True
|
||
|
|
||
|
except Exception as e:
|
||
|
logging.error(f"Failed to add custom server: {e}")
|
||
|
return False
|
||
|
|
||
|
def remove_server(self, name: str) -> bool:
|
||
|
"""Remove a custom server"""
|
||
|
for location in self.servers.values():
|
||
|
for city in location.values():
|
||
|
for i, server in enumerate(city):
|
||
|
if server['hostname'] == name:
|
||
|
city.pop(i)
|
||
|
self.save_servers()
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def generate_config(self, server_name: str) -> str:
|
||
|
server_info = self._find_server(server_name)
|
||
|
if not server_info:
|
||
|
raise ValueError(f"Server {server_name} not found")
|
||
|
|
||
|
private_key = server_info.get('private_key')
|
||
|
if not private_key:
|
||
|
private_key = self._get_or_generate_key(server_name)
|
||
|
|
||
|
dns_servers = server_info.get('dns', '1.1.1.1,1.0.0.1')
|
||
|
|
||
|
return f"""# Custom WireGuard Configuration
|
||
|
# Server: {server_name}
|
||
|
# Provider: Custom
|
||
|
|
||
|
[Interface]
|
||
|
PrivateKey = {private_key}
|
||
|
Address = {server_info.get('address', '10.0.0.2/32')}
|
||
|
DNS = {dns_servers}
|
||
|
|
||
|
# PERMANENT KILLSWITCH - CANNOT BE DISABLED
|
||
|
PreUp = iptables -F OUTPUT
|
||
|
PreUp = iptables -F FORWARD
|
||
|
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
|
||
|
PostUp = iptables -I FORWARD -i eth0 -o %i -j ACCEPT
|
||
|
PostUp = iptables -t nat -A POSTROUTING -o %i -j MASQUERADE
|
||
|
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
|
||
|
PostDown = iptables -t nat -D POSTROUTING -o %i -j MASQUERADE
|
||
|
|
||
|
[Peer]
|
||
|
PublicKey = {server_info['public_key']}
|
||
|
AllowedIPs = {server_info.get('allowed_ips', '0.0.0.0/0,::/0')}
|
||
|
Endpoint = {server_info['endpoint']}
|
||
|
PersistentKeepalive = {server_info.get('keepalive', 25)}
|
||
|
"""
|
||
|
|
||
|
def _find_server(self, name: str) -> Optional[Dict]:
|
||
|
for location in self.servers.values():
|
||
|
for city in location.values():
|
||
|
for server in city:
|
||
|
if server['hostname'] == name:
|
||
|
return server
|
||
|
return None
|
||
|
|
||
|
def _get_or_generate_key(self, name: str) -> str:
|
||
|
key_file = f"{WIREGUARD_DIR}/custom_{name}_private.key"
|
||
|
if os.path.exists(key_file):
|
||
|
with open(key_file, 'r') as f:
|
||
|
return f.read().strip()
|
||
|
else:
|
||
|
private_key = subprocess.check_output(['wg', 'genkey'], text=True).strip()
|
||
|
with open(key_file, 'w') as f:
|
||
|
f.write(private_key)
|
||
|
os.chmod(key_file, 0o600)
|
||
|
return private_key
|
||
|
|
||
|
class ImportedConfigProvider(VPNProvider):
|
||
|
"""Provider for imported WireGuard configs"""
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__("imported")
|
||
|
self.configs_dir = f"{PROVIDERS_DIR}/imported"
|
||
|
os.makedirs(self.configs_dir, exist_ok=True)
|
||
|
self.load_configs()
|
||
|
|
||
|
def load_configs(self):
|
||
|
"""Load all imported configs"""
|
||
|
self.servers = {"Imported": {"Configs": []}}
|
||
|
|
||
|
for config_file in Path(self.configs_dir).glob("*.conf"):
|
||
|
name = config_file.stem
|
||
|
self.servers["Imported"]["Configs"].append({
|
||
|
'hostname': name,
|
||
|
'file': str(config_file),
|
||
|
'type': 'WireGuard',
|
||
|
'provider': 'Imported'
|
||
|
})
|
||
|
|
||
|
def import_config(self, name: str, config_content: str) -> bool:
|
||
|
"""Import a WireGuard config"""
|
||
|
try:
|
||
|
# Validate config
|
||
|
if '[Interface]' not in config_content or '[Peer]' not in config_content:
|
||
|
raise ValueError("Invalid WireGuard configuration")
|
||
|
|
||
|
# Add killswitch if not present
|
||
|
if 'PostUp' not in config_content:
|
||
|
config_content = self._add_killswitch(config_content)
|
||
|
|
||
|
# Save config
|
||
|
config_file = f"{self.configs_dir}/{name}.conf"
|
||
|
with open(config_file, 'w') as f:
|
||
|
f.write(config_content)
|
||
|
os.chmod(config_file, 0o600)
|
||
|
|
||
|
self.load_configs()
|
||
|
return True
|
||
|
|
||
|
except Exception as e:
|
||
|
logging.error(f"Failed to import config: {e}")
|
||
|
return False
|
||
|
|
||
|
def _add_killswitch(self, config: str) -> str:
|
||
|
"""Add killswitch rules to imported config"""
|
||
|
killswitch_rules = """
|
||
|
# PERMANENT KILLSWITCH - ADDED AUTOMATICALLY
|
||
|
PreUp = iptables -F OUTPUT
|
||
|
PreUp = iptables -F FORWARD
|
||
|
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
|
||
|
PostUp = iptables -I FORWARD -i eth0 -o %i -j ACCEPT
|
||
|
PostUp = iptables -t nat -A POSTROUTING -o %i -j MASQUERADE
|
||
|
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
|
||
|
PostDown = iptables -t nat -D POSTROUTING -o %i -j MASQUERADE
|
||
|
"""
|
||
|
|
||
|
# Insert after [Interface] section
|
||
|
lines = config.split('\n')
|
||
|
for i, line in enumerate(lines):
|
||
|
if line.strip() == '[Interface]':
|
||
|
# Find next section or end
|
||
|
for j in range(i+1, len(lines)):
|
||
|
if lines[j].strip().startswith('['):
|
||
|
lines.insert(j, killswitch_rules)
|
||
|
break
|
||
|
else:
|
||
|
lines.insert(len(lines), killswitch_rules)
|
||
|
break
|
||
|
|
||
|
return '\n'.join(lines)
|
||
|
|
||
|
def generate_config(self, name: str) -> str:
|
||
|
for server in self.servers["Imported"]["Configs"]:
|
||
|
if server['hostname'] == name:
|
||
|
with open(server['file'], 'r') as f:
|
||
|
return f.read()
|
||
|
raise ValueError(f"Config {name} not found")
|
||
|
|
||
|
# Global provider instances
|
||
|
PROVIDERS = {
|
||
|
'mullvad': MullvadProvider(),
|
||
|
'custom': CustomWireGuardProvider(),
|
||
|
'imported': ImportedConfigProvider()
|
||
|
}
|
||
|
|
||
|
# Current provider
|
||
|
CURRENT_PROVIDER = None
|
||
|
|
||
|
# VPN Status
|
||
|
VPN_STATUS = {
|
||
|
'connected': False,
|
||
|
'provider': None,
|
||
|
'server': None,
|
||
|
'ip': None,
|
||
|
'location': None,
|
||
|
'start_time': None
|
||
|
}
|
||
|
|
||
|
def load_config():
|
||
|
"""Load application configuration"""
|
||
|
global CURRENT_PROVIDER
|
||
|
|
||
|
if os.path.exists(CONFIG_FILE):
|
||
|
try:
|
||
|
with open(CONFIG_FILE, 'r') as f:
|
||
|
config = json.load(f)
|
||
|
provider_name = config.get('provider', 'mullvad')
|
||
|
CURRENT_PROVIDER = PROVIDERS.get(provider_name)
|
||
|
logging.info(f"Loaded provider: {provider_name}")
|
||
|
except Exception as e:
|
||
|
logging.error(f"Failed to load config: {e}")
|
||
|
CURRENT_PROVIDER = PROVIDERS['mullvad']
|
||
|
else:
|
||
|
CURRENT_PROVIDER = PROVIDERS['mullvad']
|
||
|
|
||
|
def save_config():
|
||
|
"""Save application configuration"""
|
||
|
try:
|
||
|
config = {
|
||
|
'provider': CURRENT_PROVIDER.name if CURRENT_PROVIDER else 'mullvad',
|
||
|
'timestamp': datetime.now().isoformat()
|
||
|
}
|
||
|
|
||
|
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
||
|
with open(CONFIG_FILE, 'w') as f:
|
||
|
json.dump(config, f, indent=2)
|
||
|
|
||
|
except Exception as e:
|
||
|
logging.error(f"Failed to save config: {e}")
|
||
|
|
||
|
def check_vpn_status():
|
||
|
"""Check current VPN status"""
|
||
|
global VPN_STATUS
|
||
|
|
||
|
try:
|
||
|
result = subprocess.run(['wg', 'show', 'wg0'], capture_output=True, text=True)
|
||
|
|
||
|
if result.returncode == 0 and 'interface:' in result.stdout.lower():
|
||
|
VPN_STATUS['connected'] = True
|
||
|
|
||
|
# Get public IP and location
|
||
|
try:
|
||
|
# Try Mullvad API first
|
||
|
response = requests.get('https://am.i.mullvad.net/json', timeout=5)
|
||
|
data = response.json()
|
||
|
VPN_STATUS['ip'] = data.get('ip')
|
||
|
|
||
|
if data.get('mullvad_exit_ip'):
|
||
|
VPN_STATUS['location'] = f"{data.get('city')}, {data.get('country')}"
|
||
|
else:
|
||
|
# Fallback to ipinfo
|
||
|
response = requests.get('https://ipinfo.io/json', timeout=5)
|
||
|
data = response.json()
|
||
|
VPN_STATUS['ip'] = data.get('ip')
|
||
|
VPN_STATUS['location'] = f"{data.get('city')}, {data.get('country')}"
|
||
|
except:
|
||
|
pass
|
||
|
else:
|
||
|
VPN_STATUS['connected'] = False
|
||
|
VPN_STATUS['server'] = None
|
||
|
VPN_STATUS['ip'] = None
|
||
|
VPN_STATUS['location'] = None
|
||
|
VPN_STATUS['provider'] = None
|
||
|
|
||
|
except Exception as e:
|
||
|
logging.error(f"Status check error: {e}")
|
||
|
VPN_STATUS['connected'] = False
|
||
|
|
||
|
# Flask Routes
|
||
|
|
||
|
@app.route('/')
|
||
|
def index():
|
||
|
return send_from_directory('/opt/vpn-gateway/static', 'index.html')
|
||
|
|
||
|
@app.route('/api/providers')
|
||
|
def get_providers():
|
||
|
"""Get available providers"""
|
||
|
return jsonify({
|
||
|
'providers': list(PROVIDERS.keys()),
|
||
|
'current': CURRENT_PROVIDER.name if CURRENT_PROVIDER else None
|
||
|
})
|
||
|
|
||
|
@app.route('/api/provider/<provider_name>', methods=['POST'])
|
||
|
def set_provider(provider_name):
|
||
|
"""Switch provider"""
|
||
|
global CURRENT_PROVIDER
|
||
|
|
||
|
if provider_name not in PROVIDERS:
|
||
|
return jsonify({'success': False, 'error': 'Invalid provider'}), 400
|
||
|
|
||
|
# Disconnect if connected
|
||
|
if VPN_STATUS['connected']:
|
||
|
subprocess.run(['wg-quick', 'down', 'wg0'], capture_output=True)
|
||
|
|
||
|
CURRENT_PROVIDER = PROVIDERS[provider_name]
|
||
|
save_config()
|
||
|
|
||
|
return jsonify({'success': True, 'provider': provider_name})
|
||
|
|
||
|
@app.route('/api/servers')
|
||
|
def get_servers():
|
||
|
"""Get servers for current provider"""
|
||
|
if not CURRENT_PROVIDER:
|
||
|
return jsonify({'servers': {}})
|
||
|
|
||
|
servers = CURRENT_PROVIDER.get_servers()
|
||
|
return jsonify({
|
||
|
'servers': servers,
|
||
|
'provider': CURRENT_PROVIDER.name
|
||
|
})
|
||
|
|
||
|
@app.route('/api/custom/add', methods=['POST'])
|
||
|
def add_custom_server():
|
||
|
"""Add custom WireGuard server"""
|
||
|
if not isinstance(CURRENT_PROVIDER, CustomWireGuardProvider):
|
||
|
return jsonify({'success': False, 'error': 'Not in custom mode'}), 400
|
||
|
|
||
|
data = request.json
|
||
|
name = data.get('name')
|
||
|
config = {
|
||
|
'endpoint': data.get('endpoint'),
|
||
|
'public_key': data.get('public_key'),
|
||
|
'private_key': data.get('private_key'),
|
||
|
'address': data.get('address', '10.0.0.2/32'),
|
||
|
'dns': data.get('dns', '1.1.1.1,1.0.0.1'),
|
||
|
'allowed_ips': data.get('allowed_ips', '0.0.0.0/0,::/0'),
|
||
|
'location': data.get('location', 'Custom')
|
||
|
}
|
||
|
|
||
|
if CURRENT_PROVIDER.add_server(name, config):
|
||
|
return jsonify({'success': True})
|
||
|
else:
|
||
|
return jsonify({'success': False, 'error': 'Failed to add server'}), 500
|
||
|
|
||
|
@app.route('/api/custom/remove/<name>', methods=['DELETE'])
|
||
|
def remove_custom_server(name):
|
||
|
"""Remove custom server"""
|
||
|
if not isinstance(CURRENT_PROVIDER, CustomWireGuardProvider):
|
||
|
return jsonify({'success': False, 'error': 'Not in custom mode'}), 400
|
||
|
|
||
|
if CURRENT_PROVIDER.remove_server(name):
|
||
|
return jsonify({'success': True})
|
||
|
else:
|
||
|
return jsonify({'success': False, 'error': 'Server not found'}), 404
|
||
|
|
||
|
@app.route('/api/import', methods=['POST'])
|
||
|
def import_config():
|
||
|
"""Import WireGuard config"""
|
||
|
data = request.json
|
||
|
name = data.get('name')
|
||
|
config_content = data.get('config')
|
||
|
|
||
|
if not name or not config_content:
|
||
|
return jsonify({'success': False, 'error': 'Missing name or config'}), 400
|
||
|
|
||
|
provider = PROVIDERS['imported']
|
||
|
if provider.import_config(name, config_content):
|
||
|
return jsonify({'success': True})
|
||
|
else:
|
||
|
return jsonify({'success': False, 'error': 'Failed to import config'}), 500
|
||
|
|
||
|
@app.route('/api/status')
|
||
|
def get_status():
|
||
|
"""Get VPN status"""
|
||
|
check_vpn_status()
|
||
|
|
||
|
uptime = None
|
||
|
if VPN_STATUS['connected'] and VPN_STATUS['start_time']:
|
||
|
uptime_seconds = int(time.time() - VPN_STATUS['start_time'])
|
||
|
hours = uptime_seconds // 3600
|
||
|
minutes = (uptime_seconds % 3600) // 60
|
||
|
uptime = f"{hours}h {minutes}m"
|
||
|
|
||
|
return jsonify({
|
||
|
'connected': VPN_STATUS['connected'],
|
||
|
'provider': VPN_STATUS['provider'],
|
||
|
'server': VPN_STATUS['server'],
|
||
|
'ip': VPN_STATUS['ip'],
|
||
|
'location': VPN_STATUS['location'],
|
||
|
'uptime': uptime,
|
||
|
'killswitch_active': True # Always true
|
||
|
})
|
||
|
|
||
|
@app.route('/api/connect', methods=['POST'])
|
||
|
def connect_vpn():
|
||
|
"""Connect to VPN"""
|
||
|
data = request.json
|
||
|
server = data.get('server')
|
||
|
|
||
|
if not server or not CURRENT_PROVIDER:
|
||
|
return jsonify({'success': False, 'error': 'No server or provider selected'}), 400
|
||
|
|
||
|
try:
|
||
|
# Disconnect if connected
|
||
|
subprocess.run(['wg-quick', 'down', 'wg0'], capture_output=True)
|
||
|
time.sleep(1)
|
||
|
|
||
|
# Generate config
|
||
|
config = CURRENT_PROVIDER.generate_config(server)
|
||
|
|
||
|
# Write config
|
||
|
with open('/etc/wireguard/wg0.conf', 'w') as f:
|
||
|
f.write(config)
|
||
|
os.chmod('/etc/wireguard/wg0.conf', 0o600)
|
||
|
|
||
|
# Add firewall exception for endpoint
|
||
|
endpoint_match = re.search(r'Endpoint = ([\d.]+):', config)
|
||
|
if endpoint_match:
|
||
|
subprocess.run([
|
||
|
'iptables', '-I', 'OUTPUT', '1', '-p', 'udp',
|
||
|
'--dport', '51820', '-d', endpoint_match.group(1), '-j', 'ACCEPT'
|
||
|
])
|
||
|
|
||
|
# Connect
|
||
|
result = subprocess.run(['wg-quick', 'up', 'wg0'],
|
||
|
capture_output=True, text=True)
|
||
|
|
||
|
if result.returncode == 0:
|
||
|
VPN_STATUS['start_time'] = time.time()
|
||
|
VPN_STATUS['server'] = server
|
||
|
VPN_STATUS['provider'] = CURRENT_PROVIDER.name
|
||
|
|
||
|
logging.info(f"Connected to {server} via {CURRENT_PROVIDER.name}")
|
||
|
return jsonify({'success': True})
|
||
|
else:
|
||
|
logging.error(f"Connection failed: {result.stderr}")
|
||
|
return jsonify({'success': False, 'error': result.stderr}), 500
|
||
|
|
||
|
except Exception as e:
|
||
|
logging.error(f"Connect error: {e}")
|
||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
|
||
|
@app.route('/api/disconnect', methods=['POST'])
|
||
|
def disconnect_vpn():
|
||
|
"""Disconnect VPN"""
|
||
|
try:
|
||
|
result = subprocess.run(['wg-quick', 'down', 'wg0'],
|
||
|
capture_output=True, text=True)
|
||
|
|
||
|
VPN_STATUS['start_time'] = None
|
||
|
VPN_STATUS['connected'] = False
|
||
|
VPN_STATUS['provider'] = None
|
||
|
|
||
|
return jsonify({
|
||
|
'success': result.returncode == 0,
|
||
|
'message': 'Disconnected - No internet (killswitch active)'
|
||
|
})
|
||
|
|
||
|
except Exception as e:
|
||
|
logging.error(f"Disconnect error: {e}")
|
||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
|
||
|
@app.route('/api/keypair', methods=['GET'])
|
||
|
def generate_keypair():
|
||
|
"""Generate WireGuard keypair"""
|
||
|
try:
|
||
|
private_key = subprocess.check_output(['wg', 'genkey'], text=True).strip()
|
||
|
public_key = subprocess.check_output(['wg', 'pubkey'], input=private_key, text=True).strip()
|
||
|
|
||
|
return jsonify({
|
||
|
'private_key': private_key,
|
||
|
'public_key': public_key
|
||
|
})
|
||
|
except Exception as e:
|
||
|
return jsonify({'error': str(e)}), 500
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
# Load configuration
|
||
|
load_config()
|
||
|
|
||
|
# Create directories
|
||
|
os.makedirs('/opt/vpn-gateway/static', exist_ok=True)
|
||
|
os.makedirs(PROVIDERS_DIR, exist_ok=True)
|
||
|
|
||
|
# Start app
|
||
|
app.run(host='0.0.0.0', port=5000, debug=False)
|