Skip to content

Commit d8be4ef

Browse files
committed
Add WireGuard VPN support and warning banner
1 parent ffdc0b6 commit d8be4ef

File tree

5 files changed

+83
-1
lines changed

5 files changed

+83
-1
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ ELEVENLABS_MODEL_ID=eleven_flash_v2_5
161161
# Directory to store weekly summary audio files
162162
WEEKLY_SUMMARY_AUDIO_DIR=/var/audio-summaries
163163

164+
# WireGuard VPN Check (Optional)
165+
# If set, a warning banner is shown when the app is accessed from outside this subnet
166+
# Set this to your WireGuard peer subnet (e.g. the IP range assigned to VPN clients)
167+
# Example: WIREGUARD_SUBNET=10.13.13.0/24
168+
# WIREGUARD_SUBNET=
169+
164170
# Client-Side Logging (Optional)
165171
# Remote logging helps debug issues on mobile/car displays where console access is limited
166172
# Logs are batched and sent to the server at this interval (in milliseconds)

config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ class Config:
116116
# Client-side logging settings
117117
client_log_batch_interval: int # Milliseconds between log batches
118118

119+
# WireGuard VPN settings
120+
wireguard_subnet: Optional[
121+
str
122+
] # e.g. "10.13.13.0/24" — warn if client is not in this subnet
123+
119124
@classmethod
120125
def load_from_env(cls) -> "Config":
121126
"""Load configuration from environment variables."""
@@ -218,6 +223,8 @@ def load_from_env(cls) -> "Config":
218223
client_log_batch_interval=_parse_int(
219224
os.getenv("CLIENT_LOG_BATCH_INTERVAL", "5000"), 5000, 1000, 60000
220225
), # 1-60 seconds
226+
# WireGuard VPN settings
227+
wireguard_subnet=os.getenv("WIREGUARD_SUBNET") or None,
221228
)
222229

223230
# Validate configuration if transcription is enabled

main.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import threading
66
import signal
77
import atexit
8+
import ipaddress
9+
from typing import Optional
810
import uvicorn
911
from dotenv import load_dotenv
1012
from fastapi import FastAPI, Request
@@ -191,15 +193,31 @@ def shutdown_handler(signum=None, frame=None) -> None:
191193
app.include_router(weekly_summaries_router)
192194

193195

196+
def _is_on_wireguard(client_ip: str, wireguard_subnet: Optional[str]) -> bool:
197+
"""Return True if client IP is within the configured WireGuard subnet (or no subnet is set)."""
198+
if not wireguard_subnet:
199+
return True
200+
try:
201+
network = ipaddress.ip_network(wireguard_subnet, strict=False)
202+
return ipaddress.ip_address(client_ip) in network
203+
except ValueError:
204+
logger.warning(f"Invalid WIREGUARD_SUBNET value: {wireguard_subnet!r}")
205+
return True
206+
207+
194208
@app.get("/")
195209
def index(request: Request) -> HTMLResponse:
196210
"""Serve the main HTML page."""
197211
server_host = request.url.hostname
198212
client = request.client
199-
logger.info(f"📄 Index page requested by {client.host if client else 'unknown'}")
213+
client_ip = client.host if client else ""
214+
logger.info(f"📄 Index page requested by {client_ip or 'unknown'}")
200215
logger.info(
201216
f" Audio files served from: http://{server_host}:{api_port}/audio/{{video_id}}"
202217
)
218+
vpn_warning = config.wireguard_subnet is not None and not _is_on_wireguard(
219+
client_ip, config.wireguard_subnet
220+
)
203221
return templates.TemplateResponse(
204222
"index.html",
205223
{
@@ -213,6 +231,7 @@ def index(request: Request) -> HTMLResponse:
213231
"prefetch_threshold_seconds": config.prefetch_threshold_seconds,
214232
"trilium_url": config.trilium_url,
215233
"client_log_batch_interval": config.client_log_batch_interval,
234+
"vpn_warning": vpn_warning,
216235
},
217236
)
218237

static/style.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,46 @@ body {
188188
flex-direction: column;
189189
}
190190

191+
/* VPN Warning Banner */
192+
.vpn-banner {
193+
display: flex;
194+
align-items: center;
195+
gap: 10px;
196+
background: rgba(245, 158, 11, 0.15);
197+
border: 1px solid rgba(245, 158, 11, 0.4);
198+
border-radius: 10px;
199+
padding: 12px 16px;
200+
margin-bottom: 16px;
201+
color: #f59e0b;
202+
font-size: 0.9rem;
203+
font-weight: 500;
204+
}
205+
206+
.vpn-banner i.fa-shield-alt {
207+
font-size: 1.1rem;
208+
flex-shrink: 0;
209+
}
210+
211+
.vpn-banner span {
212+
flex: 1;
213+
}
214+
215+
.vpn-banner-close {
216+
background: none;
217+
border: none;
218+
color: #f59e0b;
219+
cursor: pointer;
220+
padding: 2px 6px;
221+
border-radius: 4px;
222+
opacity: 0.7;
223+
flex-shrink: 0;
224+
}
225+
226+
.vpn-banner-close:hover {
227+
opacity: 1;
228+
background: rgba(245, 158, 11, 0.2);
229+
}
230+
191231
/* Header */
192232
header {
193233
text-align: center;

templates/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
<h1><i class="fab fa-youtube"></i> Private YouTube Radio</h1>
1919
</header>
2020

21+
{% if vpn_warning %}
22+
<div class="vpn-banner" id="vpn-banner">
23+
<i class="fas fa-shield-alt"></i>
24+
<span>You are not connected to the WireGuard VPN. Turn it on for a secure connection.</span>
25+
<button class="vpn-banner-close" onclick="document.getElementById('vpn-banner').remove()" aria-label="Dismiss">
26+
<i class="fas fa-times"></i>
27+
</button>
28+
</div>
29+
{% endif %}
30+
2131
<main>
2232
<div class="control-section">
2333
<div class="input-group">

0 commit comments

Comments
 (0)