diff --git a/messages/en-US.json b/messages/en-US.json
index 7e2d0e5b2..a9e038829 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -4314,9 +4314,12 @@
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
+ "resourceLauncherExitNodeTitle": "Exit Node Routing",
+ "resourceLauncherExitNodeDescription": "After connecting your client, pick this exit node in the client menu to route all traffic through the attached sites.",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
+ "resourceLauncherAllInternetTraffic": "All internet traffic",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherUnlabeled": "Unlabeled",
diff --git a/server/routers/launcher/formatLauncherAccess.ts b/server/routers/launcher/formatLauncherAccess.ts
index ca2104ccd..59862b7dc 100644
--- a/server/routers/launcher/formatLauncherAccess.ts
+++ b/server/routers/launcher/formatLauncherAccess.ts
@@ -125,6 +125,16 @@ export function formatPublicResourceAccess(
export function formatSiteResourceAccess(
resource: SiteResourceAccessInput
): LauncherAccessFields {
+ // Exit node (gateway) destinations are always 0.0.0.0/0 — not useful to
+ // show or copy. The launcher UI renders a localized capability label.
+ if (resource.mode === "gateway") {
+ return {
+ accessDisplay: "",
+ accessCopyValue: "",
+ accessUrl: null
+ };
+ }
+
if (
(resource.mode === "http" || resource.mode === "inference") &&
resource.fullDomain
diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
index 7eb54c829..e60363c05 100644
--- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts
+++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
@@ -55,12 +55,12 @@ const listAllSiteResourcesByOrgQuerySchema = z.strictObject({
}),
query: z.string().optional(),
mode: z
- .enum(["host", "cidr", "http", "ssh", "inference"])
+ .enum(["host", "cidr", "http", "ssh", "inference", "gateway"])
.optional()
.catch(undefined)
.openapi({
type: "string",
- enum: ["host", "cidr", "http", "ssh", "inference"],
+ enum: ["host", "cidr", "http", "ssh", "inference", "gateway"],
description: "Filter site resources by mode"
}),
sort_by: z
diff --git a/src/components/PrivateResourcesTable.tsx b/src/components/PrivateResourcesTable.tsx
index 8c6281dd2..902faf434 100644
--- a/src/components/PrivateResourcesTable.tsx
+++ b/src/components/PrivateResourcesTable.tsx
@@ -357,6 +357,12 @@ export default function PrivateResourcesTable({
label: t(
"editInternalResourceDialogModeInference"
)
+ },
+ {
+ value: "gateway",
+ label: t(
+ "editInternalResourceDialogModeGateway"
+ )
}
]}
selectedValue={searchParams.get("mode") ?? undefined}
diff --git a/src/components/resource-launcher/LauncherResourceAccess.tsx b/src/components/resource-launcher/LauncherResourceAccess.tsx
index 81a867e17..71106c125 100644
--- a/src/components/resource-launcher/LauncherResourceAccess.tsx
+++ b/src/components/resource-launcher/LauncherResourceAccess.tsx
@@ -1,6 +1,7 @@
"use client";
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
+import { useTranslations } from "next-intl";
import Link from "next/link";
import { LauncherCopyIcon } from "./LauncherCopyIcon";
@@ -8,6 +9,7 @@ type LauncherResourceAccessProps = {
accessDisplay: string;
accessCopyValue: string;
accessUrl?: string | null;
+ mode?: string;
variant: "grid" | "list";
};
@@ -15,15 +17,23 @@ export function LauncherResourceAccess({
accessDisplay,
accessCopyValue,
accessUrl,
+ mode,
variant
}: LauncherResourceAccessProps) {
- if (!accessDisplay) {
+ const t = useTranslations();
+ const isExitNode = mode === "gateway";
+ const display = isExitNode
+ ? t("resourceLauncherAllInternetTraffic")
+ : accessDisplay;
+
+ if (!display) {
return null;
}
const href = accessUrl ?? undefined;
- const canLink = href && isSafeUrlForLink(href);
+ const canLink = !isExitNode && href && isSafeUrlForLink(href);
const copyValue = canLink ? href : accessCopyValue;
+ const showCopy = !isExitNode && Boolean(copyValue);
if (variant === "list") {
return (
@@ -35,14 +45,14 @@ export function LauncherResourceAccess({
rel="noopener noreferrer"
className="min-w-0 truncate text-sm text-muted-foreground hover:underline max-md:overflow-visible max-md:whitespace-nowrap"
>
- {accessDisplay}
+ {display}
) : (
- {accessDisplay}
+ {display}
)}
-