diff --git a/server/db/schema.ts b/server/db/schema.ts index 05825c3f..479b6604 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -44,8 +44,8 @@ export const sites = sqliteTable("sites", { type: text("type").notNull(), // "newt" or "wireguard" online: integer("online", { mode: "boolean" }).notNull().default(false), - // exit node stuff that is how to connect to the site when it has a gerbil - address: text("address"), // this is the address of the wireguard interface in gerbil + // exit node stuff that is how to connect to the site when it has a wg server + address: text("address"), // this is the address of the wireguard interface in newt endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config publicKey: text("pubicKey"), lastHolePunch: integer("lastHolePunch"), diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 132cd344..627f4b1f 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -80,7 +80,7 @@ export async function createClient( ); } - if (subnet && !isValidIP(subnet)) { + if (!isValidIP(subnet)) { return next( createHttpError( HttpCode.BAD_REQUEST, @@ -103,7 +103,7 @@ export async function createClient( ); } - if (subnet && !isIpInCidr(subnet, org.subnet)) { + if (!isIpInCidr(subnet, org.subnet)) { return next( createHttpError( HttpCode.BAD_REQUEST, @@ -114,6 +114,22 @@ export async function createClient( const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org + // make sure the subnet is unique + const subnetExists = await db + .select() + .from(clients) + .where(eq(clients.subnet, updatedSubnet)) + .limit(1); + + if (subnetExists.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + `Subnet ${subnet} already exists` + ) + ); + } + await db.transaction(async (trx) => { // TODO: more intelligent way to pick the exit node diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index f4b08f49..f4cec201 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -5,7 +5,6 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { generateId } from "@server/auth/sessions/app"; import { getNextAvailableClientSubnet } from "@server/lib/ip"; -import config from "@server/lib/config"; import { z } from "zod"; import { fromError } from "zod-validation-error"; @@ -43,6 +42,14 @@ export async function pickClientDefaults( const secret = generateId(48); const newSubnet = await getNextAvailableClientSubnet(orgId); + if (!newSubnet) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "No available subnet found" + ) + ); + } const subnet = newSubnet.split("/")[0]; diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index eb27bc64..7c15d875 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -64,45 +64,15 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { return; } - let site: Site | undefined; - if (!existingSite.address) { - // This is a new site configuration - let address = await getNextAvailableClientSubnet(existingSite.orgId); - if (!address) { - logger.error("handleGetConfigMessage: No available address"); - return; - } - - // TODO: WE NEED TO PULL THE CIDR FROM THE DB SUBNET ON THE ORG INSTEAD BECAUSE IT CAN BE DIFFERENT - // TODO: SOMEHOW WE NEED TO ALLOW THEM TO PUT IN THEIR OWN ADDRESS - address = `${address.split("/")[0]}/${config.getRawConfig().orgs.block_size}`; // we want the block size of the whole org - - // Update the site with new WireGuard info - const [updateRes] = await db - .update(sites) - .set({ - publicKey, - address, - listenPort: port - }) - .where(eq(sites.siteId, siteId)) - .returning(); - - site = updateRes; - logger.info(`Updated site ${siteId} with new WG Newt info`); - } else { - // update the endpoint and the public key - const [siteRes] = await db - .update(sites) - .set({ - publicKey, - listenPort: port - }) - .where(eq(sites.siteId, siteId)) - .returning(); - - site = siteRes; - } + // update the endpoint and the public key + const [site] = await db + .update(sites) + .set({ + publicKey, + listenPort: port + }) + .where(eq(sites.siteId, siteId)) + .returning(); if (!site) { logger.error("handleGetConfigMessage: Failed to update site"); @@ -139,7 +109,9 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { const peerData = { publicKey: client.clients.pubKey!, allowedIps: [client.clients.subnet!], - endpoint: client.clientSites.isRelayed ? "" : client.clients.endpoint! // if its relayed it should be localhost + endpoint: client.clientSites.isRelayed + ? "" + : client.clients.endpoint! // if its relayed it should be localhost }; // Add or update this peer on the olm if it is connected diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 457c7fe4..452c4c86 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -71,7 +71,7 @@ export async function createOrg( const { orgId, name, subnet } = parsedBody.data; - if (subnet && !isValidCIDR(subnet)) { + if (!isValidCIDR(subnet)) { return next( createHttpError( HttpCode.BAD_REQUEST, @@ -80,6 +80,22 @@ export async function createOrg( ); } + // make sure the subnet is unique + const subnetExists = await db + .select() + .from(orgs) + .where(eq(orgs.subnet, subnet)) + .limit(1); + + if (subnetExists.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + `Subnet ${subnet} already exists` + ) + ); + } + // make sure the orgId is unique const orgExists = await db .select() diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index f79149cc..cb309b2d 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -1,7 +1,14 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles, userSites, sites, roleSites, Site } from "@server/db/schema"; +import { + roles, + userSites, + sites, + roleSites, + Site, + orgs +} from "@server/db/schema"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -13,6 +20,8 @@ import { fromError } from "zod-validation-error"; import { newts } from "@server/db/schema"; import moment from "moment"; import { hashPassword } from "@server/auth/password"; +import { isValidIP } from "@server/lib/validators"; +import { isIpInCidr } from "@server/lib/ip"; const createSiteParamsSchema = z .object({ @@ -34,6 +43,7 @@ const createSiteSchema = z subnet: z.string().optional(), newtId: z.string().optional(), secret: z.string().optional(), + address: z.string().optional(), type: z.enum(["newt", "wireguard", "local"]) }) .strict(); @@ -58,8 +68,16 @@ export async function createSite( ); } - const { name, type, exitNodeId, pubKey, subnet, newtId, secret } = - parsedBody.data; + const { + name, + type, + exitNodeId, + pubKey, + subnet, + newtId, + secret, + address + } = parsedBody.data; const parsedParams = createSiteParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -79,6 +97,53 @@ export async function createSite( ); } + const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId)); + + if (!org) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Organization with ID ${orgId} not found` + ) + ); + } + + if (address) { + if (!isValidIP(address)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid subnet format. Please provide a valid CIDR notation." + ) + ); + } + + if (!isIpInCidr(address, org.subnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IP is not in the CIDR range of the subnet." + ) + ); + } + + // make sure the subnet is unique + const addressExists = await db + .select() + .from(sites) + .where(eq(sites.address, address)) + .limit(1); + + if (addressExists.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + `Subnet ${subnet} already exists` + ) + ); + } + } + const niceId = await getUniqueSiteName(orgId); await db.transaction(async (trx) => { @@ -102,6 +167,7 @@ export async function createSite( exitNodeId, name, niceId, + address: address || null, subnet, type, ...(pubKey && type == "wireguard" && { pubKey }) @@ -116,6 +182,7 @@ export async function createSite( orgId, name, niceId, + address: address || null, type, subnet: "0.0.0.0/0" }) diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index e0d8b03c..d8f50ba8 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -6,9 +6,11 @@ import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; -import { findNextAvailableCidr } from "@server/lib/ip"; +import { findNextAvailableCidr, getNextAvailableClientSubnet } from "@server/lib/ip"; import { generateId } from "@server/auth/sessions/app"; import config from "@server/lib/config"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; export type PickSiteDefaultsResponse = { exitNodeId: number; @@ -20,14 +22,32 @@ export type PickSiteDefaultsResponse = { subnet: string; newtId: string; newtSecret: string; + clientAddress: string; }; +const pickSiteDefaultsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + export async function pickSiteDefaults( req: Request, res: Response, next: NextFunction ): Promise { try { + const parsedParams = pickSiteDefaultsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; // TODO: more intelligent way to pick the exit node // make sure there is an exit node by counting the exit nodes table @@ -73,6 +93,18 @@ export async function pickSiteDefaults( ); } + const newClientAddress = await getNextAvailableClientSubnet(orgId); + if (!newClientAddress) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "No available subnet found" + ) + ); + } + + const clientAddress = newClientAddress.split("/")[0]; + const newtId = generateId(15); const secret = generateId(48); @@ -86,6 +118,7 @@ export async function pickSiteDefaults( endpoint: exitNode.endpoint, // subnet: `${newSubnet.split("/")[0]}/${config.getRawConfig().gerbil.block_size}`, // we want the block size of the whole subnet subnet: newSubnet, + clientAddress: clientAddress, newtId, newtSecret: secret }, diff --git a/src/app/[orgId]/settings/clients/CreateClientsForm.tsx b/src/app/[orgId]/settings/clients/CreateClientsForm.tsx index 76ac6d93..b1787cef 100644 --- a/src/app/[orgId]/settings/clients/CreateClientsForm.tsx +++ b/src/app/[orgId]/settings/clients/CreateClientsForm.tsx @@ -264,7 +264,7 @@ export default function CreateClientForm({ name="subnet" render={({ field }) => ( - Subnet + Address - The subnet that this client will use for connectivity. + The address that this client will use for connectivity. diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index acafd5bb..2e57c6d8 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -69,7 +69,8 @@ const createSiteFormSchema = z message: "Name must not be longer than 30 characters." }), method: z.string(), - copied: z.boolean() + copied: z.boolean(), + clientAddress: z.string().optional() }) .refine( (data) => { @@ -130,7 +131,7 @@ export default function Page() { const [newtId, setNewtId] = useState(""); const [newtSecret, setNewtSecret] = useState(""); const [newtEndpoint, setNewtEndpoint] = useState(""); - + const [clientAddress, setClientAddress] = useState(""); const [publicKey, setPublicKey] = useState(""); const [privateKey, setPrivateKey] = useState(""); const [wgConfig, setWgConfig] = useState(""); @@ -315,7 +316,8 @@ PersistentKeepalive = 5`; defaultValues: { name: "", copied: false, - method: "newt" + method: "newt", + clientAddress: "" } }); @@ -324,7 +326,7 @@ PersistentKeepalive = 5`; let payload: CreateSiteBody = { name: data.name, - type: data.method as any, + type: data.method as any }; if (data.method == "wireguard") { @@ -361,7 +363,8 @@ PersistentKeepalive = 5`; subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, secret: siteDefaults.newtSecret, - newtId: siteDefaults.newtId + newtId: siteDefaults.newtId, + address: clientAddress }; } @@ -431,10 +434,12 @@ PersistentKeepalive = 5`; const newtId = data.newtId; const newtSecret = data.newtSecret; const newtEndpoint = data.endpoint; + const clientAddress = data.clientAddress; setNewtId(newtId); setNewtSecret(newtSecret); setNewtEndpoint(newtEndpoint); + setClientAddress(clientAddress); hydrateCommands( newtId, @@ -617,18 +622,6 @@ PersistentKeepalive = 5`; - - - - Save Your Credentials - - - You will only be able to see - this once. Make sure to copy it - to a secure place. - - -
)} /> + ( + + + Client Address + + + { + setClientAddress( + e + .target + .value + ); + field.onChange( + e + .target + .value + ); + }} + /> + + + + Specify the IP + address of the + host. + + + )} + /> - - - - - Install Newt - - - Get Newt running on your system - - - -
-

- Operating System -

-
- {[ - "linux", - "docker", - "mac", - "windows", - "freebsd" - ].map((os) => ( - - ))} -
-
- -
-

- {platform === "docker" - ? "Method" - : "Architecture"} -

-
- {getArchitectures().map( - (arch) => ( - - ) - )} -
-
-

- Commands -

-
- -
-
-
-
-
)}