- Improved dashboard access control by blocking tunnel/Tailscale access when disabled.

This commit is contained in:
decolua
2026-04-13 17:38:09 +07:00
parent e02dd07a2c
commit 41c079baba
5 changed files with 29 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "9router-app",
"version": "0.3.87",
"version": "0.3.88",
"description": "9Router web dashboard",
"private": true,
"scripts": {

View File

@@ -121,8 +121,8 @@ export default function APIPageClient({ machineId }) {
// Ping once to verify reachable
const healthUrl = `${tPublicUrl || tUrl}/api/health`;
try {
const ping = await fetch(healthUrl, { mode: "no-cors", cache: "no-store" });
if (ping.ok || ping.type === "opaque") {
const ping = await fetch(healthUrl, { cache: "no-store" });
if (ping.ok) {
setTunnelEnabled(true);
} else {
pingTunnelHealth(tPublicUrl || tUrl);
@@ -769,7 +769,7 @@ export default function APIPageClient({ machineId }) {
/>
<div className="flex items-center gap-1.5">
<p className="font-medium text-sm">Allow dashboard access via tunnel</p>
<Tooltip text="When enabled, the dashboard can be accessed through your tunnel or Tailscale URL without requiring login. Only enable if you trust everyone who can reach your tunnel URL." />
<Tooltip text="When enabled, the dashboard can be accessed through your tunnel or Tailscale URL (login still required). When disabled, dashboard access via tunnel/Tailscale is completely blocked." />
</div>
</div>
)}

View File

@@ -8,11 +8,23 @@ const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
function isTunnelRequest(request, settings) {
const host = (request.headers.get("host") || "").split(":")[0].toLowerCase();
const tunnelHost = settings.tunnelUrl ? new URL(settings.tunnelUrl).hostname.toLowerCase() : "";
const tailscaleHost = settings.tailscaleUrl ? new URL(settings.tailscaleUrl).hostname.toLowerCase() : "";
return (tunnelHost && host === tunnelHost) || (tailscaleHost && host === tailscaleHost);
}
export async function POST(request) {
try {
const { password } = await request.json();
const settings = await getSettings();
// Block login via tunnel/tailscale if dashboard access is disabled
if (isTunnelRequest(request, settings) && settings.tunnelDashboardAccess !== true) {
return NextResponse.json({ error: "Dashboard access via tunnel is disabled" }, { status: 403 });
}
// Default password is '123456' if not set
const storedHash = settings.password;

View File

@@ -6,7 +6,9 @@ export async function GET() {
const settings = await getSettings();
const requireLogin = settings.requireLogin !== false;
const tunnelDashboardAccess = settings.tunnelDashboardAccess === true;
return NextResponse.json({ requireLogin, tunnelDashboardAccess });
const tunnelUrl = settings.tunnelUrl || "";
const tailscaleUrl = settings.tailscaleUrl || "";
return NextResponse.json({ requireLogin, tunnelDashboardAccess, tunnelUrl, tailscaleUrl });
} catch (error) {
return NextResponse.json({ requireLogin: true }, { status: 200 });
}

View File

@@ -81,15 +81,20 @@ export async function proxy(request) {
const data = await res.json();
requireLogin = data.requireLogin !== false;
tunnelDashboardAccess = data.tunnelDashboardAccess === true;
// Block tunnel/tailscale access if disabled (redirect to login)
if (!tunnelDashboardAccess) {
const host = (request.headers.get("host") || "").split(":")[0].toLowerCase();
const tunnelHost = data.tunnelUrl ? new URL(data.tunnelUrl).hostname.toLowerCase() : "";
const tailscaleHost = data.tailscaleUrl ? new URL(data.tailscaleUrl).hostname.toLowerCase() : "";
if ((tunnelHost && host === tunnelHost) || (tailscaleHost && host === tailscaleHost)) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
} catch {
// On error, keep defaults (require login, block tunnel)
}
// Block tunnel access if disabled (checked before token to enforce the setting)
if (!isLocalRequest(request) && !tunnelDashboardAccess) {
return NextResponse.redirect(new URL("/login", request.url));
}
// If login not required, allow through
if (!requireLogin) return NextResponse.next();