Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from redis_config import get_redis_config, should_use_pooling
from hwaccel import hw_accel
from broadcast_manager import BroadcastManager, BroadcastConfig
from vpn_watchdog import VPNWatchdog

# Set up logging
logging.basicConfig(level=logging.INFO)
Expand Down Expand Up @@ -365,6 +366,7 @@ def validate_profile_variables(cls, v):
stream_manager = StreamManager(redis_url=redis_url, enable_pooling=enable_pooling)
event_manager = EventManager()
broadcast_manager = BroadcastManager()
vpn_watchdog = VPNWatchdog()


@asynccontextmanager
Expand All @@ -380,6 +382,13 @@ async def lifespan(app: FastAPI):
await stream_manager.start()
await broadcast_manager.start()

# Start VPN watchdog if enabled
if settings.VPN_WATCHDOG_ENABLED:
vpn_watchdog.set_event_manager(event_manager)
vpn_watchdog.set_stream_manager(stream_manager)
await vpn_watchdog.start()
logger.info("VPN Watchdog started")

# Set up custom event handlers
def log_event_handler(event: StreamEvent):
"""Simple event handler that logs all events"""
Expand All @@ -394,6 +403,8 @@ def log_event_handler(event: StreamEvent):

# Shutdown
logger.info("m3u proxy shutting down...")
if settings.VPN_WATCHDOG_ENABLED:
await vpn_watchdog.stop()
await broadcast_manager.shutdown()
await stream_manager.stop()
await event_manager.stop()
Expand Down Expand Up @@ -2550,5 +2561,37 @@ async def cleanup_broadcast(network_id: str) -> dict:
raise HTTPException(status_code=500, detail=str(e))


# VPN Watchdog Endpoints


@app.get("/vpn/status", dependencies=[Depends(verify_token)])
async def vpn_status():
"""Get current VPN health status and metrics"""
if not settings.VPN_WATCHDOG_ENABLED:
raise HTTPException(status_code=404, detail="VPN Watchdog is not enabled")
return vpn_watchdog.get_status()


@app.get("/vpn/history", dependencies=[Depends(verify_token)])
async def vpn_history(
limit: int = Query(default=20, ge=1, le=100, description="Max events to return"),
):
"""Get VPN event history (state changes, rotations)"""
if not settings.VPN_WATCHDOG_ENABLED:
raise HTTPException(status_code=404, detail="VPN Watchdog is not enabled")
return {"events": vpn_watchdog.get_history(limit=limit)}


@app.post("/vpn/rotate", dependencies=[Depends(verify_token)])
async def vpn_rotate():
"""Manually trigger a VPN rotation via Gluetun"""
if not settings.VPN_WATCHDOG_ENABLED:
raise HTTPException(status_code=404, detail="VPN Watchdog is not enabled")
result = await vpn_watchdog.rotate(reason="manual_api")
if not result["success"]:
raise HTTPException(status_code=500, detail=result.get("error", "Rotation failed"))
return result


# Event Handler Examples
# Custom event handlers are now set up in the lifespan context manager above
27 changes: 27 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,33 @@ class Settings(BaseSettings):
# Can be overridden per stream during creation.
USE_STICKY_SESSION: bool = False

# VPN Watchdog Configuration
# Proactive VPN health monitoring via Gluetun Control API.
# When enabled, monitors VPN connectivity, correlates stream failures with VPN health,
# and triggers VPN rotation only when genuinely needed.
VPN_WATCHDOG_ENABLED: bool = False
# Gluetun Control API URL (typically http://127.0.0.1:8000 when sharing network_mode)
VPN_WATCHDOG_GLUETUN_URL: str = "http://127.0.0.1:8000"
# Gluetun API key (optional, recommended for v3.39.1+)
VPN_WATCHDOG_GLUETUN_API_KEY: str = ""
# Health check interval in seconds (adaptive: shorter when degraded)
VPN_WATCHDOG_CHECK_INTERVAL: int = 15
# Number of correlated failures in the window before considering rotation
VPN_WATCHDOG_FAILURE_THRESHOLD: int = 3
# Time window in seconds for counting correlated failures
VPN_WATCHDOG_FAILURE_WINDOW: int = 300
# Minimum seconds between VPN rotations
VPN_WATCHDOG_ROTATION_COOLDOWN: int = 600
# Seconds to wait between Gluetun stop and start during rotation
VPN_WATCHDOG_RECONNECT_DELAY: int = 5
# HTTP latency thresholds (ms) for state machine transitions
VPN_WATCHDOG_LATENCY_WARN_MS: int = 200
VPN_WATCHDOG_LATENCY_CRITICAL_MS: int = 500
# DNS test hostname for connectivity checks
VPN_WATCHDOG_DNS_TEST_HOST: str = "google.com"
# HTTP endpoint for latency measurement (should return 200 quickly)
VPN_WATCHDOG_HTTP_TEST_URL: str = "http://connectivitycheck.gstatic.com/generate_204"

# Model configuration
model_config = SettingsConfigDict(
env_file=".env",
Expand Down
4 changes: 4 additions & 0 deletions src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class EventType(str, Enum):
CLIENT_CONNECTED = "client_connected"
CLIENT_DISCONNECTED = "client_disconnected"
FAILOVER_TRIGGERED = "failover_triggered"
VPN_HEALTH_CHANGED = "vpn_health_changed"
VPN_ROTATION_STARTED = "vpn_rotation_started"
VPN_ROTATION_COMPLETED = "vpn_rotation_completed"
VPN_ROTATION_FAILED = "vpn_rotation_failed"


class StreamConfig(BaseModel):
Expand Down
Loading
Loading