Disable icmp packets over private resources

This commit is contained in:
Owen
2025-12-16 17:14:00 -05:00
committed by Owen Schwartz
parent 6072ee93fa
commit 3d5ae9dd5c
10 changed files with 98 additions and 16 deletions

View File

@@ -215,7 +215,8 @@ export const siteResources = pgTable("siteResources", {
alias: varchar("alias"), alias: varchar("alias"),
aliasAddress: varchar("aliasAddress"), aliasAddress: varchar("aliasAddress"),
tcpPortRangeString: varchar("tcpPortRangeString"), tcpPortRangeString: varchar("tcpPortRangeString"),
udpPortRangeString: varchar("udpPortRangeString") udpPortRangeString: varchar("udpPortRangeString"),
disableIcmp: boolean("disableIcmp").notNull().default(false)
}); });
export const clientSiteResources = pgTable("clientSiteResources", { export const clientSiteResources = pgTable("clientSiteResources", {

View File

@@ -236,7 +236,8 @@ export const siteResources = sqliteTable("siteResources", {
alias: text("alias"), alias: text("alias"),
aliasAddress: text("aliasAddress"), aliasAddress: text("aliasAddress"),
tcpPortRangeString: text("tcpPortRangeString"), tcpPortRangeString: text("tcpPortRangeString"),
udpPortRangeString: text("udpPortRangeString") udpPortRangeString: text("udpPortRangeString"),
disableIcmp: integer("disableIcmp", { mode: "boolean" })
}); });
export const clientSiteResources = sqliteTable("clientSiteResources", { export const clientSiteResources = sqliteTable("clientSiteResources", {

View File

@@ -466,6 +466,7 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
export type SubnetProxyTarget = { export type SubnetProxyTarget = {
sourcePrefix: string; // must be a cidr sourcePrefix: string; // must be a cidr
destPrefix: string; // must be a cidr destPrefix: string; // must be a cidr
disableIcmp?: boolean;
rewriteTo?: string; // must be a cidr rewriteTo?: string; // must be a cidr
portRange?: { portRange?: {
min: number; min: number;
@@ -504,6 +505,7 @@ export function generateSubnetProxyTargets(
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
...parsePortRangeString(siteResource.udpPortRangeString, "udp") ...parsePortRangeString(siteResource.udpPortRangeString, "udp")
]; ];
const disableIcmp = siteResource.disableIcmp ?? false;
if (siteResource.mode == "host") { if (siteResource.mode == "host") {
let destination = siteResource.destination; let destination = siteResource.destination;
@@ -515,7 +517,8 @@ export function generateSubnetProxyTargets(
targets.push({ targets.push({
sourcePrefix: clientPrefix, sourcePrefix: clientPrefix,
destPrefix: destination, destPrefix: destination,
portRange portRange,
disableIcmp
}); });
} }
@@ -525,14 +528,16 @@ export function generateSubnetProxyTargets(
sourcePrefix: clientPrefix, sourcePrefix: clientPrefix,
destPrefix: `${siteResource.aliasAddress}/32`, destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination, rewriteTo: destination,
portRange portRange,
disableIcmp
}); });
} }
} else if (siteResource.mode == "cidr") { } else if (siteResource.mode == "cidr") {
targets.push({ targets.push({
sourcePrefix: clientPrefix, sourcePrefix: clientPrefix,
destPrefix: siteResource.destination, destPrefix: siteResource.destination,
portRange portRange,
disableIcmp
}); });
} }
} }

View File

@@ -47,7 +47,8 @@ const createSiteResourceSchema = z
roleIds: z.array(z.int()), roleIds: z.array(z.int()),
clientIds: z.array(z.int()), clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema, tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional()
}) })
.strict() .strict()
.refine( .refine(
@@ -158,7 +159,8 @@ export async function createSiteResource(
roleIds, roleIds,
clientIds, clientIds,
tcpPortRangeString, tcpPortRangeString,
udpPortRangeString udpPortRangeString,
disableIcmp
} = parsedBody.data; } = parsedBody.data;
// Verify the site exists and belongs to the org // Verify the site exists and belongs to the org
@@ -245,7 +247,8 @@ export async function createSiteResource(
alias, alias,
aliasAddress, aliasAddress,
tcpPortRangeString, tcpPortRangeString,
udpPortRangeString udpPortRangeString,
disableIcmp
}) })
.returning(); .returning();

View File

@@ -99,6 +99,7 @@ export async function listAllSiteResourcesByOrg(
alias: siteResources.alias, alias: siteResources.alias,
tcpPortRangeString: siteResources.tcpPortRangeString, tcpPortRangeString: siteResources.tcpPortRangeString,
udpPortRangeString: siteResources.udpPortRangeString, udpPortRangeString: siteResources.udpPortRangeString,
disableIcmp: siteResources.disableIcmp,
siteName: sites.name, siteName: sites.name,
siteNiceId: sites.niceId, siteNiceId: sites.niceId,
siteAddress: sites.address siteAddress: sites.address

View File

@@ -58,7 +58,8 @@ const updateSiteResourceSchema = z
roleIds: z.array(z.int()), roleIds: z.array(z.int()),
clientIds: z.array(z.int()), clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema, tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional()
}) })
.strict() .strict()
.refine( .refine(
@@ -165,7 +166,8 @@ export async function updateSiteResource(
roleIds, roleIds,
clientIds, clientIds,
tcpPortRangeString, tcpPortRangeString,
udpPortRangeString udpPortRangeString,
disableIcmp
} = parsedBody.data; } = parsedBody.data;
const [site] = await db const [site] = await db
@@ -233,7 +235,8 @@ export async function updateSiteResource(
enabled: enabled, enabled: enabled,
alias: alias && alias.trim() ? alias : null, alias: alias && alias.trim() ? alias : null,
tcpPortRangeString: tcpPortRangeString, tcpPortRangeString: tcpPortRangeString,
udpPortRangeString: udpPortRangeString udpPortRangeString: udpPortRangeString,
disableIcmp: disableIcmp
}) })
.where( .where(
and( and(
@@ -357,8 +360,12 @@ export async function handleMessagingForUpdatedSiteResource(
existingSiteResource.alias !== updatedSiteResource.alias; existingSiteResource.alias !== updatedSiteResource.alias;
const portRangesChanged = const portRangesChanged =
existingSiteResource && existingSiteResource &&
(existingSiteResource.tcpPortRangeString !== updatedSiteResource.tcpPortRangeString || (existingSiteResource.tcpPortRangeString !==
existingSiteResource.udpPortRangeString !== updatedSiteResource.udpPortRangeString); updatedSiteResource.tcpPortRangeString ||
existingSiteResource.udpPortRangeString !==
updatedSiteResource.udpPortRangeString ||
existingSiteResource.disableIcmp !==
updatedSiteResource.disableIcmp);
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all

View File

@@ -69,7 +69,8 @@ export default async function ClientResourcesPage(
siteNiceId: siteResource.siteNiceId, siteNiceId: siteResource.siteNiceId,
niceId: siteResource.niceId, niceId: siteResource.niceId,
tcpPortRangeString: siteResource.tcpPortRangeString || null, tcpPortRangeString: siteResource.tcpPortRangeString || null,
udpPortRangeString: siteResource.udpPortRangeString || null udpPortRangeString: siteResource.udpPortRangeString || null,
disableIcmp: siteResource.disableIcmp || false,
}; };
} }
); );

View File

@@ -43,6 +43,7 @@ export type InternalResourceRow = {
niceId: string; niceId: string;
tcpPortRangeString: string | null; tcpPortRangeString: string | null;
udpPortRangeString: string | null; udpPortRangeString: string | null;
disableIcmp: boolean;
}; };
type ClientResourcesTableProps = { type ClientResourcesTableProps = {

View File

@@ -42,6 +42,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { Switch } from "@app/components/ui/switch";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
@@ -179,6 +180,7 @@ export default function CreateInternalResourceDialog({
alias: z.string().nullish(), alias: z.string().nullish(),
tcpPortRangeString: portRangeStringSchema, tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema, udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional(),
roles: z roles: z
.array( .array(
z.object({ z.object({
@@ -308,6 +310,7 @@ export default function CreateInternalResourceDialog({
alias: "", alias: "",
tcpPortRangeString: "*", tcpPortRangeString: "*",
udpPortRangeString: "*", udpPortRangeString: "*",
disableIcmp: false,
roles: [], roles: [],
users: [], users: [],
clients: [] clients: []
@@ -355,6 +358,7 @@ export default function CreateInternalResourceDialog({
alias: "", alias: "",
tcpPortRangeString: "*", tcpPortRangeString: "*",
udpPortRangeString: "*", udpPortRangeString: "*",
disableIcmp: false,
roles: [], roles: [],
users: [], users: [],
clients: [] clients: []
@@ -408,6 +412,7 @@ export default function CreateInternalResourceDialog({
: undefined, : undefined,
tcpPortRangeString: data.tcpPortRangeString, tcpPortRangeString: data.tcpPortRangeString,
udpPortRangeString: data.udpPortRangeString, udpPortRangeString: data.udpPortRangeString,
disableIcmp: data.disableIcmp ?? false,
roleIds: data.roles roleIds: data.roles
? data.roles.map((r) => parseInt(r.id)) ? data.roles.map((r) => parseInt(r.id))
: [], : [],
@@ -836,7 +841,7 @@ export default function CreateInternalResourceDialog({
<h3 className="text-lg font-semibold mb-4"> <h3 className="text-lg font-semibold mb-4">
{t("portRestrictions")} {t("portRestrictions")}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-4">
{/* TCP Ports */} {/* TCP Ports */}
<FormField <FormField
control={form.control} control={form.control}
@@ -960,6 +965,31 @@ export default function CreateInternalResourceDialog({
</FormItem> </FormItem>
)} )}
/> />
{/* ICMP Toggle */}
<FormField
control={form.control}
name="disableIcmp"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel className="min-w-10">
ICMP
</FormLabel>
<FormControl>
<Switch
checked={!field.value}
onCheckedChange={(checked) => field.onChange(!checked)}
/>
</FormControl>
<span className="text-sm text-muted-foreground">
{field.value ? t("blocked") : t("allowed")}
</span>
</div>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
</div> </div>

View File

@@ -10,6 +10,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { Switch } from "@app/components/ui/switch";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@@ -132,6 +133,7 @@ type InternalResourceData = {
alias?: string | null; alias?: string | null;
tcpPortRangeString?: string | null; tcpPortRangeString?: string | null;
udpPortRangeString?: string | null; udpPortRangeString?: string | null;
disableIcmp?: boolean;
}; };
type EditInternalResourceDialogProps = { type EditInternalResourceDialogProps = {
@@ -167,6 +169,7 @@ export default function EditInternalResourceDialog({
alias: z.string().nullish(), alias: z.string().nullish(),
tcpPortRangeString: portRangeStringSchema, tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema, udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional(),
roles: z roles: z
.array( .array(
z.object({ z.object({
@@ -358,6 +361,7 @@ export default function EditInternalResourceDialog({
alias: resource.alias ?? null, alias: resource.alias ?? null,
tcpPortRangeString: resource.tcpPortRangeString ?? "*", tcpPortRangeString: resource.tcpPortRangeString ?? "*",
udpPortRangeString: resource.udpPortRangeString ?? "*", udpPortRangeString: resource.udpPortRangeString ?? "*",
disableIcmp: resource.disableIcmp ?? false,
roles: [], roles: [],
users: [], users: [],
clients: [] clients: []
@@ -433,6 +437,7 @@ export default function EditInternalResourceDialog({
: null, : null,
tcpPortRangeString: data.tcpPortRangeString, tcpPortRangeString: data.tcpPortRangeString,
udpPortRangeString: data.udpPortRangeString, udpPortRangeString: data.udpPortRangeString,
disableIcmp: data.disableIcmp ?? false,
roleIds: (data.roles || []).map((r) => parseInt(r.id)), roleIds: (data.roles || []).map((r) => parseInt(r.id)),
userIds: (data.users || []).map((u) => u.id), userIds: (data.users || []).map((u) => u.id),
clientIds: (data.clients || []).map((c) => parseInt(c.id)) clientIds: (data.clients || []).map((c) => parseInt(c.id))
@@ -504,6 +509,7 @@ export default function EditInternalResourceDialog({
alias: resource.alias ?? null, alias: resource.alias ?? null,
tcpPortRangeString: resource.tcpPortRangeString ?? "*", tcpPortRangeString: resource.tcpPortRangeString ?? "*",
udpPortRangeString: resource.udpPortRangeString ?? "*", udpPortRangeString: resource.udpPortRangeString ?? "*",
disableIcmp: resource.disableIcmp ?? false,
roles: [], roles: [],
users: [], users: [],
clients: [] clients: []
@@ -561,6 +567,7 @@ export default function EditInternalResourceDialog({
alias: resource.alias ?? null, alias: resource.alias ?? null,
tcpPortRangeString: resource.tcpPortRangeString ?? "*", tcpPortRangeString: resource.tcpPortRangeString ?? "*",
udpPortRangeString: resource.udpPortRangeString ?? "*", udpPortRangeString: resource.udpPortRangeString ?? "*",
disableIcmp: resource.disableIcmp ?? false,
roles: [], roles: [],
users: [], users: [],
clients: [] clients: []
@@ -815,7 +822,7 @@ export default function EditInternalResourceDialog({
<h3 className="text-lg font-semibold mb-4"> <h3 className="text-lg font-semibold mb-4">
{t("portRestrictions")} {t("portRestrictions")}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-4">
{/* TCP Ports */} {/* TCP Ports */}
<FormField <FormField
control={form.control} control={form.control}
@@ -939,6 +946,31 @@ export default function EditInternalResourceDialog({
</FormItem> </FormItem>
)} )}
/> />
{/* ICMP Toggle */}
<FormField
control={form.control}
name="disableIcmp"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel className="min-w-10">
ICMP
</FormLabel>
<FormControl>
<Switch
checked={!field.value}
onCheckedChange={(checked) => field.onChange(!checked)}
/>
</FormControl>
<span className="text-sm text-muted-foreground">
{field.value ? t("blocked") : t("allowed")}
</span>
</div>
<FormMessage />
</FormItem>
)}
/>
</div> </div>
</div> </div>