feat: external IP detection, port status, fix peer config generation
- WireGuardManager: get_external_ip() (cached 1h), check_port_open(), get_server_config() returning public_key + detected endpoint - API: /api/wireguard/server-config returns real external IP; /api/wireguard/refresh-ip forces re-detection; /api/wireguard/peers/config now looks up peer IP + private key from registry and uses real server endpoint automatically - Fix doubled port in Endpoint (178.x:51820:51820 → 178.x:51820) - Fix Address=/32 when peer_ip already has mask - WebUI nginx: proxy /api/ and /health to cell-api (fixes localhost:3000 hardcode — UI now works from any machine) - api.js: baseURL='' so all calls go through nginx proxy - WireGuard page: show Server Endpoint card with external IP, endpoint, public key, and Refresh IP button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,13 +6,20 @@ WireGuard Manager for Personal Internet Cell
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import socket
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Any
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
|
||||
from base_service_manager import BaseServiceManager
|
||||
|
||||
try:
|
||||
import requests as _requests
|
||||
except ImportError:
|
||||
_requests = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SERVER_ADDRESS = '172.20.0.1/16'
|
||||
@@ -215,16 +222,95 @@ class WireGuardManager(BaseServiceManager):
|
||||
return (
|
||||
f'[Interface]\n'
|
||||
f'PrivateKey = {peer_private_key}\n'
|
||||
f'Address = {peer_ip}/32\n'
|
||||
f'Address = {peer_ip if "/" in peer_ip else f"{peer_ip}/32"}\n'
|
||||
f'DNS = {PEER_DNS}\n'
|
||||
f'\n'
|
||||
f'[Peer]\n'
|
||||
f'PublicKey = {server_keys["public_key"]}\n'
|
||||
f'AllowedIPs = {SERVER_NETWORK}\n'
|
||||
f'Endpoint = {server_endpoint}:{DEFAULT_PORT}\n'
|
||||
f'Endpoint = {server_endpoint if ":" in server_endpoint else f"{server_endpoint}:{DEFAULT_PORT}"}\n'
|
||||
f'PersistentKeepalive = 25\n'
|
||||
)
|
||||
|
||||
# ── External IP & port ────────────────────────────────────────────────────
|
||||
|
||||
def _ip_cache_file(self) -> str:
|
||||
return os.path.join(self.keys_dir, 'external_ip.json')
|
||||
|
||||
def get_external_ip(self, force_refresh: bool = False) -> Optional[str]:
|
||||
"""Detect external IP, caching result for 1 hour."""
|
||||
cache_file = self._ip_cache_file()
|
||||
if not force_refresh and os.path.exists(cache_file):
|
||||
try:
|
||||
with open(cache_file) as f:
|
||||
data = json.load(f)
|
||||
if time.time() - data.get('ts', 0) < 3600:
|
||||
return data.get('ip')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ip = None
|
||||
services = [
|
||||
'https://api.ipify.org',
|
||||
'https://ifconfig.me/ip',
|
||||
'https://icanhazip.com',
|
||||
]
|
||||
if _requests:
|
||||
for url in services:
|
||||
try:
|
||||
resp = _requests.get(url, timeout=5)
|
||||
candidate = resp.text.strip()
|
||||
if candidate and len(candidate) < 45:
|
||||
ip = candidate
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if ip:
|
||||
try:
|
||||
with open(cache_file, 'w') as f:
|
||||
json.dump({'ip': ip, 'ts': time.time()}, f)
|
||||
except (PermissionError, OSError):
|
||||
pass
|
||||
return ip
|
||||
|
||||
def check_port_open(self, port: int = DEFAULT_PORT) -> bool:
|
||||
"""Check if the WireGuard UDP port is reachable from outside."""
|
||||
external_ip = self.get_external_ip()
|
||||
if not external_ip or _requests is None:
|
||||
return False
|
||||
# Use an external UDP port-check service
|
||||
try:
|
||||
resp = _requests.get(
|
||||
f'https://portchecker.co/api/v1/query',
|
||||
params={'host': external_ip, 'port': port},
|
||||
timeout=8,
|
||||
)
|
||||
if resp.ok:
|
||||
data = resp.json()
|
||||
return bool(data.get('isOpen') or data.get('open'))
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: try TCP (won't work for UDP WireGuard, but gives a network clue)
|
||||
try:
|
||||
sock = socket.create_connection((external_ip, port), timeout=3)
|
||||
sock.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_server_config(self) -> Dict[str, Any]:
|
||||
"""Return server public key, external IP, endpoint, and port status."""
|
||||
keys = self.get_keys()
|
||||
external_ip = self.get_external_ip()
|
||||
endpoint = f'{external_ip}:{DEFAULT_PORT}' if external_ip else None
|
||||
return {
|
||||
'public_key': keys['public_key'],
|
||||
'external_ip': external_ip,
|
||||
'endpoint': endpoint,
|
||||
'port': DEFAULT_PORT,
|
||||
}
|
||||
|
||||
# ── Status & connectivity ─────────────────────────────────────────────────
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user