Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
14 changes: 11 additions & 3 deletions proxy/internal/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (mw *Middleware) Protect(next http.Handler) http.Handler {
if mw.forwardWithTunnelPeer(w, r, host, config, next) {
return
}
http.Error(w, "Forbidden", http.StatusForbidden)
mw.serveForbiddenPage(w, r, "This private service can only be reached from an authorized NetBird peer.")
return
}

Expand Down Expand Up @@ -223,7 +223,7 @@ func (mw *Middleware) checkIPRestrictions(w http.ResponseWriter, r *http.Request
clientIP := mw.resolveClientIP(r)
if !clientIP.IsValid() {
mw.logger.Debugf("IP restriction: cannot resolve client address for %q, denying", r.RemoteAddr)
http.Error(w, "Forbidden", http.StatusForbidden)
mw.serveForbiddenPage(w, r, "We could not validate your client address for this protected service.")
return false
}

Expand Down Expand Up @@ -258,10 +258,18 @@ func (mw *Middleware) checkIPRestrictions(w http.ResponseWriter, r *http.Request

reason := verdict.String()
mw.blockIPRestriction(r, reason)
http.Error(w, "Forbidden", http.StatusForbidden)
mw.serveForbiddenPage(w, r, "Your current network or location does not satisfy the access policy for this service.")
return false
}

func (mw *Middleware) serveForbiddenPage(w http.ResponseWriter, r *http.Request, message string) {
var requestID string
if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil {
requestID = cd.GetRequestID()
}
web.ServeAccessDeniedPage(w, r, http.StatusForbidden, "Forbidden", message, requestID)
}

// resolveClientIP extracts the real client IP from CapturedData, falling back to r.RemoteAddr.
func (mw *Middleware) resolveClientIP(r *http.Request) netip.Addr {
if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil {
Expand Down
12 changes: 6 additions & 6 deletions proxy/web/dist/assets/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion proxy/web/dist/assets/style.css

Large diffs are not rendered by default.

91 changes: 76 additions & 15 deletions proxy/web/src/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,93 @@
import { useEffect, useState } from "react";
import {BookText, RotateCw, Globe, UserIcon, WaypointsIcon} from "lucide-react";
import { BookText, RotateCw, Globe, UserIcon, WaypointsIcon, ShieldAlert } from "lucide-react";
import { Title } from "@/components/Title";
import { Description } from "@/components/Description";
import Button from "@/components/Button";
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
import { StatusCard } from "@/components/StatusCard";
import type { ErrorData } from "@/data";

export function ErrorPage({ code, title, message, proxy = true, destination = true, requestId, simple = false, retryUrl }: Readonly<ErrorData>) {
type ForbiddenPageProps = Pick<ErrorData, "code" | "message" | "requestId" | "retryUrl">;

function ForbiddenPage({ code, message, requestId, retryUrl }: Readonly<ForbiddenPageProps>) {
const [timestamp] = useState(() => new Date().toISOString());

return (
<main className="flex flex-col items-center mt-24 px-4 max-w-3xl mx-auto">
<div className="text-sm text-netbird font-normal font-mono mb-3 z-10 relative">
Error {code}
</div>

<Title className="text-3xl!">Forbidden</Title>

<Description className="mt-2 mb-8 max-w-md">
{message || "This private service can only be reached from an authorized NetBird peer."}
</Description>

<div className="hidden sm:flex items-start justify-center w-full mt-6 mb-16 z-10 relative">
<StatusCard icon={UserIcon} label="You" line={false} />
<StatusCard icon={ShieldAlert} label="Policy" success={false} />
<StatusCard icon={Globe} label="Destination" success={false} />
</div>

<div className="flex gap-3 justify-center items-center mb-6 z-10 relative">
<Button
variant="primary"
onClick={() => {
if (retryUrl) {
globalThis.location.href = retryUrl;
} else {
globalThis.location.reload();
}
}}
>
<RotateCw size={16} />
Refresh Page
</Button>
<Button
variant="secondary"
onClick={() => globalThis.open("https://docs.netbird.io", "_blank", "noopener,noreferrer")}
>
<BookText size={16} />
Documentation
</Button>
</div>

<div className="text-center text-xs text-nb-gray-300 uppercase z-10 relative font-mono flex flex-col sm:flex-row gap-2 sm:gap-10 mt-4 mb-3">
<div>
<span className="text-nb-gray-400">REQUEST-ID:</span> {requestId || "Unavailable"}
</div>
<div>
<span className="text-nb-gray-400">TIMESTAMP:</span> {timestamp}
</div>
</div>

<PoweredByNetBird />
</main>
);
}

export function ErrorPage({ code, title, message, proxy = true, destination = true, requestId, simple = false, variant = "connection", retryUrl }: Readonly<ErrorData>) {
useEffect(() => {
document.title = `${title} - NetBird Service`;
}, [title]);

const [timestamp] = useState(() => new Date().toISOString());

if (variant === "forbidden") {
return <ForbiddenPage code={code} message={message} requestId={requestId} retryUrl={retryUrl} />;
}

return (
<main className="flex flex-col items-center mt-24 px-4 max-w-3xl mx-auto">
{/* Error Code */}
<div className="text-sm text-netbird font-normal font-mono mb-3 z-10 relative">
Error {code}
</div>

{/* Title */}
<Title className="text-3xl!">{title}</Title>

{/* Description */}
<Description className="mt-2 mb-8 max-w-md">{message}</Description>

{/* Status Cards - hidden in simple mode */}
{!simple && (
<div className="hidden sm:flex items-start justify-center w-full mt-6 mb-16 z-10 relative">
<StatusCard icon={UserIcon} label="You" line={false} />
Expand All @@ -36,15 +96,17 @@ export function ErrorPage({ code, title, message, proxy = true, destination = tr
</div>
)}

{/* Buttons */}
<div className="flex gap-3 justify-center items-center mb-6 z-10 relative">
<Button variant="primary" onClick={() => {
if (retryUrl) {
globalThis.location.href = retryUrl;
} else {
globalThis.location.reload();
}
}}>
<Button
variant="primary"
onClick={() => {
if (retryUrl) {
globalThis.location.href = retryUrl;
} else {
globalThis.location.reload();
}
}}
>
<RotateCw size={16} />
Refresh Page
</Button>
Expand All @@ -57,7 +119,6 @@ export function ErrorPage({ code, title, message, proxy = true, destination = tr
</Button>
</div>

{/* Request Info */}
<div className="text-center text-xs text-nb-gray-300 uppercase z-10 relative font-mono flex flex-col sm:flex-row gap-2 sm:gap-10 mt-4 mb-3">
<div>
<span className="text-nb-gray-400">REQUEST-ID:</span> {requestId}
Expand Down
1 change: 1 addition & 0 deletions proxy/web/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ErrorData {
destination?: boolean
requestId?: string
simple?: boolean
variant?: 'connection' | 'forbidden'
retryUrl?: string
}

Expand Down
1 change: 1 addition & 0 deletions proxy/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ func ServeAccessDeniedPage(w http.ResponseWriter, r *http.Request, code int, tit
"message": message,
"requestId": requestID,
"simple": true,
"variant": "forbidden",
"retryUrl": stripAuthParams(r.URL),
},
}, code)
Expand Down