diff --git a/messages/en-US.json b/messages/en-US.json index 1efdb3a1..8d126e2b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1454,9 +1454,7 @@ "sitesFetchError": "An error occurred while fetching sites.", "olmErrorFetchReleases": "An error occurred while fetching Olm releases.", "olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.", - "remoteSubnets": "Remote Subnets", "enterCidrRange": "Enter CIDR range", - "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled", diff --git a/server/lib/ip.ts b/server/lib/ip.ts index c929f025..956d4af1 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,7 +1,8 @@ -import { db } from "@server/db"; +import { db, SiteResource } from "@server/db"; import { clients, orgs, sites } from "@server/db"; import { and, eq, isNotNull } from "drizzle-orm"; import config from "@server/lib/config"; +import z from "zod"; interface IPRange { start: bigint; @@ -300,3 +301,28 @@ export async function getNextAvailableOrgSubnet(): Promise { return subnet; } + +export function generateRemoteSubnetsStr(allSiteResources: SiteResource[]) { + let remoteSubnets = allSiteResources + .filter((sr) => { + if (sr.mode === "cidr") return true; + if (sr.mode === "host") { + // check if its a valid IP using zod + const ipSchema = z.string().ip(); + const parseResult = ipSchema.safeParse(sr.destination); + return parseResult.success; + } + return false; + }) + .map((sr) => { + if (sr.mode === "cidr") return sr.destination; + if (sr.mode === "host") { + return `${sr.destination}/32`; + } + }); + // remove duplicates + remoteSubnets = Array.from(new Set(remoteSubnets)); + const remoteSubnetsStr = + remoteSubnets.length > 0 ? remoteSubnets.join(",") : null; + return remoteSubnetsStr; +} diff --git a/server/lib/rebuildSiteClientAssociations.ts b/server/lib/rebuildSiteClientAssociations.ts index 2b98c68d..aa1d2dec 100644 --- a/server/lib/rebuildSiteClientAssociations.ts +++ b/server/lib/rebuildSiteClientAssociations.ts @@ -29,6 +29,8 @@ import { } from "@server/routers/olm/peers"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; import logger from "@server/logger"; +import z from "zod"; +import { generateRemoteSubnetsStr } from "@server/lib/ip"; export async function rebuildSiteClientAssociations( siteResource: SiteResource, @@ -331,14 +333,6 @@ async function handleMessagesForSiteClients( .from(siteResources) .where(eq(siteResources.siteId, site.siteId)); - let remoteSubnets = allSiteResources - .filter((sr) => sr.mode == "cidr") - .map((sr) => sr.destination); - // remove duplicates - remoteSubnets = Array.from(new Set(remoteSubnets)); - const remoteSubnetsStr = - remoteSubnets.length > 0 ? remoteSubnets.join(",") : null; - olmJobs.push( olmAddPeer( client.clientId, @@ -351,7 +345,7 @@ async function handleMessagesForSiteClients( publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort, - remoteSubnets: remoteSubnetsStr + remoteSubnets: generateRemoteSubnetsStr(allSiteResources) }, olm.olmId ) diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 3c135210..4be5ae14 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -15,6 +15,7 @@ import { clients, clientSites, Newt, sites } from "@server/db"; import { eq, and, inArray } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; +import { generateRemoteSubnetsStr } from "@server/lib/ip"; const inputSchema = z.object({ publicKey: z.string(), @@ -188,23 +189,13 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { .from(siteResources) .where(eq(siteResources.siteId, site.siteId)); - let remoteSubnets = allSiteResources - .filter((sr) => sr.mode == "cidr") - .map((sr) => sr.destination); - // remove duplicates - remoteSubnets = Array.from(new Set(remoteSubnets)); - const remoteSubnetsStr = - remoteSubnets.length > 0 - ? remoteSubnets.join(",") - : null; - await updatePeer(client.clients.clientId, { siteId: site.siteId, endpoint: endpoint, publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort, - remoteSubnets: remoteSubnetsStr + remoteSubnets: generateRemoteSubnetsStr(allSiteResources) }); } catch (error) { logger.error( @@ -231,46 +222,35 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { .from(siteResources) .where(eq(siteResources.siteId, siteId)); - const { tcpTargets, udpTargets } = allSiteResources.reduce( - (acc, resource) => { - // Only process port mode resources - if (resource.mode !== "port") { - return acc; + let targets: { + cidr: string; + portRange?: { + min: number; + max: number; + }[]; + }[] = []; + + for (const siteResource of allSiteResources) { + if (siteResource.mode == "host") { + // check if this is a valid ip + const ipSchema = z.string().ip(); + if (ipSchema.safeParse(siteResource.destination).success) { + targets.push({ + cidr: `${siteResource.destination}/32` + }); } - - // Filter out invalid targets - if ( - !resource.proxyPort || - !resource.destination || - !resource.destinationPort || - !resource.protocol - ) { - return acc; - } - - // Format target into string - const formattedTarget = `${resource.proxyPort}:${resource.destination}:${resource.destinationPort}`; - - // Add to the appropriate protocol array - if (resource.protocol === "tcp") { - acc.tcpTargets.push(formattedTarget); - } else { - acc.udpTargets.push(formattedTarget); - } - - return acc; - }, - { tcpTargets: [] as string[], udpTargets: [] as string[] } - ); + } else if (siteResource.mode == "cidr") { + targets.push({ + cidr: siteResource.destination + }); + } + } // Build the configuration response const configResponse = { ipAddress: site.address, peers: validPeers, - targets: { - udp: udpTargets, - tcp: tcpTargets - } + targets: targets }; logger.debug("Sending config: ", configResponse); diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 58d7753d..dee52417 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -18,6 +18,7 @@ import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; import { listExitNodes } from "#dynamic/lib/exitNodes"; import { getNextAvailableClientSubnet } from "@server/lib/ip"; +import { generateRemoteSubnetsStr } from "@server/lib/ip"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.info("Handling register olm message!"); @@ -238,11 +239,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { .from(siteResources) .where(eq(siteResources.siteId, site.siteId)); - let remoteSubnets = allSiteResources.filter((sr => sr.mode == "cidr")).map(sr => sr.destination); - // remove duplicates - remoteSubnets = Array.from(new Set(remoteSubnets)); - const remoteSubnetsStr = remoteSubnets.length > 0 ? remoteSubnets.join(",") : null; - // Add the peer to the exit node for this site if (clientSite.endpoint) { logger.info( @@ -280,7 +276,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { publicKey: site.publicKey, serverIP: site.address, serverPort: site.listenPort, - remoteSubnets: remoteSubnetsStr + remoteSubnets: generateRemoteSubnetsStr(allSiteResources) }); } diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 1200c38b..f04d14e4 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -47,7 +47,39 @@ const createSiteResourceSchema = z message: "Protocol, proxy port, and destination port are required for port mode" } - ); + ) + .refine( + (data) => { + if (data.mode === "host") { + // Check if it's a valid IP address using zod + const isValidIP = z.string().ip().safeParse(data.destination).success; + + // Check if it's a valid domain (hostname pattern, TLD not required) + const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; + const isValidDomain = domainRegex.test(data.destination); + + return isValidIP || isValidDomain; + } + return true; + }, + { + message: + "Destination must be a valid IP address or domain name for host mode" + } + ) + .refine( + (data) => { + if (data.mode === "cidr") { + // Check if it's a valid CIDR + const isValidCIDR = z.string().cidr().safeParse(data.destination).success; + return isValidCIDR; + } + return true; + }, + { + message: "Destination must be a valid CIDR notation for cidr mode" + } + ); export type CreateSiteResourceBody = z.infer; export type CreateSiteResourceResponse = SiteResource;