mvpg/install.sh
2025-08-10 15:34:34 +02:00

1408 lines
41 KiB
Bash

#!/bin/bash
#############################################################
# #
# Mullvad VPN Gateway Installer for LXC #
# Secure VPN Gateway with Permanent Killswitch #
# #
# Usage: curl -sSL https://your-domain/install.sh | bash #
# #
#############################################################
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Configuration variables
INSTALL_DIR="/opt/vpn-gateway"
LOG_FILE="/var/log/vpn-gateway-install.log"
GITHUB_REPO="https://raw.githubusercontent.com/yourusername/vpn-gateway/main"
VERSION="1.0.0"
# System variables (will be auto-detected)
LAN_INTERFACE=""
LAN_IP=""
LAN_NETWORK=""
CONTAINER_TYPE=""
MULLVAD_ACCOUNT=""
# VPN Provider variables
VPN_PROVIDER=""
WG_CONFIG_PATH=""
WG_IMPORTED_CONFIG=""
WG_CUSTOM_LOCATION=""
WG_CUSTOM_NAME=""
WG_ENDPOINT=""
WG_SERVER_PUBKEY=""
WG_CLIENT_PRIVKEY=""
WG_CLIENT_IP=""
WG_DNS=""
WG_ALLOWED_IPS=""
BACKUP_SERVERS=()
# ASCII Art Banner
show_banner() {
clear
echo -e "${CYAN}"
cat << "EOF"
__ __ _ _ _ __ ______ _ _
| \/ | | | | | | \ \ / / _ \| \ | |
| \ / |_ _| | |_ ____ _ ___| | \ \ / /| |_) | \| |
| |\/| | | | | | \ \ / / _` |/ _ | | \ V / | __/| . ` |
| | | | |_| | | |\ V | (_| | __| | | | | | | |\ |
|_| |_|\__,_|_|_| \_/ \__,_|\___|_| |_| |_| |_| \_|
Secure Gateway with Permanent Killswitch
EOF
echo -e "${NC}"
echo -e "${BOLD}Version:${NC} ${VERSION}"
echo -e "${BOLD}GitHub:${NC} github.com/yourusername/vpn-gateway"
echo ""
}
# Logging functions
log() {
echo -e "${GREEN}[+]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
error() {
echo -e "${RED}[!]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE"
}
warning() {
echo -e "${YELLOW}[*]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$LOG_FILE"
}
info() {
echo -e "${BLUE}[i]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE"
}
# Progress spinner
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
exit 1
fi
}
# Detect container type
detect_container() {
info "Detecting container type..."
if [ -f /run/systemd/container ]; then
CONTAINER_TYPE=$(cat /run/systemd/container)
elif [ -f /proc/1/environ ]; then
if grep -q lxc /proc/1/environ; then
CONTAINER_TYPE="lxc"
elif grep -q docker /proc/1/environ; then
CONTAINER_TYPE="docker"
fi
fi
if [ -n "$CONTAINER_TYPE" ]; then
log "Container type: $CONTAINER_TYPE"
else
warning "Could not detect container type, assuming LXC"
CONTAINER_TYPE="lxc"
fi
}
# Auto-detect network configuration
detect_network() {
info "Auto-detecting network configuration..."
# Find primary network interface (excluding lo and wg*)
LAN_INTERFACE=$(ip route | grep default | awk '{print $5}' | head -n1)
if [ -z "$LAN_INTERFACE" ]; then
# Fallback: find first UP interface
LAN_INTERFACE=$(ip link show | grep "state UP" | grep -v "lo:" | awk -F': ' '{print $2}' | head -n1)
fi
if [ -z "$LAN_INTERFACE" ]; then
error "Could not detect network interface"
read -p "Please enter your network interface (e.g., eth0): " LAN_INTERFACE
fi
# Get IP address
LAN_IP=$(ip -4 addr show "$LAN_INTERFACE" | grep inet | awk '{print $2}' | cut -d/ -f1)
# Get network subnet
LAN_NETWORK=$(ip route | grep "$LAN_INTERFACE" | grep -v default | awk '{print $1}' | head -n1)
if [ -z "$LAN_NETWORK" ]; then
# Calculate from IP
LAN_NETWORK=$(echo "$LAN_IP" | cut -d. -f1-3).0/24
fi
echo ""
echo -e "${CYAN}Detected Network Configuration:${NC}"
echo -e " Interface: ${BOLD}$LAN_INTERFACE${NC}"
echo -e " IP Address: ${BOLD}$LAN_IP${NC}"
echo -e " Network: ${BOLD}$LAN_NETWORK${NC}"
echo ""
read -p "Is this correct? (Y/n): " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]] && [ -n "$REPLY" ]; then
read -p "Enter network interface: " LAN_INTERFACE
read -p "Enter network subnet (e.g., 192.168.1.0/24): " LAN_NETWORK
LAN_IP=$(ip -4 addr show "$LAN_INTERFACE" | grep inet | awk '{print $2}' | cut -d/ -f1)
fi
}
# Choose VPN provider
choose_vpn_provider() {
echo ""
echo -e "${CYAN}VPN Provider Selection${NC}"
echo ""
echo -e " ${BOLD}1)${NC} Mullvad VPN (Commercial Provider)"
echo -e " ${BOLD}2)${NC} Custom WireGuard (Own Server/VPS)"
echo -e " ${BOLD}3)${NC} Import existing WireGuard config"
echo ""
while true; do
read -p "Select provider [1-3]: " VPN_PROVIDER_CHOICE
case $VPN_PROVIDER_CHOICE in
1)
VPN_PROVIDER="mullvad"
get_mullvad_account
break
;;
2)
VPN_PROVIDER="custom"
get_custom_wireguard_details
break
;;
3)
VPN_PROVIDER="import"
import_wireguard_config
break
;;
*)
error "Invalid choice. Please select 1, 2, or 3."
;;
esac
done
log "VPN provider selected: $VPN_PROVIDER"
}
# Get Mullvad account
get_mullvad_account() {
echo ""
echo -e "${CYAN}Mullvad VPN Account Setup${NC}"
echo -e "You need a Mullvad account number to continue."
echo -e "Get one at: ${BOLD}https://mullvad.net${NC}"
echo ""
while [ -z "$MULLVAD_ACCOUNT" ]; do
read -p "Enter your Mullvad account number: " MULLVAD_ACCOUNT
# Validate format (16 digits)
if [[ ! "$MULLVAD_ACCOUNT" =~ ^[0-9]{16}$ ]]; then
error "Invalid account format. Should be 16 digits."
MULLVAD_ACCOUNT=""
fi
done
log "Mullvad account configured"
}
# Get custom WireGuard details
get_custom_wireguard_details() {
echo ""
echo -e "${CYAN}Custom WireGuard Configuration${NC}"
echo -e "Configure your own WireGuard server/VPS"
echo ""
# Server endpoint
while [ -z "$WG_ENDPOINT" ]; do
read -p "Server endpoint (IP:Port or domain:port): " WG_ENDPOINT
if [[ ! "$WG_ENDPOINT" =~ ^[^:]+:[0-9]+$ ]]; then
error "Invalid format. Use IP:Port or domain:port (e.g., 1.2.3.4:51820)"
WG_ENDPOINT=""
fi
done
# Server public key
while [ -z "$WG_SERVER_PUBKEY" ]; do
read -p "Server public key: " WG_SERVER_PUBKEY
if [[ ! "$WG_SERVER_PUBKEY" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then
error "Invalid public key format"
WG_SERVER_PUBKEY=""
fi
done
# Client private key (optional - generate if not provided)
read -p "Client private key (leave empty to generate): " WG_CLIENT_PRIVKEY
if [ -z "$WG_CLIENT_PRIVKEY" ]; then
WG_CLIENT_PRIVKEY=$(wg genkey)
WG_CLIENT_PUBKEY=$(echo "$WG_CLIENT_PRIVKEY" | wg pubkey)
echo -e "${GREEN}Generated keypair:${NC}"
echo -e " Private key: ${BOLD}$WG_CLIENT_PRIVKEY${NC}"
echo -e " Public key: ${BOLD}$WG_CLIENT_PUBKEY${NC}"
echo -e "${YELLOW}Add this public key to your server's peer configuration!${NC}"
read -p "Press Enter when you've added the key to your server..."
fi
# Client IP in VPN
read -p "Client VPN IP (e.g., 10.0.0.2/32): " WG_CLIENT_IP
if [ -z "$WG_CLIENT_IP" ]; then
WG_CLIENT_IP="10.0.0.2/32"
fi
# DNS servers
read -p "DNS servers (comma-separated, default: 1.1.1.1,1.0.0.1): " WG_DNS
if [ -z "$WG_DNS" ]; then
WG_DNS="1.1.1.1,1.0.0.1"
fi
# Allowed IPs
read -p "Allowed IPs (default: 0.0.0.0/0): " WG_ALLOWED_IPS
if [ -z "$WG_ALLOWED_IPS" ]; then
WG_ALLOWED_IPS="0.0.0.0/0"
fi
# Ask about kill switch bypass
echo ""
read -p "Allow direct access to server IP (bypass killswitch)? (Y/n): " -n 1 -r
echo ""
WG_DNS="1.1.1.1,1.0.0.1"
fi
# AllowedIPs
read -p "Route all traffic through VPN? (Y/n): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Nn]$ ]]; then
read -p "Allowed IPs (comma-separated): " WG_ALLOWED_IPS
else
WG_ALLOWED_IPS="0.0.0.0/0,::/0"
fi
# Optional: Multiple servers for failover
read -p "Add backup servers? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
BACKUP_SERVERS=()
while true; do
read -p "Backup server endpoint (or 'done' to finish): " BACKUP
if [ "$BACKUP" = "done" ] || [ -z "$BACKUP" ]; then
break
fi
BACKUP_SERVERS+=("$BACKUP")
done
fi
# Save custom server name for later use
WG_CUSTOM_NAME="${WG_ENDPOINT%:*}_custom"
WG_CUSTOM_LOCATION="Custom Location"
log "Custom WireGuard configuration complete"
}
# Import existing WireGuard config
import_wireguard_config() {
echo ""
echo -e "${CYAN}Import WireGuard Configuration${NC}"
echo ""
read -p "Path to WireGuard config file: " WG_CONFIG_PATH
if [ ! -f "$WG_CONFIG_PATH" ]; then
error "File not found: $WG_CONFIG_PATH"
choose_vpn_provider
return
fi
# Validate config
if ! grep -q "\[Interface\]" "$WG_CONFIG_PATH" || ! grep -q "\[Peer\]" "$WG_CONFIG_PATH"; then
error "Invalid WireGuard configuration file"
choose_vpn_provider
return
fi
# Copy config
cp "$WG_CONFIG_PATH" /tmp/imported_wg.conf
WG_IMPORTED_CONFIG="/tmp/imported_wg.conf"
log "WireGuard config imported successfully"
}
# Choose VPN provider
choose_vpn_provider() {
echo ""
echo -e "${CYAN}Choose your VPN provider:${NC}"
echo "1) Mullvad VPN"
echo "2) Custom WireGuard Server"
echo "3) Import existing WireGuard config"
echo ""
while true; do
read -p "Select option (1-3): " choice
case $choice in
1)
VPN_PROVIDER="mullvad"
get_mullvad_account
break
;;
2)
VPN_PROVIDER="custom"
get_custom_wireguard_details
break
;;
3)
VPN_PROVIDER="import"
import_wireguard_config
break
;;
*)
error "Invalid choice. Please select 1, 2, or 3."
;;
esac
done
}
# Check system requirements
check_requirements() {
info "Checking system requirements..."
# Check OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
fi
if [[ ! "$OS" =~ "Ubuntu" ]] && [[ ! "$OS" =~ "Debian" ]]; then
warning "This script is tested on Ubuntu/Debian. Continue at your own risk."
read -p "Continue anyway? (y/N): " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Check kernel modules for WireGuard
if ! lsmod | grep -q wireguard; then
warning "WireGuard kernel module not loaded"
modprobe wireguard 2>/dev/null || true
fi
log "System requirements checked"
}
# Install dependencies
install_dependencies() {
log "Installing dependencies..."
export DEBIAN_FRONTEND=noninteractive
# Update package lists
apt-get update &>/dev/null &
spinner $!
# Install required packages
local packages=(
wireguard
wireguard-tools
iptables
iptables-persistent
python3
python3-pip
python3-venv
nginx
curl
wget
git
resolvconf
net-tools
jq
)
for package in "${packages[@]}"; do
echo -n " Installing $package..."
apt-get install -y "$package" &>/dev/null &
spinner $!
echo " ✓"
done
log "Dependencies installed successfully"
}
# Create directory structure
create_directories() {
log "Creating directory structure..."
mkdir -p "$INSTALL_DIR"/{scripts,config,static,logs}
mkdir -p /etc/wireguard
mkdir -p /var/log
log "Directories created"
}
# Install killswitch script
install_killswitch() {
log "Installing permanent killswitch..."
# Create killswitch script
cat > /usr/local/bin/vpn-killswitch.sh << 'EOFSRIPT'
#!/bin/bash
LAN_IF="__LAN_INTERFACE__"
LAN_NET="__LAN_NETWORK__"
enable_killswitch() {
echo "ENABLING PERMANENT KILLSWITCH - NO INTERNET WITHOUT VPN"
# Reset all rules
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
# DEFAULT: BLOCK EVERYTHING
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Allow LAN communication
iptables -A INPUT -i $LAN_IF -s $LAN_NET -j ACCEPT
iptables -A OUTPUT -o $LAN_IF -d $LAN_NET -j ACCEPT
# Allow DNS for initial connection (root only)
iptables -A OUTPUT -p udp --dport 53 -m owner --uid-owner root -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -m owner --uid-owner root -j ACCEPT
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow forwarding from LAN to VPN (when active)
iptables -A FORWARD -i $LAN_IF -s $LAN_NET -j ACCEPT
# Block IPv6 completely
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT DROP
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A OUTPUT -o lo -j ACCEPT
# Save rules
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
echo "KILLSWITCH ACTIVE - System is now protected"
}
disable_killswitch() {
echo "WARNING: Killswitch cannot be disabled for security!"
enable_killswitch
}
case "$1" in
enable|disable)
enable_killswitch
;;
*)
echo "Usage: $0 {enable|disable}"
exit 1
;;
esac
EOFSCRIPT
# Replace placeholders
sed -i "s|__LAN_INTERFACE__|$LAN_INTERFACE|g" /usr/local/bin/vpn-killswitch.sh
sed -i "s|__LAN_NETWORK__|$LAN_NETWORK|g" /usr/local/bin/vpn-killswitch.sh
chmod +x /usr/local/bin/vpn-killswitch.sh
# Create systemd service
cat > /etc/systemd/system/vpn-killswitch.service << 'EOF'
[Unit]
Description=VPN Killswitch - Block ALL traffic except through VPN
DefaultDependencies=no
Before=network-pre.target
Wants=network-pre.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/vpn-killswitch.sh enable
[Install]
WantedBy=sysinit.target
EOF
# Enable and start killswitch
systemctl daemon-reload
systemctl enable vpn-killswitch.service
systemctl start vpn-killswitch.service
log "Killswitch installed and activated"
}
# Install VPN provider specific components
install_vpn_provider() {
log "Installing VPN provider components..."
case "$VPN_PROVIDER" in
mullvad)
install_mullvad
;;
custom)
setup_custom_provider
;;
import)
setup_import_provider
;;
esac
}
# Install Mullvad
install_mullvad() {
log "Installing Mullvad client..."
# Download Mullvad signing key
curl -fsSL https://mullvad.net/media/mullvad-code-signing.asc | gpg --dearmor -o /usr/share/keyrings/mullvad-keyring.gpg
# Add Mullvad repository
echo "deb [signed-by=/usr/share/keyrings/mullvad-keyring.gpg arch=$( dpkg --print-architecture )] https://repository.mullvad.net/deb/stable $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/mullvad.list
# Update and install
apt-get update &>/dev/null
apt-get install -y mullvad-vpn &>/dev/null || {
warning "Could not install Mullvad client, using WireGuard directly"
}
# Login to Mullvad
if command -v mullvad &>/dev/null; then
mullvad account login "$MULLVAD_ACCOUNT" &>/dev/null || {
warning "Could not login to Mullvad account"
}
fi
# Generate WireGuard keys
if [ ! -f /etc/wireguard/mullvad_private.key ]; then
wg genkey | tee /etc/wireguard/mullvad_private.key | wg pubkey > /etc/wireguard/mullvad_public.key
chmod 600 /etc/wireguard/mullvad_private.key
fi
# Save config
echo "mullvad" > "$INSTALL_DIR/provider.conf"
echo "$MULLVAD_ACCOUNT" > "$INSTALL_DIR/.mullvad_account"
chmod 600 "$INSTALL_DIR/.mullvad_account"
log "Mullvad configuration complete"
}
# Setup custom provider
setup_custom_provider() {
log "Setting up custom WireGuard provider..."
# Create config file for custom servers
mkdir -p "$INSTALL_DIR/providers"
# Save the custom server configuration
cat > "$INSTALL_DIR/providers/custom_servers.json" << EOF
{
"$WG_CUSTOM_LOCATION": {
"Custom": [{
"hostname": "$WG_CUSTOM_NAME",
"endpoint": "$WG_ENDPOINT",
"public_key": "$WG_SERVER_PUBKEY",
"private_key": "$WG_CLIENT_PRIVKEY",
"address": "$WG_CLIENT_IP",
"dns": "$WG_DNS",
"allowed_ips": "$WG_ALLOWED_IPS",
"type": "WireGuard",
"provider": "Custom"
}]
}
}
EOF
# Save provider config
echo "custom" > "$INSTALL_DIR/provider.conf"
# If backup servers were provided
if [ ${#BACKUP_SERVERS[@]} -gt 0 ]; then
log "Adding backup servers..."
# Additional logic for backup servers
fi
log "Custom provider setup complete"
}
# Setup import provider
setup_import_provider() {
log "Setting up import provider..."
mkdir -p "$INSTALL_DIR/providers/imported"
# Copy imported config
if [ -n "$WG_IMPORTED_CONFIG" ]; then
cp "$WG_IMPORTED_CONFIG" "$INSTALL_DIR/providers/imported/"
log "Imported configuration saved"
fi
# Save provider config
echo "import" > "$INSTALL_DIR/provider.conf"
log "Import provider setup complete"
}
# Install VPN provider (unified function)
install_vpn_provider() {
case "$VPN_PROVIDER" in
"mullvad")
setup_mullvad_provider
;;
"custom")
setup_custom_provider
;;
"import")
setup_import_provider
;;
*)
error "Unknown VPN provider: $VPN_PROVIDER"
exit 1
;;
esac
}
# Install Python backend
install_backend() {
log "Installing VPN Gateway backend..."
# Ensure install directory exists
mkdir -p "$INSTALL_DIR"
# Create virtual environment
python3 -m venv "$INSTALL_DIR/venv"
source "$INSTALL_DIR/venv/bin/activate"
# Install Python packages
pip install --upgrade pip &>/dev/null
pip install flask flask-cors requests gunicorn pyyaml &>/dev/null
# Download or create the multi-provider backend
# For production, this would be downloaded from GitHub
# Here we create it inline for the complete solution
log "Installing multi-provider backend..."
# The backend app.py would be downloaded from GitHub in production:
# wget -O "$INSTALL_DIR/app.py" "$GITHUB_REPO/app.py"
# For now, we use a simplified version that supports all providers
cat > "$INSTALL_DIR/app.py" << 'EOFAPP'
#!/usr/bin/env python3
# Multi-Provider VPN Backend
# This is a placeholder - in production, use the full backend from the artifact
# Download from: https://github.com/yourusername/vpn-gateway/blob/main/backend/app.py
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
import subprocess
import json
import os
import logging
app = Flask(__name__)
CORS(app)
# ... (Full backend code would be here)
# For production, download the complete multi-provider backend
@app.route('/')
def index():
return send_from_directory('__INSTALL_DIR__/static', 'index.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOFAPP
# Replace placeholders
sed -i "s|__INSTALL_DIR__|$INSTALL_DIR|g" "$INSTALL_DIR/app.py"
log "Backend installed"
}
#!/usr/bin/env python3
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 logging
from pathlib import Path
app = Flask(__name__)
CORS(app)
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/log/vpn-gateway.log'),
logging.StreamHandler()
]
)
MULLVAD_SERVERS = {}
LAST_SERVER_UPDATE = 0
VPN_STATUS = {
'connected': False,
'server': None,
'ip': None,
'location': None,
'start_time': None
}
def update_mullvad_servers():
global MULLVAD_SERVERS, LAST_SERVER_UPDATE
try:
response = requests.get('https://api.mullvad.net/www/relays/all/', 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'],
'type': 'WireGuard'
})
global MULLVAD_SERVERS
MULLVAD_SERVERS = organized
LAST_SERVER_UPDATE = time.time()
return True
except Exception as e:
logging.error(f"Failed to update servers: {e}")
return False
def generate_wireguard_config(server_hostname):
try:
server_info = None
for country in MULLVAD_SERVERS.values():
for city in country.values():
for server in city:
if server['hostname'] == server_hostname:
server_info = server
break
if not server_info:
return False
with open('/etc/wireguard/mullvad_private.key', 'r') as f:
private_key = f.read().strip()
# Mullvad public key
mullvad_pubkey = "g+9JNZp3SvLPvBb+PzXHyOPHhqNiUdATrz1YdNEPvWo="
config = f"""[Interface]
PrivateKey = {private_key}
Address = 10.64.0.2/32,fc00:bbbb:bbbb:bb01::2/128
DNS = 100.64.0.1
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 __LAN_INTERFACE__ -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 = {mullvad_pubkey}
AllowedIPs = 0.0.0.0/0,::/0
Endpoint = {server_info['ipv4']}:51820
PersistentKeepalive = 25
"""
# Add firewall exception for this server
subprocess.run([
'iptables', '-I', 'OUTPUT', '1', '-p', 'udp',
'--dport', '51820', '-d', server_info['ipv4'], '-j', 'ACCEPT'
])
with open('/etc/wireguard/wg0.conf', 'w') as f:
f.write(config)
os.chmod('/etc/wireguard/wg0.conf', 0o600)
return True
except Exception as e:
logging.error(f"Failed to generate config: {e}")
return False
def check_vpn_status():
global VPN_STATUS
try:
result = subprocess.run(['wg', 'show', 'wg0'], capture_output=True, text=True)
if result.returncode == 0:
VPN_STATUS['connected'] = True
# Get public IP
try:
response = requests.get('https://am.i.mullvad.net/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
except:
VPN_STATUS['connected'] = False
@app.route('/')
def index():
return send_from_directory('__INSTALL_DIR__/static', 'index.html')
@app.route('/api/servers')
def get_servers():
global LAST_SERVER_UPDATE
if time.time() - LAST_SERVER_UPDATE > 3600:
update_mullvad_servers()
return jsonify({'servers': MULLVAD_SERVERS})
@app.route('/api/status')
def get_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'],
'server': VPN_STATUS['server'],
'ip': VPN_STATUS['ip'],
'location': VPN_STATUS['location'],
'uptime': uptime
})
@app.route('/api/connect', methods=['POST'])
def connect_vpn():
data = request.json
server = data.get('server')
try:
subprocess.run(['wg-quick', 'down', 'wg0'], capture_output=True)
if not generate_wireguard_config(server):
return jsonify({'success': False, 'error': 'Failed to generate config'})
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
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': result.stderr})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/disconnect', methods=['POST'])
def disconnect_vpn():
try:
result = subprocess.run(['wg-quick', 'down', 'wg0'], capture_output=True, text=True)
VPN_STATUS['start_time'] = None
return jsonify({'success': result.returncode == 0})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
if __name__ == '__main__':
update_mullvad_servers()
app.run(host='0.0.0.0', port=5000)
EOFAPP
# Replace placeholders
sed -i "s|__LAN_INTERFACE__|$LAN_INTERFACE|g" "$INSTALL_DIR/app.py"
sed -i "s|__INSTALL_DIR__|$INSTALL_DIR|g" "$INSTALL_DIR/app.py"
log "Backend installed"
}
# Setup nginx (placeholder function)
setup_nginx() {
log "Setting up nginx reverse proxy..."
# This would contain nginx configuration
# For now, we'll skip this as it's optional
log "Nginx setup skipped (optional)"
}
# Finalize installation
finalize_installation() {
log "Finalizing installation..."
# Set proper permissions
chown -R root:root "$INSTALL_DIR"
chmod +x "$INSTALL_DIR"/scripts/*.sh 2>/dev/null || true
# Enable and start services
systemctl enable vpn-webui vpn-killswitch vpn-security-monitor
systemctl start vpn-killswitch
systemctl start vpn-security-monitor
systemctl start vpn-webui
log "Installation finalized"
}
# Show installation summary
show_summary() {
echo ""
echo -e "${GREEN}${BOLD}Installation Complete!${NC}"
echo ""
echo -e "${CYAN}Services Status:${NC}"
systemctl is-active vpn-webui && echo -e " WebUI: ${GREEN}Running${NC}" || echo -e " WebUI: ${RED}Stopped${NC}"
systemctl is-active vpn-killswitch && echo -e " Killswitch: ${GREEN}Active${NC}" || echo -e " Killswitch: ${RED}Inactive${NC}"
systemctl is-active vpn-security-monitor && echo -e " Security Monitor: ${GREEN}Running${NC}" || echo -e " Security Monitor: ${RED}Stopped${NC}"
echo ""
echo -e "${CYAN}Access Information:${NC}"
echo -e " WebUI URL: ${BOLD}http://$LAN_IP:5000${NC}"
echo -e " Install Dir: ${BOLD}$INSTALL_DIR${NC}"
echo -e " Log File: ${BOLD}$LOG_FILE${NC}"
echo ""
echo -e "${YELLOW}Important Notes:${NC}"
echo -e " • The killswitch is ${BOLD}PERMANENTLY ACTIVE${NC}"
echo -e " • No internet access without VPN connection"
echo -e " • Use the WebUI to connect to VPN servers"
echo ""
}
# Install WebUI
install_webui() {
log "Installing WebUI..."
# Download WebUI from GitHub or create inline
cat > "$INSTALL_DIR/static/index.html" << 'EOFHTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPN Gateway Control</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 800px;
width: 100%;
padding: 40px;
}
h1 { text-align: center; margin-bottom: 30px; }
.status-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
}
select, button {
width: 100%;
padding: 15px;
margin: 10px 0;
border-radius: 10px;
font-size: 16px;
}
button {
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
color: white;
border: none;
cursor: pointer;
}
button:hover { transform: translateY(-2px); }
.btn-disconnect {
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
}
.security-notice {
background: #4ade80;
color: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 VPN Gateway Control</h1>
<div class="security-notice">
<strong>🛡️ Security Status: PROTECTED</strong><br>
✓ Killswitch permanently active<br>
✓ No internet without VPN
</div>
<div class="status-card">
<h2>Status: <span id="status">Checking...</span></h2>
<p>IP: <span id="ip">-</span></p>
<p>Location: <span id="location">-</span></p>
</div>
<select id="countrySelect" onchange="updateCities()">
<option>Loading countries...</option>
</select>
<select id="citySelect" onchange="updateServers()">
<option>Select country first</option>
</select>
<select id="serverSelect">
<option>Select city first</option>
</select>
<button onclick="connectVPN()">Connect</button>
<button class="btn-disconnect" onclick="disconnectVPN()">Disconnect</button>
</div>
<script>
let servers = {};
async function loadServers() {
const response = await fetch('/api/servers');
const data = await response.json();
servers = data.servers;
const countrySelect = document.getElementById('countrySelect');
countrySelect.innerHTML = '<option value="">Select country</option>';
Object.keys(servers).forEach(country => {
countrySelect.innerHTML += '<option value="' + country + '">' + country + '</option>';
});
}
function updateCities() {
const country = document.getElementById('countrySelect').value;
const citySelect = document.getElementById('citySelect');
if (country && servers[country]) {
citySelect.innerHTML = '<option value="">Select city</option>';
Object.keys(servers[country]).forEach(city => {
citySelect.innerHTML += '<option value="' + city + '">' + city + '</option>';
});
}
}
function updateServers() {
const country = document.getElementById('countrySelect').value;
const city = document.getElementById('citySelect').value;
const serverSelect = document.getElementById('serverSelect');
if (country && city && servers[country][city]) {
serverSelect.innerHTML = '<option value="">Select server</option>';
servers[country][city].forEach(server => {
serverSelect.innerHTML += '<option value="' + server.hostname + '">' + server.hostname + '</option>';
});
}
}
async function updateStatus() {
const response = await fetch('/api/status');
const data = await response.json();
document.getElementById('status').textContent = data.connected ? 'Connected' : 'Disconnected';
document.getElementById('ip').textContent = data.ip || '-';
document.getElementById('location').textContent = data.location || '-';
}
async function connectVPN() {
const server = document.getElementById('serverSelect').value;
if (!server) { alert('Select a server'); return; }
const response = await fetch('/api/connect', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({server: server})
});
const data = await response.json();
if (data.success) {
alert('Connected!');
updateStatus();
} else {
alert('Connection failed: ' + data.error);
}
}
async function disconnectVPN() {
const response = await fetch('/api/disconnect', {method: 'POST'});
const data = await response.json();
if (data.success) {
alert('Disconnected - No internet (killswitch active)');
updateStatus();
}
}
loadServers();
updateStatus();
setInterval(updateStatus, 10000);
</script>
</body>
</html>
EOFHTML
log "WebUI installed"
}
# Setup systemd services
setup_services() {
log "Setting up systemd services..."
# VPN WebUI service
cat > /etc/systemd/system/vpn-webui.service << EOF
[Unit]
Description=VPN Gateway WebUI
After=network.target vpn-killswitch.service
[Service]
Type=simple
User=root
WorkingDirectory=$INSTALL_DIR
Environment="PATH=$INSTALL_DIR/venv/bin"
ExecStart=$INSTALL_DIR/venv/bin/gunicorn --bind 0.0.0.0:5000 app:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# Security monitor service
cat > /etc/systemd/system/vpn-security-monitor.service << 'EOF'
[Unit]
Description=VPN Security Monitor
After=vpn-killswitch.service
[Service]
Type=simple
ExecStart=/usr/local/bin/vpn-security-monitor.sh
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# Create security monitor script
cat > /usr/local/bin/vpn-security-monitor.sh << 'EOFMON'
#!/bin/bash
while true; do
# Check if killswitch is active
if ! iptables -L -n | grep -q "policy DROP"; then
/usr/local/bin/vpn-killswitch.sh enable
fi
sleep 10
done
EOFMON
chmod +x /usr/local/bin/vpn-security-monitor.sh
# Reload and start services
systemctl daemon-reload
systemctl enable vpn-killswitch vpn-webui vpn-security-monitor
systemctl start vpn-killswitch vpn-webui vpn-security-monitor
log "Services configured and started"
}
# Configure Nginx
setup_nginx() {
log "Configuring Nginx..."
cat > /etc/nginx/sites-available/vpn-gateway << EOF
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
EOF
ln -sf /etc/nginx/sites-available/vpn-gateway /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl restart nginx
log "Nginx configured"
}
# Final setup
finalize_installation() {
log "Finalizing installation..."
# Set IP forwarding
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
echo "net.ipv6.conf.all.disable_ipv6=1" >> /etc/sysctl.conf
sysctl -p &>/dev/null
# Save iptables rules
netfilter-persistent save &>/dev/null
log "Installation complete!"
}
# Show summary
show_summary() {
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ VPN Gateway Installation Complete! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${CYAN}Access WebUI:${NC}"
echo -e " Local: ${BOLD}http://$LAN_IP${NC}"
echo -e " Port: ${BOLD}5000${NC}"
echo ""
echo -e "${CYAN}VPN Provider:${NC}"
echo -e " Type: ${BOLD}$VPN_PROVIDER${NC}"
if [ "$VPN_PROVIDER" = "mullvad" ]; then
echo -e " Account: ${BOLD}Configured${NC}"
elif [ "$VPN_PROVIDER" = "custom" ]; then
echo -e " Server: ${BOLD}$WG_ENDPOINT${NC}"
elif [ "$VPN_PROVIDER" = "import" ]; then
echo -e " Config: ${BOLD}Imported${NC}"
fi
echo ""
echo -e "${CYAN}Security Status:${NC}"
echo -e " Killswitch: ${GREEN}✓ Active${NC}"
echo -e " Firewall: ${GREEN}✓ Configured${NC}"
echo -e " No Leaks: ${GREEN}✓ Protected${NC}"
echo ""
echo -e "${YELLOW}Important:${NC}"
echo -e " • Killswitch is ${BOLD}permanently active${NC}"
echo -e "${BOLD}No internet${NC} without VPN connection"
echo -e " • Configure clients to use ${BOLD}$LAN_IP${NC} as gateway"
echo ""
echo -e "${CYAN}Commands:${NC}"
echo -e " Status: ${BOLD}systemctl status vpn-webui${NC}"
echo -e " Logs: ${BOLD}journalctl -u vpn-webui -f${NC}"
echo -e " Test: ${BOLD}curl http://localhost:5000/api/status${NC}"
echo ""
}
# Cleanup on error
cleanup_on_error() {
error "Installation failed! Cleaning up..."
systemctl stop vpn-webui vpn-killswitch vpn-security-monitor 2>/dev/null
rm -rf "$INSTALL_DIR"
exit 1
}
# Main installation flow
main() {
trap cleanup_on_error ERR
show_banner
check_root
detect_container
check_requirements
detect_network
choose_vpn_provider
echo ""
echo -e "${CYAN}Ready to install with the following configuration:${NC}"
echo -e " Network Interface: ${BOLD}$LAN_INTERFACE${NC}"
echo -e " Network Subnet: ${BOLD}$LAN_NETWORK${NC}"
echo -e " Container Type: ${BOLD}$CONTAINER_TYPE${NC}"
echo -e " VPN Provider: ${BOLD}$VPN_PROVIDER${NC}"
if [ "$VPN_PROVIDER" = "mullvad" ]; then
echo -e " Mullvad Account: ${BOLD}${MULLVAD_ACCOUNT:0:4}...${MULLVAD_ACCOUNT: -4}${NC}"
elif [ "$VPN_PROVIDER" = "custom" ]; then
echo -e " Custom Server: ${BOLD}$WG_ENDPOINT${NC}"
elif [ "$VPN_PROVIDER" = "import" ]; then
echo -e " Config File: ${BOLD}$WG_IMPORTED_CONFIG${NC}"
fi
echo ""
read -p "Proceed with installation? (Y/n): " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]] && [ -n "$REPLY" ]; then
echo "Installation cancelled"
exit 0
fi
install_dependencies
create_directories
install_killswitch
install_vpn_provider # Replaces install_mullvad
install_backend
install_webui
setup_services
setup_nginx
finalize_installation
show_summary
}
# Run main function
main "$@"