diff --git a/messages/en-US.json b/messages/en-US.json index e0728c94..c6028ec2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2300,6 +2300,8 @@ "setupFailedToFetchSubnet": "Failed to fetch default subnet", "setupSubnetAdvanced": "Subnet (Advanced)", "setupSubnetDescription": "The subnet for this organization's internal network.", + "setupUtilitySubnet": "Utility Subnet (Advanced)", + "setupUtilitySubnetDescription": "The subnet for this organization's alias addresses and DNS server.", "siteRegenerateAndDisconnect": "Regenerate and Disconnect", "siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?", "siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.", diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 21c148ac..87a0c3c6 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -301,6 +301,29 @@ export function isIpInCidr(ip: string, cidr: string): boolean { return ipBigInt >= range.start && ipBigInt <= range.end; } +/** + * Checks if two CIDR ranges overlap + * @param cidr1 First CIDR string + * @param cidr2 Second CIDR string + * @returns boolean indicating if the two CIDRs overlap + */ +export function doCidrsOverlap(cidr1: string, cidr2: string): boolean { + const version1 = detectIpVersion(cidr1.split("/")[0]); + const version2 = detectIpVersion(cidr2.split("/")[0]); + if (version1 !== version2) { + // Different IP versions cannot overlap + return false; + } + const range1 = cidrToRange(cidr1); + const range2 = cidrToRange(cidr2); + + // Overlap if the ranges intersect + return ( + range1.start <= range2.end && + range2.start <= range1.end + ); +} + export async function getNextAvailableClientSubnet( orgId: string, transaction: Transaction | typeof db = db diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index fe610663..365bcb13 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -255,11 +255,11 @@ export const configSchema = z orgs: z .object({ block_size: z.number().positive().gt(0).optional().default(24), - subnet_group: z.string().optional().default("100.90.128.0/24"), + subnet_group: z.string().optional().default("100.90.128.0/20"), utility_subnet_group: z .string() .optional() - .default("100.96.128.0/24") //just hardcode this for now as well + .default("100.96.128.0/20") //just hardcode this for now as well }) .optional() .default({ diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index f1d06566..e93af889 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -27,6 +27,7 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; +import { doCidrsOverlap } from "@server/lib/ip"; const createOrgSchema = z.strictObject({ orgId: z.string(), @@ -36,6 +37,11 @@ const createOrgSchema = z.strictObject({ .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere .refine((val) => isValidCIDR(val), { message: "Invalid subnet CIDR" + }), + utilitySubnet: z + .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere + .refine((val) => isValidCIDR(val), { + message: "Invalid utility subnet CIDR" }) }); @@ -84,7 +90,7 @@ export async function createOrg( ); } - const { orgId, name, subnet } = parsedBody.data; + const { orgId, name, subnet, utilitySubnet } = parsedBody.data; // TODO: for now we are making all of the orgs the same subnet // make sure the subnet is unique @@ -119,6 +125,15 @@ export async function createOrg( ); } + if (doCidrsOverlap(subnet, utilitySubnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Subnet ${subnet} overlaps with utility subnet ${utilitySubnet}` + ) + ); + } + let error = ""; let org: Org | null = null; @@ -128,9 +143,6 @@ export async function createOrg( .from(domains) .where(eq(domains.configManaged, true)); - const utilitySubnet = - config.getRawConfig().orgs.utility_subnet_group; - const newOrg = await trx .insert(orgs) .values({ diff --git a/server/routers/org/pickOrgDefaults.ts b/server/routers/org/pickOrgDefaults.ts index 771b0d99..cce46a01 100644 --- a/server/routers/org/pickOrgDefaults.ts +++ b/server/routers/org/pickOrgDefaults.ts @@ -8,6 +8,7 @@ import config from "@server/lib/config"; export type PickOrgDefaultsResponse = { subnet: string; + utilitySubnet: string; }; export async function pickOrgDefaults( @@ -20,10 +21,13 @@ export async function pickOrgDefaults( // const subnet = await getNextAvailableOrgSubnet(); // Just hard code the subnet for now for everyone const subnet = config.getRawConfig().orgs.subnet_group; + const utilitySubnet = + config.getRawConfig().orgs.utility_subnet_group; return response(res, { data: { - subnet: subnet + subnet: subnet, + utilitySubnet: utilitySubnet }, success: true, error: false, diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 36853e5c..10a8b14e 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -41,13 +41,14 @@ export default function StepperForm() { const [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); - const [error, setError] = useState(null); + // Removed error state, now using toast for API errors const [orgCreated, setOrgCreated] = useState(false); const orgSchema = z.object({ orgName: z.string().min(1, { message: t("orgNameRequired") }), orgId: z.string().min(1, { message: t("orgIdRequired") }), - subnet: z.string().min(1, { message: t("subnetRequired") }) + subnet: z.string().min(1, { message: t("subnetRequired") }), + utilitySubnet: z.string().min(1, { message: t("subnetRequired") }) }); const orgForm = useForm({ @@ -55,7 +56,8 @@ export default function StepperForm() { defaultValues: { orgName: "", orgId: "", - subnet: "" + subnet: "", + utilitySubnet: "" } }); @@ -72,6 +74,7 @@ export default function StepperForm() { const res = await api.get(`/pick-org-defaults`); if (res && res.data && res.data.data) { orgForm.setValue("subnet", res.data.data.subnet); + orgForm.setValue("utilitySubnet", res.data.data.utilitySubnet); } } catch (e) { console.error("Failed to fetch default subnet:", e); @@ -129,7 +132,8 @@ export default function StepperForm() { const res = await api.put(`/org`, { orgId: values.orgId, name: values.orgName, - subnet: values.subnet + subnet: values.subnet, + utilitySubnet: values.utilitySubnet }); if (res && res.status === 201) { @@ -138,7 +142,11 @@ export default function StepperForm() { } } catch (e) { console.error(e); - setError(formatAxiosError(e, t("orgErrorCreate"))); + toast({ + title: t("error"), + description: formatAxiosError(e, t("orgErrorCreate")), + variant: "destructive" + }); } setLoading(false); @@ -320,6 +328,30 @@ export default function StepperForm() { )} /> + ( + + + {t("setupUtilitySubnet")} + + + + + + + {t( + "setupUtilitySubnetDescription" + )} + + + )} + /> + {orgIdTaken && !orgCreated ? ( @@ -328,20 +360,13 @@ export default function StepperForm() { ) : null} - {error && ( - - - {error} - - - )} + {/* Error Alert removed, errors now shown as toast */}