diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts
index 84bd6de07..770e891fb 100644
--- a/server/db/pg/schema/schema.ts
+++ b/server/db/pg/schema/schema.ts
@@ -346,7 +346,7 @@ export const siteResources = pgTable("siteResources", {
niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(),
ssl: boolean("ssl").notNull().default(false),
- mode: varchar("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
+ mode: varchar("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode
diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts
index fecfa9e04..c36719130 100644
--- a/server/db/sqlite/schema/schema.ts
+++ b/server/db/sqlite/schema/schema.ts
@@ -380,7 +380,7 @@ export const siteResources = sqliteTable("siteResources", {
niceId: text("niceId").notNull(),
name: text("name").notNull(),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
- mode: text("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
+ mode: text("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode
diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts
index e56de628a..ee7bc5ad1 100644
--- a/server/routers/siteResource/createSiteResource.ts
+++ b/server/routers/siteResource/createSiteResource.ts
@@ -44,7 +44,7 @@ const createSiteResourceSchema = z
name: z.string().min(1).max(255),
niceId: z.string().optional(),
// protocol: z.enum(["tcp", "udp"]).optional(),
- mode: z.enum(["host", "cidr", "http"]),
+ mode: z.enum(["host", "cidr", "http", "ssh"]),
ssl: z.boolean().optional(), // only used for http mode
scheme: z.enum(["http", "https"]).optional(),
siteIds: z.array(z.int()).optional(),
@@ -75,7 +75,7 @@ const createSiteResourceSchema = z
.strict()
.refine(
(data) => {
- if (data.mode === "host") {
+ if (data.mode === "host" || data.mode === "ssh") {
// Check if it's a valid IP address using zod (v4 or v6)
const isValidIP = z
// .union([z.ipv4(), z.ipv6()])
@@ -117,13 +117,24 @@ const createSiteResourceSchema = z
)
.refine(
(data) => {
- if (data.mode !== "http") return true;
- return (
- data.scheme !== undefined &&
- data.destinationPort !== undefined &&
- data.destinationPort >= 1 &&
- data.destinationPort <= 65535
- );
+ if (data.mode === "http") {
+ return (
+ data.scheme !== undefined &&
+ data.scheme !== null &&
+ data.destinationPort !== undefined &&
+ data.destinationPort !== null &&
+ data.destinationPort >= 1 &&
+ data.destinationPort <= 65535
+ );
+ } else if (data.mode === "ssh") {
+ // just check the destinationPort
+ return (
+ data.destinationPort === undefined ||
+ (data.destinationPort !== null &&
+ data.destinationPort >= 1 &&
+ data.destinationPort <= 65535)
+ );
+ }
},
{
message:
@@ -391,6 +402,15 @@ export async function createSiteResource(
);
}
+ let tcpPortRangeStringAdjusted = tcpPortRangeString;
+ if (mode === "http") {
+ tcpPortRangeStringAdjusted = "443,80";
+ } else if (mode === "ssh") {
+ tcpPortRangeStringAdjusted = destinationPort
+ ? destinationPort.toString()
+ : "22";
+ }
+
// Create the site resource
const insertValues: typeof siteResources.$inferInsert = {
niceId: updatedNiceId!,
@@ -405,10 +425,12 @@ export async function createSiteResource(
enabled,
alias: alias ? alias.trim() : null,
aliasAddress,
- tcpPortRangeString:
- mode == "http" ? "443,80" : tcpPortRangeString,
- udpPortRangeString: mode == "http" ? "" : udpPortRangeString,
- disableIcmp: disableIcmp || (mode == "http" ? true : false), // default to true for http resources, otherwise false
+ tcpPortRangeString: tcpPortRangeStringAdjusted,
+ udpPortRangeString:
+ mode == "http" || mode == "ssh" ? "" : udpPortRangeString,
+ disableIcmp:
+ disableIcmp ||
+ (mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
domainId,
subdomain: finalSubdomain,
fullDomain
diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts
index 332b395b0..4f827d904 100644
--- a/server/routers/siteResource/updateSiteResource.ts
+++ b/server/routers/siteResource/updateSiteResource.ts
@@ -56,7 +56,7 @@ const updateSiteResourceSchema = z
)
.optional(),
// mode: z.enum(["host", "cidr", "port"]).optional(),
- mode: z.enum(["host", "cidr", "http"]).optional(),
+ mode: z.enum(["host", "cidr", "http", "ssh"]).optional(),
ssl: z.boolean().optional(),
scheme: z.enum(["http", "https"]).nullish(),
// proxyPort: z.int().positive().nullish(),
@@ -85,7 +85,10 @@ const updateSiteResourceSchema = z
.strict()
.refine(
(data) => {
- if (data.mode === "host" && data.destination) {
+ if (
+ (data.mode === "host" || data.mode == "ssh") &&
+ data.destination
+ ) {
const isValidIP = z
// .union([z.ipv4(), z.ipv6()])
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
@@ -126,15 +129,24 @@ const updateSiteResourceSchema = z
)
.refine(
(data) => {
- if (data.mode !== "http") return true;
- return (
- data.scheme !== undefined &&
- data.scheme !== null &&
- data.destinationPort !== undefined &&
- data.destinationPort !== null &&
- data.destinationPort >= 1 &&
- data.destinationPort <= 65535
- );
+ if (data.mode === "http") {
+ return (
+ data.scheme !== undefined &&
+ data.scheme !== null &&
+ data.destinationPort !== undefined &&
+ data.destinationPort !== null &&
+ data.destinationPort >= 1 &&
+ data.destinationPort <= 65535
+ );
+ } else if (data.mode === "ssh") {
+ // just check the destinationPort
+ return (
+ data.destinationPort === undefined ||
+ (data.destinationPort !== null &&
+ data.destinationPort >= 1 &&
+ data.destinationPort <= 65535)
+ );
+ }
},
{
message:
@@ -446,6 +458,16 @@ export async function updateSiteResource(
})
}
: {};
+
+ let tcpPortRangeStringAdjusted = tcpPortRangeString;
+ if (mode === "http") {
+ tcpPortRangeStringAdjusted = "443,80";
+ } else if (mode === "ssh") {
+ tcpPortRangeStringAdjusted = destinationPort
+ ? destinationPort.toString()
+ : "22";
+ }
+
[updatedSiteResource] = await trx
.update(siteResources)
.set({
@@ -458,12 +480,14 @@ export async function updateSiteResource(
destinationPort,
enabled,
alias: alias ? alias.trim() : null,
- tcpPortRangeString:
- mode == "http" ? "443,80" : tcpPortRangeString,
+ tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
- mode == "http" ? "" : udpPortRangeString,
+ mode == "http" || mode == "ssh"
+ ? ""
+ : udpPortRangeString,
disableIcmp:
- disableIcmp || (mode == "http" ? true : false), // default to true for http resources, otherwise false
+ disableIcmp ||
+ (mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
domainId,
subdomain: finalSubdomain,
fullDomain,
diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx
index 295541e8c..0400d63a5 100644
--- a/src/app/[orgId]/settings/resources/client/page.tsx
+++ b/src/app/[orgId]/settings/resources/client/page.tsx
@@ -120,7 +120,7 @@ export default async function ClientResourcesPage(
// proxyPort: siteResource.proxyPort,
siteIds: siteResource.siteIds,
destination: siteResource.destination,
- httpHttpsPort: siteResource.destinationPort ?? null,
+ destinationPort: siteResource.destinationPort ?? null,
alias: siteResource.alias || null,
aliasAddress: siteResource.aliasAddress || null,
siteNiceIds: siteResource.siteNiceIds,
diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx
index 32e00be50..a4232e98e 100644
--- a/src/components/ClientResourcesTable.tsx
+++ b/src/components/ClientResourcesTable.tsx
@@ -83,7 +83,7 @@ export type InternalResourceRow = {
// protocol: string | null;
// proxyPort: number | null;
destination: string;
- httpHttpsPort: number | null;
+ destinationPort: number | null;
alias: string | null;
aliasAddress: string | null;
niceId: string;
@@ -107,7 +107,7 @@ function formatDestinationDisplay(row: InternalResourceRow): string {
return formatSiteResourceDestinationDisplay({
mode: row.mode,
destination: row.destination,
- httpHttpsPort: row.httpHttpsPort,
+ destinationPort: row.destinationPort,
scheme: row.scheme
});
}
diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx
index 58eac9b92..6e6f1abf4 100644
--- a/src/components/CreateInternalResourceDialog.tsx
+++ b/src/components/CreateInternalResourceDialog.tsx
@@ -76,7 +76,7 @@ export default function CreateInternalResourceDialog({
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
- destinationPort: data.httpHttpsPort ?? undefined,
+ destinationPort: data.destinationPort ?? undefined,
domainId: data.httpConfigDomainId
? data.httpConfigDomainId
: undefined,
diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx
index 11d31ba13..038a163db 100644
--- a/src/components/EditInternalResourceDialog.tsx
+++ b/src/components/EditInternalResourceDialog.tsx
@@ -78,7 +78,7 @@ export default function EditInternalResourceDialog({
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
- destinationPort: data.httpHttpsPort ?? null,
+ destinationPort: data.destinationPort ?? null,
domainId: data.httpConfigDomainId
? data.httpConfigDomainId
: undefined,
diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx
index f733df099..41008a2e8 100644
--- a/src/components/InternalResourceForm.tsx
+++ b/src/components/InternalResourceForm.tsx
@@ -54,6 +54,7 @@ import {
MultiSitesSelector,
formatMultiSitesSelectorLabel
} from "./multi-site-selector";
+import { SitesSelector } from "./site-selector";
import type { Selectedsite } from "./site-selector";
import { MachinesSelector } from "./machines-selector";
@@ -154,7 +155,7 @@ export type InternalResourceData = {
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
- httpHttpsPort?: number | null;
+ destinationPort?: number | null;
scheme?: "http" | "https" | null;
ssl?: boolean;
subdomain?: string | null;
@@ -187,7 +188,7 @@ export type InternalResourceFormValues = {
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
- httpHttpsPort?: number | null;
+ destinationPort?: number | null;
scheme?: "http" | "https";
ssl?: boolean;
httpConfigSubdomain?: string | null;
@@ -286,7 +287,7 @@ export function InternalResourceForm({
variant === "create"
? "createInternalResourceDialogAlias"
: "editInternalResourceDialogAlias";
- const httpHttpsPortLabelKey =
+ const destinationPortLabelKey =
variant === "create"
? "createInternalResourceDialogModePort"
: "editInternalResourceDialogModePort";
@@ -308,16 +309,9 @@ export function InternalResourceForm({
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
siteIds: siteIdsSchema,
mode: z.enum(["host", "cidr", "http", "ssh"]),
- destination: z
- .string()
- .min(
- 1,
- destinationRequiredKey
- ? { message: t(destinationRequiredKey) }
- : undefined
- ),
+ destination: z.string(),
alias: z.string().nullish(),
- httpHttpsPort: z
+ destinationPort: z
.number()
.int()
.min(1)
@@ -356,6 +350,20 @@ export function InternalResourceForm({
.optional()
})
.superRefine((data, ctx) => {
+ const isNativeSsh =
+ data.mode === "ssh" && data.authDaemonMode === "native";
+ if (
+ !isNativeSsh &&
+ (!data.destination || data.destination.length < 1)
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: destinationRequiredKey
+ ? t(destinationRequiredKey)
+ : "Destination is required",
+ path: ["destination"]
+ });
+ }
if (data.mode !== "http") return;
if (!data.scheme) {
ctx.addIssue({
@@ -365,14 +373,15 @@ export function InternalResourceForm({
});
}
if (
- data.httpHttpsPort == null ||
- !Number.isFinite(data.httpHttpsPort) ||
- data.httpHttpsPort < 1
+ !isNativeSsh &&
+ (data.destinationPort == null ||
+ !Number.isFinite(data.destinationPort) ||
+ data.destinationPort < 1)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceHttpPortRequired"),
- path: ["httpHttpsPort"]
+ path: ["destinationPort"]
});
}
});
@@ -523,7 +532,7 @@ export function InternalResourceForm({
: (resource.authDaemonMode ?? "site"),
authDaemonPort: resource.authDaemonPort ?? null,
pamMode: resource.pamMode ?? "passthrough",
- httpHttpsPort: resource.httpHttpsPort ?? null,
+ destinationPort: resource.destinationPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
httpConfigSubdomain: resource.subdomain ?? null,
@@ -540,7 +549,7 @@ export function InternalResourceForm({
mode: "host",
destination: "",
alias: null,
- httpHttpsPort: null,
+ destinationPort: null,
scheme: "http",
ssl: true,
httpConfigSubdomain: null,
@@ -605,7 +614,7 @@ export function InternalResourceForm({
mode: "host",
destination: "",
alias: null,
- httpHttpsPort: null,
+ destinationPort: null,
scheme: "http",
ssl: true,
httpConfigSubdomain: null,
@@ -641,7 +650,7 @@ export function InternalResourceForm({
mode: resource.mode ?? "host",
destination: resource.destination ?? "",
alias: resource.alias ?? null,
- httpHttpsPort: resource.httpHttpsPort ?? null,
+ destinationPort: resource.destinationPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
httpConfigSubdomain: resource.subdomain ?? null,
@@ -812,56 +821,120 @@ export function InternalResourceForm({